mirror of
https://github.com/discourse/discourse.git
synced 2025-06-20 12:41:37 +08:00
FEATURE: Add automation statistics tracking to Automation (#31921)
introduces comprehensive statistics tracking for the Discourse Automation plugin, allowing users to monitor the performance and execution patterns of their automations: - Add `discourse_automation_stats` table to track execution metrics including run counts, execution times, and performance data - Create a new `Stat` model to handle tracking and retrieving automation statistics - Update the admin UI to display automation stats (runs today/this week/month and last run time) - Modernize the automation list interface using Glimmer components - Replace the older enable/disable icon with a toggle switch for better UX - Add schema annotations to existing models for better code documentation - Include extensive test coverage for the new statistics functionality This helps administrators understand how their automations are performing and identify potential bottlenecks or optimization opportunities. --------- Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com> Co-authored-by: Ted Johansson <ted@discourse.org>
This commit is contained in:
@ -0,0 +1,233 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { fn } from "@ember/helper";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { LinkTo } from "@ember/routing";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import DPageSubheader from "discourse/components/d-page-subheader";
|
||||||
|
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
||||||
|
import avatar from "discourse/helpers/avatar";
|
||||||
|
import formatDate from "discourse/helpers/format-date";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
|
import AdminConfigAreaEmptyList from "admin/components/admin-config-area-empty-list";
|
||||||
|
|
||||||
|
// number of runs required to show the runs count for the period
|
||||||
|
const RUN_THRESHOLD = 10;
|
||||||
|
|
||||||
|
export default class AutomationList extends Component {
|
||||||
|
@service dialog;
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
@action
|
||||||
|
destroyAutomation(automation) {
|
||||||
|
this.dialog.deleteConfirm({
|
||||||
|
message: i18n("discourse_automation.destroy_automation.confirm", {
|
||||||
|
name: escapeExpression(automation.name),
|
||||||
|
}),
|
||||||
|
didConfirm: () => {
|
||||||
|
return automation
|
||||||
|
.destroyRecord()
|
||||||
|
.then(() => this.send("triggerRefresh"))
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async toggleEnabled(automation) {
|
||||||
|
automation.set("enabled", !automation.enabled);
|
||||||
|
try {
|
||||||
|
await automation.save({ enabled: automation.enabled });
|
||||||
|
} catch (e) {
|
||||||
|
popupAjaxError(e);
|
||||||
|
automation.set("enabled", !automation.enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statsText(stats) {
|
||||||
|
if (!stats || !stats.last_month || stats.last_month.total_runs === 0) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.last_day?.total_runs > RUN_THRESHOLD) {
|
||||||
|
return i18n("discourse_automation.models.automation.runs_today", {
|
||||||
|
count: stats.last_day.total_runs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.last_week?.total_runs > RUN_THRESHOLD) {
|
||||||
|
return i18n("discourse_automation.models.automation.runs_this_week", {
|
||||||
|
count: stats.last_day.total_runs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n("discourse_automation.models.automation.runs_this_month", {
|
||||||
|
count: stats.last_day.total_runs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="discourse-automations-table">
|
||||||
|
<DPageSubheader @titleLabel={{i18n "discourse_automation.table_title"}}>
|
||||||
|
<:actions as |actions|>
|
||||||
|
<actions.Primary
|
||||||
|
@label="discourse_automation.create"
|
||||||
|
@route="adminPlugins.show.automation.new"
|
||||||
|
@icon="plus"
|
||||||
|
class="discourse-automation__create-btn"
|
||||||
|
/>
|
||||||
|
</:actions>
|
||||||
|
</DPageSubheader>
|
||||||
|
|
||||||
|
{{#if @model.length}}
|
||||||
|
<table class="d-admin-table automations">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{i18n
|
||||||
|
"discourse_automation.models.automation.name.label"
|
||||||
|
}}</th>
|
||||||
|
<th>{{i18n
|
||||||
|
"discourse_automation.models.automation.last_updated_by.label"
|
||||||
|
}}</th>
|
||||||
|
<th>{{i18n
|
||||||
|
"discourse_automation.models.automation.runs.label"
|
||||||
|
}}</th>
|
||||||
|
<th>{{i18n
|
||||||
|
"discourse_automation.models.automation.last_run.label"
|
||||||
|
}}</th>
|
||||||
|
<th>{{i18n
|
||||||
|
"discourse_automation.models.automation.enabled.label"
|
||||||
|
}}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each @model as |automation|}}
|
||||||
|
<tr class="d-admin-row__content">
|
||||||
|
{{#if automation.script.not_found}}
|
||||||
|
<td
|
||||||
|
colspan="5"
|
||||||
|
class="d-admin-row__detail alert alert-danger"
|
||||||
|
>
|
||||||
|
<div class="d-admin-row__mobile-label">
|
||||||
|
{{i18n
|
||||||
|
"discourse_automation.models.automation.status.label"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
{{i18n
|
||||||
|
"discourse_automation.scriptables.not_found"
|
||||||
|
script=automation.script.id
|
||||||
|
automation=automation.name
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
{{else if automation.trigger.not_found}}
|
||||||
|
<td
|
||||||
|
colspan="5"
|
||||||
|
class="d-admin-row__detail alert alert-danger"
|
||||||
|
>
|
||||||
|
<div class="d-admin-row__mobile-label">
|
||||||
|
{{i18n
|
||||||
|
"discourse_automation.models.automation.status.label"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
{{i18n
|
||||||
|
"discourse_automation.triggerables.not_found"
|
||||||
|
trigger=automation.trigger.id
|
||||||
|
automation=automation.name
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
{{else}}
|
||||||
|
<td class="d-admin-row__overview automations__name">
|
||||||
|
{{if
|
||||||
|
automation.name
|
||||||
|
automation.name
|
||||||
|
(i18n "discourse_automation.unnamed_automation")
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td class="d-admin-row__detail automations__updated-by">
|
||||||
|
<div class="d-admin-row__mobile-label">
|
||||||
|
{{i18n
|
||||||
|
"discourse_automation.models.automation.last_updated_by.label"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="automations__user-timestamp">
|
||||||
|
<a
|
||||||
|
href={{automation.last_updated_by.userPath}}
|
||||||
|
data-user-card={{automation.last_updated_by.username}}
|
||||||
|
>
|
||||||
|
{{avatar automation.last_updated_by imageSize="small"}}
|
||||||
|
</a>
|
||||||
|
{{formatDate automation.updated_at leaveAgo="true"}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="d-admin-row__detail automations__runs">
|
||||||
|
<div class="d-admin-row__mobile-label">
|
||||||
|
{{i18n
|
||||||
|
"discourse_automation.models.automation.runs.label"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<span class="automations__stats">
|
||||||
|
{{this.statsText automation.stats}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="d-admin-row__detail automations__last-run">
|
||||||
|
<div class="d-admin-row__mobile-label">
|
||||||
|
{{i18n
|
||||||
|
"discourse_automation.models.automation.last_run.label"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
{{#if automation.stats.last_run_at}}
|
||||||
|
{{formatDate
|
||||||
|
automation.stats.last_run_at
|
||||||
|
leaveAgo="true"
|
||||||
|
}}
|
||||||
|
{{else}}
|
||||||
|
-
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
<td class="d-admin-row__detail automations__enabled">
|
||||||
|
<div class="d-admin-row__mobile-label">
|
||||||
|
{{i18n
|
||||||
|
"discourse_automation.models.automation.enabled.label"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<DToggleSwitch
|
||||||
|
@state={{automation.enabled}}
|
||||||
|
{{on "click" (fn this.toggleEnabled automation)}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<td class="d-admin-row__controls automations__controls">
|
||||||
|
<LinkTo
|
||||||
|
@route="adminPlugins.show.automation.edit"
|
||||||
|
@model={{automation.id}}
|
||||||
|
class="btn btn-default btn-text btn-small"
|
||||||
|
>
|
||||||
|
{{i18n "discourse_automation.edit"}}
|
||||||
|
</LinkTo>
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
@icon="trash-can"
|
||||||
|
@action={{this.destroyAutomation automation}}
|
||||||
|
class="btn-small btn-danger"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<AdminConfigAreaEmptyList
|
||||||
|
@ctaLabel="discourse_automation.create"
|
||||||
|
@ctaRoute="adminPlugins.show.automation.new"
|
||||||
|
@ctaClass="discourse-automation__create-btn"
|
||||||
|
@emptyLabel="discourse_automation.no_automation_yet"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
import { htmlSafe } from "@ember/template";
|
|
||||||
import { iconHTML } from "discourse/lib/icon-library";
|
|
||||||
|
|
||||||
export default function formatEnabledAutomation(enabled, trigger) {
|
|
||||||
if (enabled && trigger.id) {
|
|
||||||
return htmlSafe(
|
|
||||||
iconHTML("check", {
|
|
||||||
class: "enabled-automation",
|
|
||||||
title: "discourse_automation.models.automation.enabled.label",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return htmlSafe(
|
|
||||||
iconHTML("xmark", {
|
|
||||||
class: "disabled-automation",
|
|
||||||
title: "discourse_automation.models.automation.disabled.label",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,131 +1 @@
|
|||||||
<section class="discourse-automations-table">
|
<AutomationList @model={{this.model}} />
|
||||||
<DPageSubheader @titleLabel={{i18n "discourse_automation.table_title"}}>
|
|
||||||
<:actions as |actions|>
|
|
||||||
<actions.Primary
|
|
||||||
@label="discourse_automation.create"
|
|
||||||
@route="adminPlugins.show.automation.new"
|
|
||||||
@icon="plus"
|
|
||||||
class="discourse-automation__create-btn"
|
|
||||||
/>
|
|
||||||
</:actions>
|
|
||||||
</DPageSubheader>
|
|
||||||
|
|
||||||
{{#if this.model.length}}
|
|
||||||
<table class="d-admin-table automations">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>{{i18n "discourse_automation.models.automation.name.label"}}</th>
|
|
||||||
<th>{{i18n
|
|
||||||
"discourse_automation.models.automation.trigger.label"
|
|
||||||
}}</th>
|
|
||||||
<th>{{i18n
|
|
||||||
"discourse_automation.models.automation.script.label"
|
|
||||||
}}</th>
|
|
||||||
<th>{{i18n
|
|
||||||
"discourse_automation.models.automation.last_updated_by.label"
|
|
||||||
}}</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each this.model as |automation|}}
|
|
||||||
<tr class="d-admin-row__content">
|
|
||||||
{{#if automation.script.not_found}}
|
|
||||||
<td colspan="5" class="d-admin-row__detail alert alert-danger">
|
|
||||||
<div class="d-admin-row__mobile-label">
|
|
||||||
{{i18n "discourse_automation.models.automation.status.label"}}
|
|
||||||
</div>
|
|
||||||
{{i18n
|
|
||||||
"discourse_automation.scriptables.not_found"
|
|
||||||
script=automation.script.id
|
|
||||||
automation=automation.name
|
|
||||||
}}
|
|
||||||
</td>
|
|
||||||
{{else if automation.trigger.not_found}}
|
|
||||||
<td colspan="5" class="d-admin-row__detail alert alert-danger">
|
|
||||||
<div class="d-admin-row__mobile-label">
|
|
||||||
{{i18n "discourse_automation.models.automation.status.label"}}
|
|
||||||
</div>
|
|
||||||
{{i18n
|
|
||||||
"discourse_automation.triggerables.not_found"
|
|
||||||
trigger=automation.trigger.id
|
|
||||||
automation=automation.name
|
|
||||||
}}
|
|
||||||
</td>
|
|
||||||
{{else}}
|
|
||||||
<td class="d-admin-row__detail automations__status">
|
|
||||||
<div class="d-admin-row__mobile-label">
|
|
||||||
{{i18n "discourse_automation.models.automation.status.label"}}
|
|
||||||
</div>
|
|
||||||
{{format-enabled-automation
|
|
||||||
automation.enabled
|
|
||||||
automation.trigger
|
|
||||||
}}
|
|
||||||
</td>
|
|
||||||
<td class="d-admin-row__overview automations__name">
|
|
||||||
{{if
|
|
||||||
automation.name
|
|
||||||
automation.name
|
|
||||||
(i18n "discourse_automation.unnamed_automation")
|
|
||||||
}}
|
|
||||||
</td>
|
|
||||||
<td class="d-admin-row__detail automations__script">
|
|
||||||
<div class="d-admin-row__mobile-label">
|
|
||||||
{{i18n
|
|
||||||
"discourse_automation.models.automation.trigger.label"
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
{{if automation.trigger.id automation.trigger.name "-"}}
|
|
||||||
</td>
|
|
||||||
<td class="d-admin-row__detail automations__version">
|
|
||||||
<div class="d-admin-row__mobile-label">
|
|
||||||
{{i18n "discourse_automation.models.automation.script.label"}}
|
|
||||||
</div>
|
|
||||||
{{automation.script.name}}
|
|
||||||
(v{{automation.script.version}})
|
|
||||||
</td>
|
|
||||||
<td class="d-admin-row__detail automations__updated-by">
|
|
||||||
<div class="d-admin-row__mobile-label">
|
|
||||||
{{i18n
|
|
||||||
"discourse_automation.models.automation.last_updated_by.label"
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="automations__user-timestamp">
|
|
||||||
<a
|
|
||||||
href={{automation.last_updated_by.userPath}}
|
|
||||||
data-user-card={{automation.last_updated_by.username}}
|
|
||||||
>
|
|
||||||
{{avatar automation.last_updated_by imageSize="small"}}
|
|
||||||
</a>
|
|
||||||
{{format-date automation.updated_at leaveAgo="true"}}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<td class="d-admin-row__controls automations__controls">
|
|
||||||
<DButton
|
|
||||||
@label="discourse_automation.edit"
|
|
||||||
class="btn-small"
|
|
||||||
@action={{action "editAutomation" automation}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DButton
|
|
||||||
@icon="trash-can"
|
|
||||||
@action={{action "destroyAutomation" automation}}
|
|
||||||
class="btn-small btn-danger"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{{else}}
|
|
||||||
<AdminConfigAreaEmptyList
|
|
||||||
@ctaLabel="discourse_automation.create"
|
|
||||||
@ctaRoute="adminPlugins.show.automation.new"
|
|
||||||
@ctaClass="discourse-automation__create-btn"
|
|
||||||
@emptyLabel="discourse_automation.no_automation_yet"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</section>
|
|
@ -11,6 +11,9 @@ module DiscourseAutomation
|
|||||||
automations,
|
automations,
|
||||||
each_serializer: DiscourseAutomation::AutomationSerializer,
|
each_serializer: DiscourseAutomation::AutomationSerializer,
|
||||||
root: "automations",
|
root: "automations",
|
||||||
|
scope: {
|
||||||
|
stats: DiscourseAutomation::Stat.fetch_period_summaries,
|
||||||
|
},
|
||||||
).as_json
|
).as_json
|
||||||
render_json_dump(serializer)
|
render_json_dump(serializer)
|
||||||
end
|
end
|
||||||
@ -50,31 +53,35 @@ module DiscourseAutomation
|
|||||||
last_updated_by_id: current_user.id,
|
last_updated_by_id: current_user.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if automation.trigger != params[:automation][:trigger]
|
if attributes.key?(:trigger) && automation.trigger != params[:automation][:trigger]
|
||||||
params[:automation][:fields] = []
|
params[:automation][:fields] = []
|
||||||
attributes[:enabled] = false
|
attributes[:enabled] = false
|
||||||
automation.fields.destroy_all
|
automation.fields.destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
if automation.script != params[:automation][:script]
|
if attributes.key?(:script)
|
||||||
attributes[:trigger] = nil
|
if automation.script != params[:automation][:script]
|
||||||
params[:automation][:fields] = []
|
attributes[:trigger] = nil
|
||||||
attributes[:enabled] = false
|
params[:automation][:fields] = []
|
||||||
automation.fields.destroy_all
|
attributes[:enabled] = false
|
||||||
automation.tap { |r| r.assign_attributes(attributes) }.save!(validate: false)
|
automation.fields.destroy_all
|
||||||
else
|
automation.tap { |r| r.assign_attributes(attributes) }.save!(validate: false)
|
||||||
Array(params[:automation][:fields])
|
else
|
||||||
.reject(&:empty?)
|
Array(params[:automation][:fields])
|
||||||
.each do |field|
|
.reject(&:empty?)
|
||||||
automation.upsert_field!(
|
.each do |field|
|
||||||
field[:name],
|
automation.upsert_field!(
|
||||||
field[:component],
|
field[:name],
|
||||||
field[:metadata],
|
field[:component],
|
||||||
target: field[:target],
|
field[:metadata],
|
||||||
)
|
target: field[:target],
|
||||||
end
|
)
|
||||||
|
end
|
||||||
|
|
||||||
automation.tap { |r| r.assign_attributes(attributes) }.save!
|
automation.update!(attributes)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
automation.update!(attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
render_serialized_automation(automation)
|
render_serialized_automation(automation)
|
||||||
|
@ -17,6 +17,8 @@ module DiscourseAutomation
|
|||||||
dependent: :delete_all,
|
dependent: :delete_all,
|
||||||
foreign_key: "automation_id"
|
foreign_key: "automation_id"
|
||||||
|
|
||||||
|
has_many :stats, class_name: "DiscourseAutomation::Stat", dependent: :delete_all
|
||||||
|
|
||||||
validates :script, presence: true
|
validates :script, presence: true
|
||||||
validate :validate_trigger_fields
|
validate :validate_trigger_fields
|
||||||
|
|
||||||
@ -137,8 +139,10 @@ module DiscourseAutomation
|
|||||||
if scriptable.background && !running_in_background
|
if scriptable.background && !running_in_background
|
||||||
trigger_in_background!(context)
|
trigger_in_background!(context)
|
||||||
else
|
else
|
||||||
triggerable&.on_call&.call(self, serialized_fields)
|
Stat.log(id) do
|
||||||
scriptable.script.call(context, serialized_fields, self)
|
triggerable&.on_call&.call(self, serialized_fields)
|
||||||
|
scriptable.script.call(context, serialized_fields, self)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
DiscourseAutomation.set_active_automation(nil)
|
DiscourseAutomation.set_active_automation(nil)
|
||||||
@ -187,3 +191,17 @@ module DiscourseAutomation
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: discourse_automation_automations
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# name :string
|
||||||
|
# script :string not null
|
||||||
|
# enabled :boolean default(FALSE), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# last_updated_by_id :integer not null
|
||||||
|
# trigger :string
|
||||||
|
#
|
||||||
|
@ -250,3 +250,17 @@ module DiscourseAutomation
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: discourse_automation_fields
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# automation_id :bigint not null
|
||||||
|
# metadata :jsonb not null
|
||||||
|
# component :string not null
|
||||||
|
# name :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# target :string
|
||||||
|
#
|
||||||
|
@ -7,3 +7,14 @@ module DiscourseAutomation
|
|||||||
belongs_to :automation, class_name: "DiscourseAutomation::Automation"
|
belongs_to :automation, class_name: "DiscourseAutomation::Automation"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: discourse_automation_pending_automations
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# automation_id :bigint not null
|
||||||
|
# execute_at :datetime not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
@ -7,3 +7,19 @@ module DiscourseAutomation
|
|||||||
belongs_to :automation, class_name: "DiscourseAutomation::Automation"
|
belongs_to :automation, class_name: "DiscourseAutomation::Automation"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: discourse_automation_pending_pms
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# target_usernames :string is an Array
|
||||||
|
# sender :string
|
||||||
|
# title :string
|
||||||
|
# raw :string
|
||||||
|
# automation_id :bigint not null
|
||||||
|
# execute_at :datetime not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# prefers_encrypt :boolean default(FALSE), not null
|
||||||
|
#
|
||||||
|
128
plugins/automation/app/models/discourse_automation/stat.rb
Normal file
128
plugins/automation/app/models/discourse_automation/stat.rb
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
#
|
||||||
|
module DiscourseAutomation
|
||||||
|
class Stat < ActiveRecord::Base
|
||||||
|
self.table_name = "discourse_automation_stats"
|
||||||
|
|
||||||
|
def self.log(automation_id, run_time = nil)
|
||||||
|
if block_given? && run_time.nil?
|
||||||
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||||
|
result = yield
|
||||||
|
run_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
||||||
|
result
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
update_stats(automation_id, run_time)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.fetch_period_summaries
|
||||||
|
today = Date.current
|
||||||
|
|
||||||
|
# Define our time periods
|
||||||
|
periods = {
|
||||||
|
last_day: {
|
||||||
|
start_date: today - 1.day,
|
||||||
|
end_date: today,
|
||||||
|
},
|
||||||
|
last_week: {
|
||||||
|
start_date: today - 1.week,
|
||||||
|
end_date: today,
|
||||||
|
},
|
||||||
|
last_month: {
|
||||||
|
start_date: today - 1.month,
|
||||||
|
end_date: today,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
periods.each do |period_name, date_range|
|
||||||
|
builder = DB.build <<~SQL
|
||||||
|
SELECT
|
||||||
|
automation_id,
|
||||||
|
SUM(total_runs) AS total_runs,
|
||||||
|
SUM(total_time) AS total_time,
|
||||||
|
CASE WHEN SUM(total_runs) > 0
|
||||||
|
THEN SUM(total_time) / SUM(total_runs)
|
||||||
|
ELSE 0
|
||||||
|
END AS average_run_time,
|
||||||
|
MIN(min_run_time) AS min_run_time,
|
||||||
|
MAX(max_run_time) AS max_run_time
|
||||||
|
FROM discourse_automation_stats
|
||||||
|
WHERE date >= :start_date AND date <= :end_date
|
||||||
|
GROUP BY automation_id
|
||||||
|
SQL
|
||||||
|
|
||||||
|
stats = builder.query(start_date: date_range[:start_date], end_date: date_range[:end_date])
|
||||||
|
|
||||||
|
last_run_stats = DB.query_array <<~SQL
|
||||||
|
SELECT
|
||||||
|
automation_id,
|
||||||
|
MAX(last_run_at) AS last_run_at
|
||||||
|
FROM discourse_automation_stats
|
||||||
|
GROUP BY automation_id
|
||||||
|
SQL
|
||||||
|
last_run_stats = Hash[*last_run_stats.flatten]
|
||||||
|
|
||||||
|
stats.each do |stat|
|
||||||
|
automation_id = stat.automation_id
|
||||||
|
result[automation_id] ||= {}
|
||||||
|
result[automation_id][:last_run_at] = last_run_stats[automation_id]
|
||||||
|
result[automation_id][period_name] = {
|
||||||
|
total_runs: stat.total_runs,
|
||||||
|
total_time: stat.total_time,
|
||||||
|
average_run_time: stat.average_run_time,
|
||||||
|
min_run_time: stat.min_run_time,
|
||||||
|
max_run_time: stat.max_run_time,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.update_stats(automation_id, run_time)
|
||||||
|
today = Date.current
|
||||||
|
current_time = Time.now
|
||||||
|
|
||||||
|
builder = DB.build <<~SQL
|
||||||
|
INSERT INTO discourse_automation_stats
|
||||||
|
(automation_id, date, last_run_at, total_time, average_run_time, min_run_time, max_run_time, total_runs)
|
||||||
|
VALUES (:automation_id, :date, :current_time, :run_time, :run_time, :run_time, :run_time, 1)
|
||||||
|
ON CONFLICT (automation_id, date) DO UPDATE SET
|
||||||
|
last_run_at = :current_time,
|
||||||
|
total_time = discourse_automation_stats.total_time + :run_time,
|
||||||
|
total_runs = discourse_automation_stats.total_runs + 1,
|
||||||
|
average_run_time = (discourse_automation_stats.total_time + :run_time) / (discourse_automation_stats.total_runs + 1),
|
||||||
|
min_run_time = LEAST(discourse_automation_stats.min_run_time, :run_time),
|
||||||
|
max_run_time = GREATEST(discourse_automation_stats.max_run_time, :run_time)
|
||||||
|
SQL
|
||||||
|
|
||||||
|
builder.exec(
|
||||||
|
automation_id: automation_id,
|
||||||
|
date: today,
|
||||||
|
current_time: current_time,
|
||||||
|
run_time: run_time,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: discourse_automation_stats
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# automation_id :bigint not null
|
||||||
|
# date :date not null
|
||||||
|
# last_run_at :datetime not null
|
||||||
|
# total_time :float not null
|
||||||
|
# average_run_time :float not null
|
||||||
|
# min_run_time :float not null
|
||||||
|
# max_run_time :float not null
|
||||||
|
# total_runs :integer not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_discourse_automation_stats_on_automation_id_and_date (automation_id,date) UNIQUE
|
||||||
|
#
|
@ -7,3 +7,20 @@ module DiscourseAutomation
|
|||||||
belongs_to :user
|
belongs_to :user
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: discourse_automation_user_global_notices
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# user_id :integer not null
|
||||||
|
# notice :text not null
|
||||||
|
# identifier :string not null
|
||||||
|
# level :string default("info")
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# idx_discourse_automation_user_global_notices (user_id,identifier) UNIQUE
|
||||||
|
#
|
||||||
|
@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
module DiscourseAutomation
|
module DiscourseAutomation
|
||||||
class AutomationSerializer < ApplicationSerializer
|
class AutomationSerializer < ApplicationSerializer
|
||||||
attributes :id
|
attribute :id
|
||||||
attributes :name
|
attribute :name
|
||||||
attributes :enabled
|
attribute :enabled
|
||||||
attributes :script
|
attribute :script
|
||||||
attributes :trigger
|
attribute :trigger
|
||||||
attributes :updated_at
|
attribute :updated_at
|
||||||
attributes :last_updated_by
|
attribute :last_updated_by
|
||||||
attributes :next_pending_automation_at
|
attribute :next_pending_automation_at
|
||||||
attributes :placeholders
|
attribute :placeholders
|
||||||
|
attribute :stats
|
||||||
|
|
||||||
def last_updated_by
|
def last_updated_by
|
||||||
BasicUserSerializer.new(
|
BasicUserSerializer.new(
|
||||||
@ -84,6 +85,29 @@ module DiscourseAutomation
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_stats?
|
||||||
|
scope&.dig(:stats).present?
|
||||||
|
end
|
||||||
|
|
||||||
|
EMPTY_STATS = {
|
||||||
|
total_runs: 0,
|
||||||
|
total_time: 0,
|
||||||
|
average_run_time: 0,
|
||||||
|
min_run_time: 0,
|
||||||
|
max_run_time: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
def stats
|
||||||
|
automation_stats = scope&.dig(:stats, object.id) || {}
|
||||||
|
|
||||||
|
{
|
||||||
|
last_day: automation_stats[:last_day] || EMPTY_STATS,
|
||||||
|
last_week: automation_stats[:last_week] || EMPTY_STATS,
|
||||||
|
last_month: automation_stats[:last_month] || EMPTY_STATS,
|
||||||
|
last_run_at: automation_stats[:last_run_at],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_fields_with_priority(arr, trigger)
|
def filter_fields_with_priority(arr, trigger)
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
.admin-plugins.automation {
|
.admin-plugins.automation {
|
||||||
.automations {
|
.automations {
|
||||||
.relative-date {
|
.relative-date {
|
||||||
font-size: $font-down-1;
|
font-size: var(--font-down-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats {
|
||||||
|
font-size: var(--font-down-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
td[role="button"] {
|
td[role="button"] {
|
||||||
@ -26,31 +30,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-admin-row__overview {
|
.d-admin-row__name {
|
||||||
@include breakpoint("tablet") {
|
@include breakpoint("tablet") {
|
||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-admin-row__detail.automations__status {
|
.d-admin-row__detail.automations__runs {
|
||||||
@include breakpoint("tablet") {
|
@include breakpoint("tablet") {
|
||||||
order: 2; // move below the name to avoid empty spacing
|
order: 2; // move below the name to avoid empty spacing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-admin-row__detail.automations__script {
|
.d-admin-row__detail.automations__updated-by {
|
||||||
@include breakpoint("tablet") {
|
@include breakpoint("tablet") {
|
||||||
order: 3;
|
order: 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-admin-row__detail.automations__version {
|
.d-admin-row__detail.automations__enabled {
|
||||||
@include breakpoint("tablet") {
|
|
||||||
order: 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-admin-row__detail.automations__updated-by {
|
|
||||||
@include breakpoint("tablet") {
|
@include breakpoint("tablet") {
|
||||||
order: 5;
|
order: 5;
|
||||||
}
|
}
|
||||||
|
@ -426,6 +426,11 @@ en:
|
|||||||
name:
|
name:
|
||||||
label: Trigger
|
label: Trigger
|
||||||
automation:
|
automation:
|
||||||
|
runs_today: "%{count} today"
|
||||||
|
runs_this_week: "%{count} this week"
|
||||||
|
runs_this_month: "%{count} this month"
|
||||||
|
runs:
|
||||||
|
label: Runs
|
||||||
name:
|
name:
|
||||||
label: Name
|
label: Name
|
||||||
trigger:
|
trigger:
|
||||||
@ -444,5 +449,7 @@ en:
|
|||||||
label: Placeholders
|
label: Placeholders
|
||||||
last_updated_at:
|
last_updated_at:
|
||||||
label: Last update
|
label: Last update
|
||||||
|
last_run:
|
||||||
|
label: Last run
|
||||||
last_updated_by:
|
last_updated_by:
|
||||||
label: Updated by
|
label: Updated by
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
class AddAutomationStats < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
create_table :discourse_automation_stats do |t|
|
||||||
|
t.bigint :automation_id, null: false
|
||||||
|
t.date :date, null: false
|
||||||
|
t.datetime :last_run_at, null: false
|
||||||
|
t.float :total_time, null: false
|
||||||
|
t.float :average_run_time, null: false
|
||||||
|
t.float :min_run_time, null: false
|
||||||
|
t.float :max_run_time, null: false
|
||||||
|
t.integer :total_runs, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :discourse_automation_stats, %i[automation_id date], unique: true
|
||||||
|
end
|
||||||
|
end
|
254
plugins/automation/spec/models/stat_spec.rb
Normal file
254
plugins/automation/spec/models/stat_spec.rb
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe DiscourseAutomation::Stat do
|
||||||
|
let(:automation_id) { 42 }
|
||||||
|
let(:another_automation_id) { 43 }
|
||||||
|
let(:run_time) { 0.5 }
|
||||||
|
|
||||||
|
describe ".fetch_period_summaries" do
|
||||||
|
let(:today) { Date.new(2023, 1, 10) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
freeze_time today
|
||||||
|
|
||||||
|
# Last day: 2 runs
|
||||||
|
DiscourseAutomation::Stat.create!(
|
||||||
|
automation_id: automation_id,
|
||||||
|
date: today - 1.day,
|
||||||
|
last_run_at: today - 1.day + 10.hours,
|
||||||
|
total_time: 3.0,
|
||||||
|
average_run_time: 1.5,
|
||||||
|
min_run_time: 1.0,
|
||||||
|
max_run_time: 2.0,
|
||||||
|
total_runs: 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Same day, different automation
|
||||||
|
DiscourseAutomation::Stat.create!(
|
||||||
|
automation_id: another_automation_id,
|
||||||
|
date: today - 1.day,
|
||||||
|
last_run_at: today - 1.day + 12.hours,
|
||||||
|
total_time: 1.0,
|
||||||
|
average_run_time: 1.0,
|
||||||
|
min_run_time: 1.0,
|
||||||
|
max_run_time: 1.0,
|
||||||
|
total_runs: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Last week: 3 runs (including the day above)
|
||||||
|
DiscourseAutomation::Stat.create!(
|
||||||
|
automation_id: automation_id,
|
||||||
|
date: today - 5.days,
|
||||||
|
last_run_at: today - 5.days + 14.hours,
|
||||||
|
total_time: 2.5,
|
||||||
|
average_run_time: 1.25,
|
||||||
|
min_run_time: 0.5,
|
||||||
|
max_run_time: 2.0,
|
||||||
|
total_runs: 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Last month: 5 runs (including the week above)
|
||||||
|
DiscourseAutomation::Stat.create!(
|
||||||
|
automation_id: automation_id,
|
||||||
|
date: today - 20.days,
|
||||||
|
last_run_at: today - 20.days + 8.hours,
|
||||||
|
total_time: 4.0,
|
||||||
|
average_run_time: 2.0,
|
||||||
|
min_run_time: 1.5,
|
||||||
|
max_run_time: 2.5,
|
||||||
|
total_runs: 2,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns correctly structured data for multiple periods" do
|
||||||
|
summaries = DiscourseAutomation::Stat.fetch_period_summaries
|
||||||
|
expect(summaries.keys).to contain_exactly(automation_id, another_automation_id)
|
||||||
|
|
||||||
|
auto_summary = summaries[automation_id]
|
||||||
|
expect(auto_summary.keys).to contain_exactly(:last_run_at, :last_day, :last_week, :last_month)
|
||||||
|
expect(auto_summary[:last_run_at].to_date).to eq((today - 1.day).to_date)
|
||||||
|
|
||||||
|
%i[last_day last_week last_month].each do |period|
|
||||||
|
expect(auto_summary[period].keys).to contain_exactly(
|
||||||
|
:total_runs,
|
||||||
|
:total_time,
|
||||||
|
:average_run_time,
|
||||||
|
:min_run_time,
|
||||||
|
:max_run_time,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "correctly aggregates stats for different time periods" do
|
||||||
|
auto_summary = DiscourseAutomation::Stat.fetch_period_summaries[automation_id]
|
||||||
|
|
||||||
|
expect(auto_summary[:last_day]).to include(
|
||||||
|
total_runs: 2,
|
||||||
|
total_time: 3.0,
|
||||||
|
min_run_time: 1.0,
|
||||||
|
max_run_time: 2.0,
|
||||||
|
average_run_time: 1.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(auto_summary[:last_week]).to include(
|
||||||
|
total_runs: 4,
|
||||||
|
total_time: 5.5,
|
||||||
|
min_run_time: 0.5,
|
||||||
|
max_run_time: 2.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(auto_summary[:last_month]).to include(
|
||||||
|
total_runs: 6,
|
||||||
|
total_time: 9.5,
|
||||||
|
min_run_time: 0.5,
|
||||||
|
max_run_time: 2.5,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles multiple automations correctly" do
|
||||||
|
summaries = DiscourseAutomation::Stat.fetch_period_summaries
|
||||||
|
|
||||||
|
# Check another_automation_id data
|
||||||
|
other_summary = summaries[another_automation_id]
|
||||||
|
expect(other_summary[:last_day][:total_runs]).to eq(1)
|
||||||
|
expect(other_summary[:last_day][:total_time]).to eq(1.0)
|
||||||
|
expect(other_summary[:last_run_at].to_date).to eq((today - 1.day).to_date)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns empty hash when no stats exist" do
|
||||||
|
DiscourseAutomation::Stat.delete_all
|
||||||
|
expect(DiscourseAutomation::Stat.fetch_period_summaries).to eq({})
|
||||||
|
end
|
||||||
|
|
||||||
|
it "correctly handles automations with no stats in specific periods" do
|
||||||
|
new_automation_id = 44
|
||||||
|
|
||||||
|
DiscourseAutomation::Stat.create!(
|
||||||
|
automation_id: new_automation_id,
|
||||||
|
date: 2.days.from_now,
|
||||||
|
last_run_at: 2.days.from_now,
|
||||||
|
total_time: 1.0,
|
||||||
|
average_run_time: 1.0,
|
||||||
|
min_run_time: 1.0,
|
||||||
|
max_run_time: 1.0,
|
||||||
|
total_runs: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
summaries = DiscourseAutomation::Stat.fetch_period_summaries
|
||||||
|
|
||||||
|
expect(summaries.keys).not_to include(new_automation_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".log" do
|
||||||
|
context "with block form" do
|
||||||
|
it "measures the execution time and records it" do
|
||||||
|
# Mock Process.clock_gettime to return controlled values
|
||||||
|
allow(Process).to receive(:clock_gettime).and_return(10, 10.75)
|
||||||
|
|
||||||
|
result = DiscourseAutomation::Stat.log(automation_id) { "test result" }
|
||||||
|
|
||||||
|
expect(result).to eq("test result")
|
||||||
|
|
||||||
|
stat = DiscourseAutomation::Stat.find_by(automation_id: automation_id)
|
||||||
|
expect(stat.total_time).to eq(0.75)
|
||||||
|
expect(stat.average_run_time).to eq(0.75)
|
||||||
|
expect(stat.min_run_time).to eq(0.75)
|
||||||
|
expect(stat.max_run_time).to eq(0.75)
|
||||||
|
expect(stat.total_runs).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with direct call form" do
|
||||||
|
it "logs the provided run time" do
|
||||||
|
DiscourseAutomation::Stat.log(automation_id, run_time)
|
||||||
|
|
||||||
|
stat = DiscourseAutomation::Stat.find_by(automation_id: automation_id)
|
||||||
|
expect(stat.total_time).to eq(run_time)
|
||||||
|
expect(stat.average_run_time).to eq(run_time)
|
||||||
|
expect(stat.min_run_time).to eq(run_time)
|
||||||
|
expect(stat.max_run_time).to eq(run_time)
|
||||||
|
expect(stat.total_runs).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when updating existing stats" do
|
||||||
|
before do
|
||||||
|
freeze_time DateTime.parse("2023-01-01 12:00:00")
|
||||||
|
DiscourseAutomation::Stat.log(automation_id, 0.5)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates stats correctly for the same automation on the same day" do
|
||||||
|
freeze_time DateTime.parse("2023-01-01 14:00:00")
|
||||||
|
DiscourseAutomation::Stat.log(automation_id, 1.5)
|
||||||
|
|
||||||
|
stat = DiscourseAutomation::Stat.find_by(automation_id: automation_id)
|
||||||
|
expect(stat.total_time).to eq(2.0) # 0.5 + 1.5
|
||||||
|
expect(stat.average_run_time).to eq(1.0) # (0.5 + 1.5) / 2
|
||||||
|
expect(stat.min_run_time).to eq(0.5)
|
||||||
|
expect(stat.max_run_time).to eq(1.5)
|
||||||
|
expect(stat.total_runs).to eq(2)
|
||||||
|
expect(stat.last_run_at.to_s).to include("2023-01-01 14:00:00")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a new record for the same automation on a different day" do
|
||||||
|
freeze_time DateTime.parse("2023-01-02 12:00:00")
|
||||||
|
DiscourseAutomation::Stat.log(automation_id, 2.0)
|
||||||
|
|
||||||
|
# There should be two records now
|
||||||
|
expect(DiscourseAutomation::Stat.where(automation_id: automation_id).count).to eq(2)
|
||||||
|
|
||||||
|
# Check first day's stats
|
||||||
|
day1_stat =
|
||||||
|
DiscourseAutomation::Stat.find_by(automation_id: automation_id, date: "2023-01-01")
|
||||||
|
expect(day1_stat.total_time).to eq(0.5)
|
||||||
|
expect(day1_stat.total_runs).to eq(1)
|
||||||
|
|
||||||
|
# Check second day's stats
|
||||||
|
day2_stat =
|
||||||
|
DiscourseAutomation::Stat.find_by(automation_id: automation_id, date: "2023-01-02")
|
||||||
|
expect(day2_stat.total_time).to eq(2.0)
|
||||||
|
expect(day2_stat.total_runs).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles multiple automations on the same day" do
|
||||||
|
freeze_time DateTime.parse("2023-01-01 13:00:00")
|
||||||
|
DiscourseAutomation::Stat.log(another_automation_id, 0.7)
|
||||||
|
|
||||||
|
# Original automation should be unchanged
|
||||||
|
first_stat = DiscourseAutomation::Stat.find_by(automation_id: automation_id)
|
||||||
|
expect(first_stat.total_runs).to eq(1)
|
||||||
|
expect(first_stat.total_time).to eq(0.5)
|
||||||
|
|
||||||
|
# New automation should have its own stats
|
||||||
|
second_stat = DiscourseAutomation::Stat.find_by(automation_id: another_automation_id)
|
||||||
|
expect(second_stat.total_runs).to eq(1)
|
||||||
|
expect(second_stat.total_time).to eq(0.7)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with extreme values" do
|
||||||
|
it "correctly tracks min/max values" do
|
||||||
|
freeze_time DateTime.parse("2023-01-01 12:00:00")
|
||||||
|
|
||||||
|
# First run
|
||||||
|
DiscourseAutomation::Stat.log(automation_id, 5.0)
|
||||||
|
|
||||||
|
# Second run with lower time
|
||||||
|
DiscourseAutomation::Stat.log(automation_id, 2.0)
|
||||||
|
|
||||||
|
# Third run with higher time
|
||||||
|
DiscourseAutomation::Stat.log(automation_id, 10.0)
|
||||||
|
|
||||||
|
stat = DiscourseAutomation::Stat.find_by(automation_id: automation_id)
|
||||||
|
expect(stat.min_run_time).to eq(2.0)
|
||||||
|
expect(stat.max_run_time).to eq(10.0)
|
||||||
|
expect(stat.total_time).to eq(17.0)
|
||||||
|
expect(stat.average_run_time).to eq(17.0 / 3)
|
||||||
|
expect(stat.total_runs).to eq(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -143,6 +143,28 @@ describe DiscourseAutomation::AdminAutomationsController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when only changing enabled state" do
|
||||||
|
it "updates only the enabled state" do
|
||||||
|
original_trigger = automation.trigger
|
||||||
|
original_script = automation.script
|
||||||
|
expect(automation.enabled).to eq(true)
|
||||||
|
|
||||||
|
put "/admin/plugins/automation/automations/#{automation.id}.json",
|
||||||
|
params: {
|
||||||
|
automation: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response.parsed_body["automation"]["enabled"]).to eq(false)
|
||||||
|
automation.reload
|
||||||
|
expect(automation.enabled).to eq(false)
|
||||||
|
expect(automation.trigger).to eq(original_trigger)
|
||||||
|
expect(automation.script).to eq(original_script)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when changing trigger and script of an enabled automation" do
|
context "when changing trigger and script of an enabled automation" do
|
||||||
it "forces the automation to be disabled" do
|
it "forces the automation to be disabled" do
|
||||||
expect(automation.enabled).to eq(true)
|
expect(automation.enabled).to eq(true)
|
||||||
@ -225,6 +247,104 @@ describe DiscourseAutomation::AdminAutomationsController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#index" do
|
||||||
|
fab!(:automation1) { Fabricate(:automation, name: "First Automation") }
|
||||||
|
fab!(:automation2) { Fabricate(:automation, name: "Second Automation") }
|
||||||
|
|
||||||
|
context "when logged in as an admin" do
|
||||||
|
before { sign_in(Fabricate(:admin)) }
|
||||||
|
|
||||||
|
it "returns a list of automations" do
|
||||||
|
get "/admin/plugins/automation/automations.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
parsed_response = response.parsed_body
|
||||||
|
expect(parsed_response["automations"].length).to eq(3) # includes the default automation
|
||||||
|
|
||||||
|
automation_names = parsed_response["automations"].map { |a| a["name"] }
|
||||||
|
expect(automation_names).to include(automation1.name)
|
||||||
|
expect(automation_names).to include(automation2.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't include stats by default" do
|
||||||
|
get "/admin/plugins/automation/automations.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
parsed_response = response.parsed_body
|
||||||
|
automation_response = parsed_response["automations"].find { |a| a["id"] == automation1.id }
|
||||||
|
|
||||||
|
expect(automation_response.key?("stats")).to eq(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with stats" do
|
||||||
|
before do
|
||||||
|
# Create some stats for automation1
|
||||||
|
freeze_time DateTime.parse("2023-01-01")
|
||||||
|
|
||||||
|
# Create stats for today (within last day)
|
||||||
|
DiscourseAutomation::Stat.log(automation1.id, 0.5)
|
||||||
|
DiscourseAutomation::Stat.log(automation1.id, 1.5)
|
||||||
|
|
||||||
|
# Create stats for 3 days ago (within last week)
|
||||||
|
freeze_time DateTime.parse("2022-12-29")
|
||||||
|
DiscourseAutomation::Stat.log(automation1.id, 2.0)
|
||||||
|
|
||||||
|
# Create stats for 15 days ago (within last month)
|
||||||
|
freeze_time DateTime.parse("2022-12-17")
|
||||||
|
DiscourseAutomation::Stat.log(automation2.id, 3.0)
|
||||||
|
|
||||||
|
# Return to present
|
||||||
|
freeze_time DateTime.parse("2023-01-01")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "includes stats in the response" do
|
||||||
|
get "/admin/plugins/automation/automations.json"
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
parsed_response = response.parsed_body
|
||||||
|
automation1_response =
|
||||||
|
parsed_response["automations"].find { |a| a["id"] == automation1.id }
|
||||||
|
automation2_response =
|
||||||
|
parsed_response["automations"].find { |a| a["id"] == automation2.id }
|
||||||
|
|
||||||
|
# Verify stats exist
|
||||||
|
expect(automation1_response["stats"]).to be_present
|
||||||
|
expect(automation2_response["stats"]).to be_present
|
||||||
|
|
||||||
|
# Verify periods
|
||||||
|
expect(automation1_response["stats"]["last_day"]).to be_present
|
||||||
|
expect(automation1_response["stats"]["last_week"]).to be_present
|
||||||
|
expect(automation1_response["stats"]["last_month"]).to be_present
|
||||||
|
|
||||||
|
# Verify specific values for automation1
|
||||||
|
expect(automation1_response["stats"]["last_day"]["total_runs"]).to eq(2)
|
||||||
|
expect(automation1_response["stats"]["last_day"]["total_time"]).to eq(2.0)
|
||||||
|
expect(automation1_response["stats"]["last_day"]["average_run_time"]).to eq(1.0)
|
||||||
|
expect(automation1_response["stats"]["last_day"]["min_run_time"]).to eq(0.5)
|
||||||
|
expect(automation1_response["stats"]["last_day"]["max_run_time"]).to eq(1.5)
|
||||||
|
|
||||||
|
expect(automation1_response["stats"]["last_week"]["total_runs"]).to eq(3)
|
||||||
|
|
||||||
|
# Verify specific values for automation2
|
||||||
|
expect(automation2_response["stats"]["last_month"]["total_runs"]).to eq(1)
|
||||||
|
expect(automation2_response["stats"]["last_month"]["total_time"]).to eq(3.0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when logged in as a regular user" do
|
||||||
|
before { sign_in(Fabricate(:user)) }
|
||||||
|
|
||||||
|
it "raises a 404" do
|
||||||
|
get "/admin/plugins/automation/automations.json"
|
||||||
|
expect(response.status).to eq(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#destroy" do
|
describe "#destroy" do
|
||||||
fab!(:automation)
|
fab!(:automation)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user