Skip to content

Automated Test: rest-serializer-enhancement-post #332

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/assets/javascripts/admin/adapters/embedding.js.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import RestAdapter from 'discourse/adapters/rest';

export default RestAdapter.extend({
pathFor() {
return "/admin/customize/embedding";
}
});
63 changes: 63 additions & 0 deletions app/assets/javascripts/admin/components/embeddable-host.js.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { bufferedProperty } from 'discourse/mixins/buffered-content';
import computed from 'ember-addons/ember-computed-decorators';
import { on, observes } from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';

export default Ember.Component.extend(bufferedProperty('host'), {
editToggled: false,
tagName: 'tr',
categoryId: null,

editing: Ember.computed.or('host.isNew', 'editToggled'),

@on('didInsertElement')
@observes('editing')
_focusOnInput() {
Ember.run.schedule('afterRender', () => { this.$('.host-name').focus(); });
},

@computed('buffered.host', 'host.isSaving')
cantSave(host, isSaving) {
return isSaving || Ember.isEmpty(host);
},

actions: {
edit() {
this.set('categoryId', this.get('host.category.id'));
this.set('editToggled', true);
},

save() {
if (this.get('cantSave')) { return; }

const props = this.get('buffered').getProperties('host');
props.category_id = this.get('categoryId');

const host = this.get('host');
host.save(props).then(() => {
host.set('category', Discourse.Category.findById(this.get('categoryId')));
this.set('editToggled', false);
}).catch(popupAjaxError);
},

delete() {
bootbox.confirm(I18n.t('admin.embedding.confirm_delete'), (result) => {
if (result) {
this.get('host').destroyRecord().then(() => {
this.sendAction('deleteHost', this.get('host'));
});
}
});
},

cancel() {
const host = this.get('host');
if (host.get('isNew')) {
this.sendAction('deleteHost', host);
} else {
this.rollbackBuffer();
this.set('editToggled', false);
}
}
}
});
18 changes: 18 additions & 0 deletions app/assets/javascripts/admin/controllers/admin-embedding.js.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default Ember.Controller.extend({
embedding: null,

actions: {
saveChanges() {
this.get('embedding').update({});
},

addHost() {
const host = this.store.createRecord('embeddable-host');
this.get('embedding.embeddable_hosts').pushObject(host);
},

deleteHost(host) {
this.get('embedding.embeddable_hosts').removeObject(host);
}
}
});
9 changes: 9 additions & 0 deletions app/assets/javascripts/admin/routes/admin-embedding.js.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default Ember.Route.extend({
model() {
return this.store.find('embedding');
},

setupController(controller, model) {
controller.set('embedding', model);
}
});
1 change: 1 addition & 0 deletions app/assets/javascripts/admin/routes/admin-route-map.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default {
this.resource('adminUserFields', { path: '/user_fields' });
this.resource('adminEmojis', { path: '/emojis' });
this.resource('adminPermalinks', { path: '/permalinks' });
this.resource('adminEmbedding', { path: '/embedding' });
});
this.route('api');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{{#if editing}}
<td>
{{input value=buffered.host placeholder="example.com" enter="save" class="host-name"}}
</td>
<td>
{{category-chooser value=categoryId}}
</td>
<td>
{{d-button icon="check" action="save" class="btn-primary" disabled=cantSave}}
{{d-button icon="times" action="cancel" class="btn-danger" disabled=host.isSaving}}
</td>
{{else}}
<td>{{host.host}}</td>
<td>{{category-badge host.category}}</td>
<td>
{{d-button icon="pencil" action="edit"}}
{{d-button icon="trash-o" action="delete" class='btn-danger'}}
</td>
{{/if}}
1 change: 1 addition & 0 deletions app/assets/javascripts/admin/templates/customize.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
{{nav-item route='adminEmbedding' label='admin.embedding.title'}}
{{/admin-nav}}

<div class="admin-container">
Expand Down
15 changes: 15 additions & 0 deletions app/assets/javascripts/admin/templates/embedding.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{{#if embedding.embeddable_hosts}}
<table>
<tr>
<th style='width: 50%'>{{i18n "admin.embedding.host"}}</th>
<th style='width: 30%'>{{i18n "admin.embedding.category"}}</th>
<th style='width: 20%'>&nbsp;</th>
</tr>
{{#each embedding.embeddable_hosts as |host|}}
{{embeddable-host host=host deleteHost="deleteHost"}}
{{/each}}
</table>
{{/if}}

{{d-button label="admin.embedding.add_host" action="addHost" icon="plus" class="btn-primary"}}

4 changes: 2 additions & 2 deletions app/assets/javascripts/discourse/adapters/rest.js.es6
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const ADMIN_MODELS = ['plugin', 'site-customization'];
const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host'];

export function Result(payload, responseJson) {
this.payload = payload;
Expand All @@ -19,7 +19,7 @@ function rethrow(error) {
export default Ember.Object.extend({

basePath(store, type) {
if (ADMIN_MODELS.indexOf(type) !== -1) { return "/admin/"; }
if (ADMIN_MODELS.indexOf(type.replace('_', '-')) !== -1) { return "/admin/"; }
return "/";
},

Expand Down
18 changes: 14 additions & 4 deletions app/assets/javascripts/discourse/models/store.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,24 @@ export default Ember.Object.extend({
_hydrateEmbedded(type, obj, root) {
const self = this;
Object.keys(obj).forEach(function(k) {
const m = /(.+)\_id$/.exec(k);
const m = /(.+)\_id(s?)$/.exec(k);
if (m) {
const subType = m[1];
const hydrated = self._lookupSubType(subType, type, obj[k], root);
if (hydrated) {
obj[subType] = hydrated;

if (m[2]) {
const hydrated = obj[k].map(function(id) {
return self._lookupSubType(subType, type, id, root);
});
obj[self.pluralize(subType)] = hydrated || [];
delete obj[k];
} else {
const hydrated = self._lookupSubType(subType, type, obj[k], root);
if (hydrated) {
obj[subType] = hydrated;
delete obj[k];
}
}
Comment on lines +196 to 208

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Plural branch is less defensive than the singular branch.

A few concerns with the _ids path:

  1. No guard on obj[k] — if a key ends in _ids but the value is null or not an array, obj[k].map(...) (line 197) will throw a TypeError. The singular branch safely checks if (hydrated) before mutating.

  2. hydrated || [] on line 200 is unreachable.map() always returns an array, so the fallback never triggers.

  3. Asymmetric delete semantics — the plural branch unconditionally deletes obj[k] and assigns the hydrated array (which may contain undefined entries if lookups fail), while the singular branch only replaces when hydration succeeds. This means a failed plural lookup silently loses the original IDs.

Suggested defensive fix
         if (m[2]) {
-          const hydrated = obj[k].map(function(id) {
-            return self._lookupSubType(subType, type, id, root);
-          });
-          obj[self.pluralize(subType)] = hydrated || [];
-          delete obj[k];
+          const ids = obj[k];
+          if (Array.isArray(ids)) {
+            const hydrated = ids.map(function(id) {
+              return self._lookupSubType(subType, type, id, root);
+            });
+            obj[self.pluralize(subType)] = hydrated;
+            delete obj[k];
+          }
         } else {
🤖 Prompt for AI Agents
In `@app/assets/javascripts/discourse/models/store.js.es6` around lines 196 - 208,
The plural `_ids` branch is not defensive: it calls obj[k].map without checking
obj[k], uses an unreachable `hydrated || []`, and unconditionally deletes obj[k]
even if lookups fail. Update the plural branch in the block that references
m[2], subType, type, root, self._lookupSubType and self.pluralize to first
verify Array.isArray(obj[k]); if true, map to hydrated items, filter out falsy
results, and only assign obj[self.pluralize(subType)] and delete obj[k] when the
filtered hydrated array has at least one valid element; otherwise leave obj[k]
unchanged (do not delete) to mirror the singular branch semantics and remove the
unnecessary `|| []` fallback.


}
});
},
Expand Down
34 changes: 34 additions & 0 deletions app/controllers/admin/embeddable_hosts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class Admin::EmbeddableHostsController < Admin::AdminController

before_filter :ensure_logged_in, :ensure_staff

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find the Admin::AdminController file
fd -t f "*admin_controller*" --max-one-result | head -20

Repository: admin-coderabbit/coderabbit-discourse-mirror

Length of output: 321


🏁 Script executed:

# Search for Admin::AdminController class definition
rg -n "class Admin::AdminController" --type ruby

Repository: admin-coderabbit/coderabbit-discourse-mirror

Length of output: 184


🏁 Script executed:

# Check the embeddable_hosts_controller specifically
cat -n app/controllers/admin/embeddable_hosts_controller.rb

Repository: admin-coderabbit/coderabbit-discourse-mirror

Length of output: 1145


🏁 Script executed:

# Check the Admin::AdminController for before_filter/before_action
cat -n app/controllers/admin/admin_controller.rb | head -50

Repository: admin-coderabbit/coderabbit-discourse-mirror

Length of output: 324


🏁 Script executed:

# Search for all before_filter/before_action in admin_controller
rg -n "before_filter|before_action" app/controllers/admin/admin_controller.rb

Repository: admin-coderabbit/coderabbit-discourse-mirror

Length of output: 155


🏁 Script executed:

# Check Rails version in the project
cat Gemfile | grep -i rails

Repository: admin-coderabbit/coderabbit-discourse-mirror

Length of output: 963


Remove redundant before_filter declarations — they're already enforced by Admin::AdminController.

Admin::AdminController (lines 3-4) already defines before_filter :ensure_logged_in and before_filter :ensure_staff, making these filters in the child controller redundant. Additionally, before_filter is deprecated; use before_action if needed elsewhere. Delete lines 3 of this file.

🤖 Prompt for AI Agents
In `@app/controllers/admin/embeddable_hosts_controller.rb` at line 3, Remove the
redundant before_filter declaration in the controller: delete the line calling
"before_filter :ensure_logged_in, :ensure_staff" because Admin::AdminController
already defines those filters; if you do need to add filters elsewhere prefer
the modern "before_action" API instead of before_filter.


def create
save_host(EmbeddableHost.new)
end

def update
host = EmbeddableHost.where(id: params[:id]).first
save_host(host)
end
Comment on lines +9 to +12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing nil guard — update will raise NoMethodError if the host doesn't exist.

EmbeddableHost.where(id: params[:id]).first returns nil when the record is not found. Passing nil into save_host will blow up on line 23 with NoMethodError: undefined method 'host=' for nil.

Use find to get a 404 via RecordNotFound, or add a nil check.

Proposed fix
  def update
-   host = EmbeddableHost.where(id: params[:id]).first
+   host = EmbeddableHost.find(params[:id])
    save_host(host)
  end
🤖 Prompt for AI Agents
In `@app/controllers/admin/embeddable_hosts_controller.rb` around lines 9 - 12,
The update action calls EmbeddableHost.where(id: params[:id]).first and then
unconditionally passes the result to save_host, which will raise NoMethodError
if nil; change this to either use EmbeddableHost.find(params[:id]) so
ActiveRecord raises RecordNotFound (returning a 404) or add a nil guard: check
the host (result of EmbeddableHost.where(...).first) and render a 404 or
appropriate error response before calling save_host. Ensure you update the
update method and keep save_host unchanged.


def destroy
host = EmbeddableHost.where(id: params[:id]).first
host.destroy
render json: success_json
end
Comment on lines +14 to +18

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same nil guard issue in destroy — will 500 if the record doesn't exist.

host can be nil, causing NoMethodError on host.destroy.

Proposed fix
  def destroy
-   host = EmbeddableHost.where(id: params[:id]).first
+   host = EmbeddableHost.find(params[:id])
    host.destroy
    render json: success_json
  end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def destroy
host = EmbeddableHost.where(id: params[:id]).first
host.destroy
render json: success_json
end
def destroy
host = EmbeddableHost.find(params[:id])
host.destroy
render json: success_json
end
🤖 Prompt for AI Agents
In `@app/controllers/admin/embeddable_hosts_controller.rb` around lines 14 - 18,
The destroy action calls host.destroy without guarding against a missing record,
which raises NoMethodError when EmbeddableHost.where(id: params[:id]).first
returns nil; update the destroy method (reference: destroy, EmbeddableHost,
params[:id], success_json) to check for a nil host (e.g., use find_by or the
existing query) and return an appropriate 404 or error response (render json
with an error or head :not_found) when host is nil, otherwise call destroy and
render success_json.


protected

def save_host(host)
host.host = params[:embeddable_host][:host]
host.category_id = params[:embeddable_host][:category_id]
host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank?

if host.save
render_serialized(host, EmbeddableHostSerializer, root: 'embeddable_host', rest_serializer: true)
else
render_json_error(host)
end
end

end
21 changes: 21 additions & 0 deletions app/controllers/admin/embedding_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class Admin::EmbeddingController < Admin::AdminController

before_filter :ensure_logged_in, :ensure_staff, :fetch_embedding

def show
render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true)
end

def update
render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true)
end
Comment on lines +9 to +11

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

update action is a no-op — it never processes request params.

The action re-renders the current embedding state without reading or persisting any submitted data. If the intent is to update embedding-level settings in the future, this is a placeholder; if individual host CRUD is entirely handled via embeddable_hosts resource, this is just misleading. Consider either adding the update logic or making this action explicit about returning the current state (e.g., aliasing to show).

🤖 Prompt for AI Agents
In `@app/controllers/admin/embedding_controller.rb` around lines 9 - 11, The
update action in the controller (def update) is a no-op because it never
consumes params or persists changes; either implement proper update logic or
make it explicit that it simply returns the current resource. To fix: if update
should persist changes, read permitted params (e.g., embedding_params), call
`@embedding.update`!(embedding_params) (or update and handle errors), then
render_serialized(`@embedding`, EmbeddingSerializer, root: 'embedding',
rest_serializer: true); otherwise replace or alias update to the show action (or
return a 405/explicit message) so callers aren’t misled. Reference: the update
method, render_serialized call, EmbeddingSerializer, and the embeddable_hosts
resource mentioned in the comment.


protected

def fetch_embedding
@embedding = OpenStruct.new({
id: 'default',
embeddable_hosts: EmbeddableHost.all.order(:host)
})
end
end
3 changes: 1 addition & 2 deletions app/controllers/embed_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ def count
def ensure_embeddable

if !(Rails.env.development? && current_user.try(:admin?))
raise Discourse::InvalidAccess.new('embeddable hosts not set') if SiteSetting.embeddable_hosts.blank?
raise Discourse::InvalidAccess.new('invalid referer host') unless SiteSetting.allows_embeddable_host?(request.referer)
raise Discourse::InvalidAccess.new('invalid referer host') unless EmbeddableHost.host_allowed?(request.referer)
end

response.headers['X-Frame-Options'] = "ALLOWALL"
Expand Down
24 changes: 24 additions & 0 deletions app/models/embeddable_host.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class EmbeddableHost < ActiveRecord::Base
validates_format_of :host, :with => /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\Z/i

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

TLD regex caps at 5 characters, excluding valid TLDs like .museum or .technology.

The regex [a-z]{2,5} restricts top-level domains to 2–5 characters. Newer gTLDs can be much longer. Consider relaxing to {2,} or {2,24} if you want an upper bound.

🤖 Prompt for AI Agents
In `@app/models/embeddable_host.rb` at line 2, The host validation regex in
embeddable_host.rb uses [a-z]{2,5} which wrongly limits TLD length; update the
validates_format_of :host pattern to allow longer TLDs (e.g. change the TLD part
to [a-z]{2,} or a bounded upper limit like [a-z]{2,24}) so the :host validation
accepts modern gTLDs while keeping the rest of the pattern intact.

belongs_to :category

before_validation do
self.host.sub!(/^https?:\/\//, '')
self.host.sub!(/\/.*$/, '')
end
Comment on lines +5 to +8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

NoMethodError if host is nil.

The before_validation callback calls self.host.sub! without a nil guard. If a record is created with a nil host, this will raise NoMethodError before the format validation can reject it.

Proposed fix
   before_validation do
+    return if self.host.blank?
     self.host.sub!(/^https?:\/\//, '')
     self.host.sub!(/\/.*$/, '')
   end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
before_validation do
self.host.sub!(/^https?:\/\//, '')
self.host.sub!(/\/.*$/, '')
end
before_validation do
return if self.host.blank?
self.host.sub!(/^https?:\/\//, '')
self.host.sub!(/\/.*$/, '')
end
🤖 Prompt for AI Agents
In `@app/models/embeddable_host.rb` around lines 5 - 8, The before_validation
callback can raise NoMethodError when host is nil because it calls
self.host.sub! directly; guard against nil by skipping the substitutions when
host is blank (e.g., add a nil/blank check at the start of the before_validation
block: return unless host.present?), or use safe navigation
(self.host&.sub!(/^https?:\/\//, '') and self.host&.sub!(/\/.*$/, '')) so
records with nil host don't blow up and normal validations can run.


def self.record_for_host(host)
uri = URI(host) rescue nil
return false unless uri.present?

host = uri.host
return false unless host.present?

where("lower(host) = ?", host).first
end

def self.host_allowed?(host)
record_for_host(host).present?
end

end
14 changes: 0 additions & 14 deletions app/models/site_setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,6 @@ def self.anonymous_menu_items
@anonymous_menu_items ||= Set.new Discourse.anonymous_filters.map(&:to_s)
end

def self.allows_embeddable_host?(host)
return false if embeddable_hosts.blank?
uri = URI(host) rescue nil
return false unless uri.present?

host = uri.host
return false unless host.present?

!!embeddable_hosts.split("\n").detect {|h| h.sub(/^https?\:\/\//, '') == host }

hosts = embeddable_hosts.split("\n").map {|h| (URI(h).host rescue nil) || h }
!!hosts.detect {|h| h == host}
end

def self.anonymous_homepage
top_menu_items.map { |item| item.name }
.select { |item| anonymous_menu_items.include?(item) }
Expand Down
2 changes: 1 addition & 1 deletion app/models/topic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,7 @@ def has_topic_embed?
end

def expandable_first_post?
SiteSetting.embeddable_hosts.present? && SiteSetting.embed_truncate? && has_topic_embed?
SiteSetting.embed_truncate? && has_topic_embed?
end

TIME_TO_FIRST_RESPONSE_SQL ||= <<-SQL
Expand Down
4 changes: 3 additions & 1 deletion app/models/topic_embed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ def self.import(user, url, title, contents)
# If there is no embed, create a topic, post and the embed.
if embed.blank?
Topic.transaction do
eh = EmbeddableHost.record_for_host(url)

creator = PostCreator.new(user,
title: title,
raw: absolutize_urls(url, contents),
skip_validations: true,
cook_method: Post.cook_methods[:raw_html],
category: SiteSetting.embed_category)
category: eh.try(:category_id))
post = creator.create
if post.present?
TopicEmbed.create!(topic_id: post.topic_id,
Expand Down
16 changes: 16 additions & 0 deletions app/serializers/embeddable_host_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class EmbeddableHostSerializer < ApplicationSerializer
attributes :id, :host, :category_id

def id
object.id
end

def host
object.host
end

def category_id
object.category_id
end
end

8 changes: 8 additions & 0 deletions app/serializers/embedding_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class EmbeddingSerializer < ApplicationSerializer
attributes :id
has_many :embeddable_hosts, serializer: EmbeddableHostSerializer, embed: :ids

def id
object.id
end
end
8 changes: 8 additions & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2491,6 +2491,14 @@ en:
image: "Image"
delete_confirm: "Are you sure you want to delete the :%{name}: emoji?"

embedding:
confirm_delete: "Are you sure you want to delete that host?"
title: "Embedding"
host: "Allowed Hosts"
edit: "edit"
category: "Post to Category"
add_host: "Add Host"

permalink:
title: "Permalinks"
url: "URL"
Expand Down
2 changes: 0 additions & 2 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1164,13 +1164,11 @@ en:
autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language."
highlighted_languages: "Included syntax highlighting rules. (Warning: including too many langauges may impact performance) see: https://highlightjs.org/static/demo/ for a demo"

embeddable_hosts: "Host(s) that can embed the comments from this Discourse forum. Hostname only, do not begin with http://"
feed_polling_enabled: "EMBEDDING ONLY: Whether to embed a RSS/ATOM feed as posts."
feed_polling_url: "EMBEDDING ONLY: URL of RSS/ATOM feed to embed."
embed_by_username: "Discourse username of the user who creates the embedded topics."
embed_username_key_from_feed: "Key to pull discourse username from feed."
embed_truncate: "Truncate the embedded posts."
embed_category: "Category of embedded topics."
embed_post_limit: "Maximum number of posts to embed."
embed_whitelist_selector: "CSS selector for elements that are allowed in embeds."
embed_blacklist_selector: "CSS selector for elements that are removed from embeds."
Expand Down
Loading