By default, we want all responses to have the `Cache-Control` header set
to `no-cache, no-store`. Individual controller actions can override the
header when need be.
When a `PUT`, `POST`, or `DELETE` operation doesn't need to return any
data, we've historically either returned nothing, or `{ success: "OK"
}`.
A more consistent way to return the same data would be with a 204 status
response. This gives the same information as the `{ success: "OK" }`
body (ie, that the operation successfully completed), without needing to
read or parse the response body.
This change adds a 204 response for `Admin::SiteSettingsController`.
Additional controllers could be migrated in follow-up PRs, or on an
ad-hoc basis.
Instead of having to clean an array in a contract using a
`before_validation` block, for example, we can now pass `compact_blank:
true` to the attribute, like this:
```ruby
attribute :ids, :array, compact_blank: true
```
If, for some reasons, you were to try to "unsuspend" a moderator, you
wouldn't be able to because the "can_suspend" was checking for
"user.regular?".
Added a "can_unsuspend?" to support this use-case.
Ref - https://meta.discourse.org/t/-/385786
Category moderators (who aren't staff member) are able to a topic timer
to automatically delete replies after a certain amount of time but the
background job (DeleteReplies) was deleting the "topic timer" because
the category moderators wasn't a staff member.
There was a discrepency between the UX who used "can_delete" to
show/hide the "topic timer" option and the back-end who was checking for
"staff" membership.
This fixes it by changing the backend to use the guardian's "can_delete"
method instead.
Internal ref - t/165077
This creates an overriding `rswag:specs:swaggerize` rake task that also
adds plugin paths, and updates spec helpers to handle plugin paths.
Also adds the spec files for the discourse-calendar events index
endpoint.
### Testing
Running `rake rswag:specs:swaggerize` now generates the same
`openapi/openapi.yaml` file, with `/discourse-post-event/events.ics` and
`/discourse-post-event/events.json` GET documentation.
Using `store_translations` to override translations causes overridden
strings to leak between tests and generate flaky failures. I initially
tried `I18n.reload!` in an `ensure` block around the specs that override
translations, but for some reasons it didn't fix the issue and I don't
want to spend too much time digging into `I18n` internals and our
patches to figure this out. Let's switch to the `TranslationOverride`
system which does the same thing as the `store_translations` method but
in a way that doesn't leak.
To get the specs in questions to fail, use seed 21911:
```
bin/turbo_rspec --seed 21911 spec/requests/application_controller_spec.rb
```
When validating the openapi spec we were getting the following errors:
```
{
"messages": [
"attribute paths.'/categories.json'(post).responses.200.content.'application/json'.schema. is not of type `object`",
"attribute paths.'/categories/{id}.json'(put).responses.200.content.'application/json'.schema. is not of type `object`",
"attribute paths.'/c/{id}/show.json'(get).responses.200.content.'application/json'.schema. is not of type `object`",
"attribute paths.'/posts.json'(get).responses.200.content.'application/json'.schema.items is not of type `object`",
"attribute paths.'/groups/by-id/{id}.json'(get).operationId is repeated"
]
}
```
These changes resolve those openapi spec validation errors.
See:
https://github.com/discourse/discourse_api_docs/pull/139#issuecomment-3393138056
This adds a new notification's user preference to allow users to control
whether they are notified whenever one of their topic is being linked in
another post.
Internal ref - t/160259
When making requests to `/t/:id/status.json`, the behaviour of the
`enabled` parameter is a bit confusing: only the string `"true"` is
treated as a true value, everything else is false (including boolean
`true`, for example).
This change makes use of the ActiveModel Boolean type to cast
falsey/truthy values to `false` and `true`.
This splits the Login and Authentication admin page into multiple
sections: Settings, social logins, DiscourseConnect, OAuth2.0, OIDC.
Internal ticket `t/161648`
This PR also adds a new extension point for plugins that want to extend
this area of the app.
```
register_site_setting_area("oauth2")
register_admin_config_login_route("oauth2")
```
Adding this to a plugin allows it to extend both the setting areas
(existing functionality) and add a tab to this admin settings screen via
`register_admin_config_login_route`.
This commit splits the `moderators_manage_categories_and_groups` setting
into 2 separate settings to allow for more precise control over
categories and groups management by moderators.
Internal topic: t/141392.
This commit allows editing colors of palettes that are installed with
themes. Prior to this commit, editing colors of theme-owned palettes wasn't
allowed because a theme update could override the edits made by admins
and there was no way to revert edits to the original values. With this
commit, all of that is solved by copying the palette when it's first edited
by an admin, and making future updates to the theme update the original
copy only with the ability for admins to revert to the colors in the
original copy at any time.
Internal topic: t/162130.
We can't enable `Rails/WhereNot` lint/autofix, because it would break
code that uses mini_sql instead of AR (which rubocop, and tbh also we,
can't easily differentiate)
Those are safe because they either:
* are executed in AR model scope definitions
* are clearly chained starting from a AR model
* are less-clearly chained, but still can be traced to a AR model/scope
---------
Co-authored-by: Loïc Guitaut <loic@discourse.org>
This commit adds a new setting `moderators_change_trust_levels` to
control whether moderators are allowed to change trust level of users.
Moderators are currently allowed to change trust levels, so this new
setting is enabled by default to avoid sudden changes in behavior for
existing sites.
When the setting is disabled and moderators are not allowed to change
trust levels, they see the trust levels dropdown disabled.
Internal topic: t/141392.
---------
Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
Now that `ServerSession` can store arbitrary data, we can move some more
data into it.
This patch moves some data related to authentication into it, as
sometimes that kind of data can be pretty big.
Followup 2a65bf4522
With the above change, we inadvertently made it so that
when you edited the topic title or category, we bumped
the topic.
This fix makes it so we do not bump topics when only title, category,
or tags are changed, to restore the previous behaviour.
In addition, we are making it so the following settings no longer
influence topic bump behaviour, keeping only their intended notification
behaviour around. The site setting description never mentioned anything
about topic bumps:
* disable_category_edit_notifications
* disable_tag_edit_notifications
We have this slightly awkward UX on the Preferences > Security page, where we offer to remove the password from an account if it has other means of signing in. You need to first click a cryptic Show button which may or may not lead you to the button you want. This button loads the associated accounts so we can evaluate whether to show the Remove password button or not.
This PR loads that data when entering the route, so that we can just show the button up-front. If removing password isn't possible, the button is disabled and shows an explainer underneath.
Added subfolder support for #discourse-id automated registration in
874c875e02 but we were missing returning the "path" in the response
from the "discourse_id_challenge".
This ensures we also return the "path" field in the challenge so it can
properly be validatated by id.discourse.com.
Internal ref - t/161934/21
Now that `ServerSession` can store arbitrary data, we can move some more
data into it.
This PR moves some data related to authentication into it, as sometimes
that kind of data can be pretty big.
## 🔍 Overview
This PR adds bulk selection functionality to the PostList component and
implements optimized bulk deletion for the drafts page. Users can now
select multiple drafts and delete them all at once with a single network
request, significantly improving performance and user experience.
The implementation includes:
- A new reusable bulk selection system for PostList components
- Optimized bulk delete endpoint that reduces network requests by 90%
- Comprehensive bulk controls UI with select all/clear all functionality
- Shift+click range selection similar to topic lists
- Complete test coverage for all new functionality
## ➕ More details
**Bulk Selection System**
The PostList component now supports optional bulk selection through
these new parameters:
- `@bulkSelectEnabled={{true}}` - Shows checkboxes next to each post
- `@bulkSelectHelper={{helper}}` - Manages selection state (use
`PostBulkSelectHelper`)
- `@bulkActions={{actions}}` - Array of bulk action objects for the
dropdown menu
**Usage Example:**
```gjs
import Component from "@glimmer/component";
import { action } from "@ember/object";
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import PostBulkSelectHelper from "discourse/lib/post-bulk-select-helper";
export default class MyComponent extends Component {
bulkSelectHelper = new PostBulkSelectHelper(this);
constructor() {
super(...arguments);
// Initial updatePosts call
this.updateBulkSelectPosts();
}
@action
updateBulkSelectPosts() {
if (this.shouldEnableBulkSelect && this.args.posts) {
this.bulkSelectHelper.updatePosts(this.args.posts);
}
}
get showBulkSelectHelper() {
return this.shouldEnableBulkSelect ? this.bulkSelectHelper : null;
}
get bulkActions() {
return [
{
label: "delete_selected",
icon: "trash-can",
action: this.handleBulkDelete,
class: "btn-danger"
}
];
}
<template>
<PostList
@posts={{@posts}}
@bulkSelectEnabled={{this.shouldEnableBulkSelect}}
@bulkSelectHelper={{this.showBulkSelectHelper}}
@bulkActions={{this.bulkActions}}
{{didUpdate this.updateBulkSelectPosts @posts}}
/>
</template>
}
```
**Performance Optimization**
The drafts page now uses a new bulk delete endpoint (`DELETE
/drafts/bulk_destroy`) that:
- Processes multiple drafts in a single HTTP request instead of N
individual requests
- Uses database transactions for atomic operations (all-or-nothing)
- Reduces database queries from 2N to 2 total queries
- Validates draft sequences upfront to fail fast on conflicts
**Technical Implementation**
- `PostBulkSelectHelper`: New helper class for managing selection state
with support for individual selection, range selection (shift+click),
and bulk operations with reactive posts tracking
- `PostListBulkControls`: New component providing selection count,
select all/clear all buttons, and bulk actions dropdown
- Enhanced PostList and PostListItem components with conditional bulk
selection UI
- Updated user-stream component to use optimized bulk deletion with
automatic selection cleanup
- Comprehensive styling with responsive design
**API Changes**
- New controller action: `DraftsController#bulk_destroy`
- New route: `DELETE /drafts/bulk_destroy`
- New JavaScript method: `Draft.bulkClear(drafts)`
- Enhanced `PostBulkSelectHelper` with `updatePosts()` method for
reactive data updates
- Fully backward compatible - existing single delete functionality
unchanged
**Testing**
- 9 new controller specs covering bulk deletion edge cases, validation,
and API access
- 11 integration tests for PostList bulk selection functionality
- 10 system specs for end-to-end drafts page bulk selection workflows
- All existing tests continue to pass
## 📹 Screen Recording
https://github.com/user-attachments/assets/2d5a9b38-f1cb-43ee-88ac-285b71083612
This commit adds `register_category_update_param_with_callback` to the
plugin API, allowing plugins to register category parameters and apply a
transactional callback relating to the param. The callback receives the
category instance and parameter value, and any exception raised will
roll back the category update.
Example usage:
```
register_category_param_with_transaction_callback(:doc_index_topic_id) do |category, value|
DocCategories::CategoryIndexManager.new(category).assign!(value)
end
```
In the example above, `doc_index_topic_id` will be added to
`permitted_params` in the category controller on update, and the
callback will be invoked on `after_update`, to ensure the category save
does not happen if the callback fails.
**Dev note**: I had considered creating a `CategoryExtension` and just
doing the typical `Category.prepend`, but I thought a plugin API is
probably better.
**Description**
Currently, when using Discourse as an SSO provider through the
DiscourseConnect provider, if a user logs in using the login form, the
user is not redirected to the expected `return_sso_url`. Instead, it is
redirected to the forum's home page. This fixes this behaviour and
allows the user to return to the expected URL.
Since the session is backed by a cookie, storing too much data will lead
to a cookie overflow error.
A return path can be quite large sometimes, so intead of storing it in
the session, this patch stores it in our server session.
This patch will be followed by
https://github.com/discourse/discourse/pull/34747.
`SecureSession` doesn’t make a lot of sense anymore and can be confusing
as the current cookie store used for the session is actually secure
since it’s encrypted.
Renaming it to `ServerSession` better conveys what it does: providing a
session but on the server side only.
This patch also makes some improvements, like injecting that server
session into Rack-like request objects, allowing the server session to
be available virtually everywhere.
Just updating the api docs for GET /posts.json to match the format we
have been following with other endpoints where the response json is loaded
from a separate file instead of inline in the spec file.
We want to allow configuring fields that are both optional and editable to not show up in the signup form.
- Fields that are optional but not editable need to be on the signup form, or they can never be filled in.
- Fields that are not optional need to be on the signup form, or you can never sign up.
Prevents pathological issues with crawlers querying topic lists with
very large page parameters. The new limit also allows us to tweak this
under specific instances or circumstances (high traffic, for example).
A `page` parameter above the limit will throw an `InvalidParameters`
400 error.
---------
Co-authored-by: Martin Brennan <martin@discourse.org>