mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-11 14:30:35 +00:00
Add API for on-demand generation of annual reports (#37055)
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::AnnualReportsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
|
||||
include AsyncRefreshesConcern
|
||||
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:read, :generate]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:read, :generate]
|
||||
before_action :require_user!
|
||||
before_action :set_annual_report, except: :index
|
||||
before_action :set_annual_report, only: [:show, :read]
|
||||
|
||||
def index
|
||||
with_read_replica do
|
||||
@@ -28,14 +30,59 @@ class Api::V1::AnnualReportsController < Api::BaseController
|
||||
relationships: @relationships
|
||||
end
|
||||
|
||||
def state
|
||||
render json: { state: report_state }
|
||||
end
|
||||
|
||||
def generate
|
||||
return render_empty unless year == AnnualReport.current_campaign
|
||||
return render_empty if GeneratedAnnualReport.exists?(account_id: current_account.id, year: year)
|
||||
|
||||
async_refresh = AsyncRefresh.new(refresh_key)
|
||||
|
||||
if async_refresh.running?
|
||||
add_async_refresh_header(async_refresh, retry_seconds: 2)
|
||||
return head 202
|
||||
end
|
||||
|
||||
add_async_refresh_header(AsyncRefresh.create(refresh_key), retry_seconds: 2)
|
||||
|
||||
GenerateAnnualReportWorker.perform_async(current_account.id, year)
|
||||
|
||||
head 202
|
||||
end
|
||||
|
||||
def read
|
||||
@annual_report.view!
|
||||
render_empty
|
||||
end
|
||||
|
||||
def refresh_key
|
||||
"wrapstodon:#{current_account.id}:#{year}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def report_state
|
||||
return 'available' if GeneratedAnnualReport.exists?(account_id: current_account.id, year: year)
|
||||
|
||||
async_refresh = AsyncRefresh.new(refresh_key)
|
||||
|
||||
if async_refresh.running?
|
||||
add_async_refresh_header(async_refresh, retry_seconds: 2)
|
||||
'generating'
|
||||
elsif AnnualReport.current_campaign == year && AnnualReport.new(current_account, year).eligible?
|
||||
'eligible'
|
||||
else
|
||||
'ineligible'
|
||||
end
|
||||
end
|
||||
|
||||
def year
|
||||
params[:id]&.to_i
|
||||
end
|
||||
|
||||
def set_annual_report
|
||||
@annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: params[:id])
|
||||
@annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: year)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,6 +18,13 @@ class AnnualReport
|
||||
'annual_report_'
|
||||
end
|
||||
|
||||
def self.current_campaign
|
||||
return unless Mastodon::Feature.wrapstodon_enabled?
|
||||
|
||||
datetime = Time.now.utc
|
||||
datetime.year if datetime.month == 12 && (10..31).cover?(datetime.day)
|
||||
end
|
||||
|
||||
def initialize(account, year)
|
||||
@account = account
|
||||
@year = year
|
||||
|
||||
@@ -12,7 +12,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||
|
||||
attributes :domain, :title, :version, :source_url, :description,
|
||||
:usage, :thumbnail, :icon, :languages, :configuration,
|
||||
:registrations, :api_versions
|
||||
:registrations, :api_versions, :wrapstodon
|
||||
|
||||
has_one :contact, serializer: ContactSerializer
|
||||
has_many :rules, serializer: REST::RuleSerializer
|
||||
@@ -134,6 +134,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||
Mastodon::Version.api_versions
|
||||
end
|
||||
|
||||
def wrapstodon
|
||||
AnnualReport.current_campaign
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def registrations_enabled?
|
||||
|
||||
@@ -4,7 +4,11 @@ class GenerateAnnualReportWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(account_id, year)
|
||||
async_refresh = AsyncRefresh.new("wrapstodon:#{account_id}:#{year}}")
|
||||
|
||||
AnnualReport.new(Account.find(account_id), year).generate
|
||||
|
||||
async_refresh&.finish!
|
||||
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordNotUnique
|
||||
true
|
||||
end
|
||||
|
||||
@@ -72,6 +72,8 @@ namespace :api, format: false do
|
||||
resources :annual_reports, only: [:index, :show] do
|
||||
member do
|
||||
post :read
|
||||
post :generate
|
||||
get :state
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -42,6 +42,153 @@ RSpec.describe 'API V1 Annual Reports' do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/annual_reports/:year/state' do
|
||||
context 'when not authorized' do
|
||||
it 'returns http unauthorized' do
|
||||
get '/api/v1/annual_reports/2025/state'
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(401)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with wrong scope' do
|
||||
before do
|
||||
get '/api/v1/annual_reports/2025/state', headers: headers
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for wrong scope', 'write write:accounts'
|
||||
end
|
||||
|
||||
context 'with correct scope' do
|
||||
let(:scopes) { 'read:accounts' }
|
||||
|
||||
context 'when a report is already generated' do
|
||||
before do
|
||||
Fabricate(:generated_annual_report, account: user.account, year: 2025)
|
||||
end
|
||||
|
||||
it 'returns http success and available status' do
|
||||
get '/api/v1/annual_reports/2025/state', headers: headers
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to be_present
|
||||
.and include(state: 'available')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature is not enabled' do
|
||||
it 'returns http success and ineligible status' do
|
||||
get '/api/v1/annual_reports/2025/state', headers: headers
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to be_present
|
||||
.and include(state: 'ineligible')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature is enabled and time is within window', feature: :wrapstodon do
|
||||
before do
|
||||
travel_to Time.utc(2025, 12, 20)
|
||||
|
||||
status = Fabricate(:status, visibility: :public, account: user.account)
|
||||
status.tags << Fabricate(:tag)
|
||||
end
|
||||
|
||||
it 'returns http success and eligible status' do
|
||||
get '/api/v1/annual_reports/2025/state', headers: headers
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to be_present
|
||||
.and include(state: 'eligible')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature is enabled but we are out of the time window', feature: :wrapstodon do
|
||||
before do
|
||||
travel_to Time.utc(2025, 6, 20)
|
||||
|
||||
status = Fabricate(:status, visibility: :public, account: user.account)
|
||||
status.tags << Fabricate(:tag)
|
||||
end
|
||||
|
||||
it 'returns http success and ineligible status' do
|
||||
get '/api/v1/annual_reports/2025/state', headers: headers
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to be_present
|
||||
.and include(state: 'ineligible')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/annual_reports/:id/generate' do
|
||||
context 'when not authorized' do
|
||||
it 'returns http unauthorized' do
|
||||
post '/api/v1/annual_reports/2025/generate'
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(401)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with wrong scope' do
|
||||
before do
|
||||
post '/api/v1/annual_reports/2025/generate', headers: headers
|
||||
end
|
||||
|
||||
it_behaves_like 'forbidden for wrong scope', 'read read:accounts'
|
||||
end
|
||||
|
||||
context 'with correct scope' do
|
||||
let(:scopes) { 'write:accounts' }
|
||||
|
||||
context 'when the feature is enabled and time is within window', feature: :wrapstodon do
|
||||
before do
|
||||
travel_to Time.utc(2025, 12, 20)
|
||||
|
||||
status = Fabricate(:status, visibility: :public, account: user.account)
|
||||
status.tags << Fabricate(:tag)
|
||||
end
|
||||
|
||||
it 'returns http accepted, create an async job and schedules a job' do
|
||||
expect { post '/api/v1/annual_reports/2025/generate', headers: headers }
|
||||
.to enqueue_sidekiq_job(GenerateAnnualReportWorker).with(user.account_id, 2025)
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(202)
|
||||
|
||||
expect(response.headers['Mastodon-Async-Refresh']).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/annual_reports/:id/read' do
|
||||
context 'with correct scope' do
|
||||
let(:scopes) { 'write:accounts' }
|
||||
|
||||
@@ -42,6 +42,26 @@ RSpec.describe 'Instances' do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when wrapstodon is enabled', feature: :wrapstodon do
|
||||
before do
|
||||
travel_to Time.utc(2025, 12, 20)
|
||||
end
|
||||
|
||||
it 'returns http success and the wrapstodon year' do
|
||||
get api_v2_instance_path
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
|
||||
expect(response.parsed_body)
|
||||
.to be_present
|
||||
.and include(wrapstodon: 2025)
|
||||
end
|
||||
end
|
||||
|
||||
def include_configuration_limits
|
||||
include(
|
||||
configuration: include(
|
||||
|
||||
Reference in New Issue
Block a user