Track the history of the tag `category` and `is_deprecated` fields in the `tag_versions` table. Adds generic Versionable and VersionFor concerns that encapsulate most of the history tracking logic. These concerns are designed to make it easy to add history to any model. There are a couple notable differences between tag versions and other versions: * There is no 1 hour edit merge window. All changes to the `category` and `is_deprecated` fields produce a new version in the tag history. * New versions aren't created when a tag is created. Versions are only created when a tag is edited for the first time. The tag's initial version isn't created until *after* the tag is edited for the first time. For example, if you change the category of a tag that was last updated 10 years ago, that will create an initial version of the tag backdated to 10 years ago, plus a new version for your edit. This is for a few reasons: * So that we don't have to create new tag versions every time a new tag is created. This would be wasteful because most tags never have their category or deprecation status change. * So that if you make a typo tag, your name isn't recorded in the tag's history forever. * So that we can create new tags in various places without having to know who created the tag (which may be unknown if the current user isn't set). * Because we don't know the full history of most tags, so we have to deal with incomplete histories anyway. This has a few important consequences: * Most tags won't have any tag versions. They only gain tag versions if they're edited. * You can't track /tag_versions to see newly created tags. It only shows changes to already existing tags. * Tag version IDs won't be in strict chronological order. Higher IDs may have created_at timestamps before lower IDs. For example, if you change the category of a tag that is 10 years old, that will create an initial version with a high ID, but with a created_at timestamp dated to 10 years ago. Fixes #4402: Track tag category changes
Controllers
Controllers are the entry points to Danbooru. Every URL on the site corresponds to a controller action. When a request for an URL is made, the corresponding controller action is called to handle the request.
Controllers follow a convention where, for example, the URL https://danbooru.donmai.us/posts/1234 is handled by the
#show method inside the PostsController living at app/controllers/posts_controller.rb.
https://danbooru.donmai.us/posts?tags=touhou is handled by the #index method in the PostsController. The HTML
template for the response lives at app/views/posts/index.html.erb. See below for more
examples.
Controllers are responsible for taking the URL parameters, checking whether the user is authorized to perform the action, actually performing the action, then returning the response. Most controllers simply fetch or update a model, then render an HTML template from app/views in response.
Example
A standard controller looks something like this:
class BansController < ApplicationController
def new
@ban = authorize Ban.new(permitted_attributes(Ban))
respond_with(@ban)
end
def edit
@ban = authorize Ban.find(params[:id])
respond_with(@ban)
end
def index
@bans = authorize Ban.paginated_search(params, count_pages: true)
respond_with(@bans)
end
def show
@ban = authorize Ban.find(params[:id])
respond_with(@ban)
end
def create
@ban = authorize Ban.new(banner: CurrentUser.user, **permitted_attributes(Ban))
@ban.save
respond_with(@ban, location: bans_path)
end
def update
@ban = authorize Ban.find(params[:id])
@ban.update(permitted_attributes(@ban))
respond_with(@ban)
end
def destroy
@ban = authorize Ban.find(params[:id])
@ban.destroy
respond_with(@ban)
end
end
Routes
Each controller action above corresponds to an URL:
| Controller Action | URL | Route | Route Helper | View |
|---|---|---|---|---|
| BansController#new | https://danbooru.donmai.us/bans/new | GET /bans/new | new_ban_path | app/views/bans/new.html.erb |
| BansController#edit | https://danbooru.donmai.us/bans/1234/edit | GET /bans/:id/edit | edit_ban_path(@ban) | app/views/bans/edit.html.erb |
| BansController#index | https://danbooru.donmai.us/bans | GET /bans | bans_path | app/views/bans/index.html.erb |
| BansController#show | https://danbooru.donmai.us/bans/1234 | GET /bans/:id | ban_path(@ban) | app/views/bans/show.html.erb |
| BansController#create | POST https://danbooru.donmai.us/bans | POST /bans | ||
| BansController#update | PUT https://danbooru.donmai.us/bans/1234 | PUT /bans/:id | ||
| BansController#destroy | DELETE https://danbooru.donmai.us/bans/1234 | DELETE /bans/:id |
These routes are defined in config/routes.rb.
Authorization
Most permission checks for whether a user has permission to do something happen inside controllers, using authorize
calls.
The authorize method comes from the Pundit framework. This method checks whether
the current user is authorized to perform the current action. If not, it raises a Pundit::NotAuthorizedError, which is
caught in the ApplicationController.
The actual authorization logic for these calls lives in app/policies. They follow a convention where the
authorization logic for the BansController#create action lives in BanPolicy#create?, which lives in
app/policies/ban_policy.rb. The call to authorize in the controller simply finds and
calls the ban policy.
The #create, #new, and #update actions also use permitted_attributes to check that the user is allowed to update
the model attributes they're trying to update. This also comes from the Pundit framework. See the
permitted_attributes_for_create and permitted_attributes_for_update methods in app/policies.
Responses
Controllers use respond_with(@post) to generate a response. This comes from the Responders
gem. respond_with does the following:
- Detects whether the user wants an HTML, JSON, or XML response.
- Renders an HTML template from app/views for HTML requests.
- Renders JSON or XML for API requests.
- Handles universal URL parameters, like
onlyorincludes. - Handles universal behavior, like returning 200 OK for successful responses, or returning an error if trying to save a model with validation errors.
HTML Responses
The HTML templates for controller actions live in app/views. For example, the template for PostsController#show, which corresponds to https://danbooru.donmai.us/posts/1234, lives in app/views/posts/show.html.erb.
Instance variables set by controllers are automatically inherited by views. For example, if a controller sets @post,
then the @post variable will be available in the view.
API Responses
All URLs support JSON or XML responses. This is handled by respond_with.
The response format can be chosen in several ways. First, by adding a .json or .xml file extension:
Second, by setting the format URL parameter:
Third, by setting the Accept HTTP header:
curl -H "Accept: application/json" https://danbooru.donmai.us/posts
curl -H "Accept: application/xml" https://danbooru.donmai.us/posts
When generating API responses, respond_with uses the api_attributes method inside app/policies to
determine which attributes are visible to the current user.
Application Controller
Global behavior that runs on every request lives inside the ApplicationController. This includes the following:
- Setting the current user based on their session cookies or API keys.
- Checking rate limits.
- Checking for IP bans.
- Adding certain HTTP headers.
- Handling exceptions.