1
0
Fork 0

Merge branch 'pro-release' into pro-features

Dieser Commit ist enthalten in:
Angus McLeod 2021-09-07 20:10:12 +08:00 committet von GitHub
Commit f4d419cac2
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
33 geänderte Dateien mit 965 neuen und 153 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1,112 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { equal } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
export default Component.extend({
isText: equal("value.type", "text"),
isComposer: equal("value.type", "composer"),
isDate: equal("value.type", "date"),
isTime: equal("value.type", "time"),
isDateTime: equal("value.type", "date_time"),
isNumber: equal("value.type", "number"),
isCheckbox: equal("value.type", "checkbox"),
isUrl: equal("value.type", "url"),
isUpload: equal("value.type", "upload"),
isDropdown: equal("value.type", "dropdown"),
isTag: equal("value.type", "tag"),
isCategory: equal("value.type", "category"),
isGroup: equal("value.type", "group"),
isUser: equal("fieldName", "username"),
isUserSelector: equal("value.type", "user_selector"),
isSubmittedAt: equal("fieldName", "submitted_at"),
isTextArea: equal("value.type", "textarea"),
isComposerPreview: equal("value.type", "composer_preview"),
textState: "text-collapsed",
toggleText: I18n.t("admin.wizard.submissions.expand_text"),
@discourseComputed("value")
checkboxValue(value) {
const isCheckbox = this.get("isCheckbox");
if (isCheckbox) {
if (value.value.includes("true")) {
return true;
} else if (value.value.includes("false")) {
return false;
}
}
},
@action
expandText() {
const state = this.get("textState");
if (state === "text-collapsed") {
this.set("textState", "text-expanded");
this.set("toggleText", I18n.t("admin.wizard.submissions.collapse_text"));
} else if (state === "text-expanded") {
this.set("textState", "text-collapsed");
this.set("toggleText", I18n.t("admin.wizard.submissions.expand_text"));
}
},
@discourseComputed("value")
file(value) {
const isUpload = this.get("isUpload");
if (isUpload) {
return value.value;
}
},
@discourseComputed("value")
submittedUsers(value) {
const isUserSelector = this.get("isUserSelector");
const users = [];
if (isUserSelector) {
const userData = value.value;
const usernames = [];
if (userData.indexOf(",")) {
usernames.push(...userData.split(","));
usernames.forEach((u) => {
const user = {
username: u,
url: `/u/${u}`,
};
users.push(user);
});
}
}
return users;
},
@discourseComputed("value")
userProfileUrl(value) {
const isUser = this.get("isUser");
if (isUser) {
return `/u/${value.username}`;
}
},
@discourseComputed("value")
categoryUrl(value) {
const isCategory = this.get("isCategory");
if (isCategory) {
return `/c/${value.value}`;
}
},
@discourseComputed("value")
groupUrl(value) {
const isGroup = this.get("isGroup");
if (isGroup) {
return `/g/${value.value}`;
}
},
});

Datei anzeigen

@ -9,6 +9,8 @@ export default Controller.extend({
page: 0, page: 0,
canLoadMore: true, canLoadMore: true,
logs: [], logs: [],
documentationUrl: "https://thepavilion.io/t/2818",
messageKey: "viewing",
loadLogs() { loadLogs() {
if (!this.canLoadMore) { if (!this.canLoadMore) {

Datei anzeigen

@ -0,0 +1,15 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
actions: {
save() {
this.send("closeModal");
},
resetToDefault() {
this.get("model.fields").forEach((field) => {
field.set("enabled", true);
});
},
},
});

Datei anzeigen

@ -1,6 +1,65 @@
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import { empty } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed"; import { fmt } from "discourse/lib/computed";
import showModal from "discourse/lib/show-modal";
import CustomWizard from "../models/custom-wizard";
export default Controller.extend({ export default Controller.extend({
downloadUrl: fmt("wizard.id", "/admin/wizards/submissions/%@/download"), downloadUrl: fmt("wizard.id", "/admin/wizards/submissions/%@/download"),
noResults: empty("submissions"),
page: 0,
total: 0,
loadMoreSubmissions() {
const page = this.get("page");
const wizardId = this.get("wizard.id");
this.set("loadingMore", true);
CustomWizard.submissions(wizardId, page)
.then((result) => {
if (result.submissions) {
this.get("submissions").pushObjects(result.submissions);
}
})
.finally(() => {
this.set("loadingMore", false);
});
},
@discourseComputed("submissions", "fields.@each.enabled")
displaySubmissions(submissions, fields) {
let result = [];
submissions.forEach((submission) => {
let sub = {};
Object.keys(submission).forEach((fieldId) => {
if (fields.some((f) => f.id === fieldId && f.enabled)) {
sub[fieldId] = submission[fieldId];
}
});
result.push(sub);
});
return result;
},
actions: {
loadMore() {
if (!this.loadingMore && this.submissions.length < this.total) {
this.set("page", this.get("page") + 1);
this.loadMoreSubmissions();
}
},
showEditColumnsModal() {
return showModal("admin-wizards-submissions-columns", {
model: {
fields: this.get("fields"),
submissions: this.get("submissions"),
},
});
},
},
}); });

Datei anzeigen

@ -0,0 +1,34 @@
import Controller from "@ember/controller";
import { default as discourseComputed } from "discourse-common/utils/decorators";
export default Controller.extend({
documentationUrl: "https://thepavilion.io/t/2818",
@discourseComputed("wizardId")
wizardName(wizardId) {
let currentWizard = this.wizardList.find(
(wizard) => wizard.id === wizardId
);
if (currentWizard) {
return currentWizard.name;
}
},
@discourseComputed("wizardName")
messageOpts(wizardName) {
return {
wizardName,
};
},
@discourseComputed("wizardId")
messageKey(wizardId) {
let key = "select";
if (wizardId) {
key = "viewing";
}
return key;
},
});

Datei anzeigen

@ -1,10 +1,10 @@
import { ajax } from "discourse/lib/ajax";
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
import { buildProperties, mapped, present } from "../lib/wizard-json"; import { ajax } from "discourse/lib/ajax";
import { listProperties, snakeCase } from "../lib/wizard";
import wizardSchema from "../lib/wizard-schema";
import { Promise } from "rsvp";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { Promise } from "rsvp";
import { listProperties, snakeCase } from "../lib/wizard";
import { buildProperties, mapped, present } from "../lib/wizard-json";
import wizardSchema from "../lib/wizard-schema";
const CustomWizard = EmberObject.extend({ const CustomWizard = EmberObject.extend({
save(opts) { save(opts) {
@ -211,10 +211,55 @@ CustomWizard.reopenClass({
.catch(popupAjaxError); .catch(popupAjaxError);
}, },
submissions(wizardId) { submissions(wizardId, page = null) {
let data = {};
if (page) {
data.page = page;
}
return ajax(`/admin/wizards/submissions/${wizardId}`, { return ajax(`/admin/wizards/submissions/${wizardId}`, {
type: "GET", type: "GET",
}).catch(popupAjaxError); data,
})
.then((result) => {
if (result.wizard) {
let fields = [{ id: "username", label: "User" }];
let submissions = [];
let wizard = result.wizard;
let total = result.total;
result.submissions.forEach((s) => {
let submission = {
username: s.user,
};
Object.keys(s.fields).forEach((fieldId) => {
if (!fields.some((field) => field.id === fieldId)) {
fields.push({ id: fieldId, label: s.fields[fieldId].label });
}
submission[fieldId] = s.fields[fieldId];
});
submission["submitted_at"] = s.submitted_at;
submissions.push(submission);
});
let submittedAt = {
id: "submitted_at",
label: "Submitted At",
};
fields.push(submittedAt);
return {
wizard,
fields,
submissions,
total,
};
}
})
.catch(popupAjaxError);
}, },
create(wizardJson = {}) { create(wizardJson = {}) {

Datei anzeigen

@ -1,7 +1,7 @@
import CustomWizard from "../models/custom-wizard"; import { A } from "@ember/array";
import EmberObject from "@ember/object";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import CustomWizard from "../models/custom-wizard";
const excludedMetaFields = ["route_to", "redirect_on_complete", "redirect_to"];
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
model(params) { model(params) {
@ -9,34 +9,16 @@ export default DiscourseRoute.extend({
}, },
setupController(controller, model) { setupController(controller, model) {
if (model && model.submissions) { const fields = model.fields.map((f) => {
let fields = ["username"]; const fieldsObject = EmberObject.create(f);
model.submissions.forEach((s) => { fieldsObject.enabled = true;
Object.keys(s.fields).forEach((k) => { return fieldsObject;
if (!excludedMetaFields.includes(k) && fields.indexOf(k) < 0) { });
fields.push(k); controller.setProperties({
} wizard: model.wizard,
}); fields: A(fields),
}); submissions: A(model.submissions),
total: model.total,
let submissions = []; });
model.submissions.forEach((s) => {
let submission = {
username: s.username,
};
Object.keys(s.fields).forEach((f) => {
if (fields.includes(f)) {
submission[f] = s.fields[f];
}
});
submissions.push(submission);
});
controller.setProperties({
wizard: model.wizard,
submissions,
fields,
});
}
}, },
}); });

Datei anzeigen

@ -8,6 +8,12 @@
class="refresh"}} class="refresh"}}
</div> </div>
{{wizard-message
key=messageKey
opts=messageOpts
url=documentationUrl
component="logs"}}
{{#load-more selector=".log-list tr" action=(action "loadMore") class="wizard-logs"}} {{#load-more selector=".log-list tr" action=(action "loadMore") class="wizard-logs"}}
{{#if noResults}} {{#if noResults}}
<p>{{i18n "search.no_results"}}</p> <p>{{i18n "search.no_results"}}</p>

Datei anzeigen

@ -1,8 +1,24 @@
{{#if submissions}} {{#if submissions}}
<div class="wizard-header large"> <div class="wizard-header large">
<label>{{i18n "admin.wizard.submissions.title" name=wizard.name}}</label> <label>
{{i18n "admin.wizard.submissions.title" name=wizard.name}}
</label>
<a class="btn btn-default download-link" href={{downloadUrl}} target="_blank" rel="noopener noreferrer"> <div class="controls">
{{d-button
icon="sliders-h"
label="admin.wizard.submissions.edit_columns"
action=(action "showEditColumnsModal")
class="btn-default open-edit-columns-btn download-link"
}}
</div>
<a
class="btn btn-default download-link"
href={{downloadUrl}}
target="_blank"
rel="noopener noreferrer"
>
{{d-icon "download"}} {{d-icon "download"}}
<span class="d-button-label"> <span class="d-button-label">
{{i18n "admin.wizard.submissions.download"}} {{i18n "admin.wizard.submissions.download"}}
@ -11,23 +27,39 @@
</div> </div>
<div class="wizard-submissions"> <div class="wizard-submissions">
<table> {{#load-more selector=".wizard-submissions tr" action=(action "loadMore")}}
<thead> {{#if noResults}}
<tr> <p>
{{#each fields as |f|}} {{i18n "search.no_results"}}
<th>{{f}}</th> </p>
{{/each}} {{else}}
</tr> <table>
</thead> <thead>
<tbody> <tr>
{{#each submissions as |s|}} {{#each fields as |field|}}
<tr> {{#if field.enabled}}
{{#each-in s as |k v|}} <th>
<td>{{v}}</td> {{field.label}}
{{/each-in}} </th>
</tr> {{/if}}
{{/each}} {{/each}}
</tbody> </tr>
</table> </thead>
<tbody>
{{#each displaySubmissions as |submission|}}
<tr>
{{#each-in submission as |field value|}}
<td>
{{submission-field fieldName=field value=value}}
</td>
{{/each-in}}
</tr>
{{/each}}
</tbody>
</table>
{{/if}}
{{conditional-loading-spinner condition=loadingMore}}
{{/load-more}}
</div> </div>
{{/if}} {{/if}}

Datei anzeigen

@ -1,4 +1,4 @@
<div class="admin-wizard-select"> <div class="admin-wizard-select admin-wizard-controls">
{{combo-box {{combo-box
value=wizardId value=wizardId
content=wizardList content=wizardList
@ -8,6 +8,12 @@
)}} )}}
</div> </div>
{{wizard-message
key=messageKey
opts=messageOpts
url=documentationUrl
component="submissions"}}
<div class="admin-wizard-container"> <div class="admin-wizard-container">
{{outlet}} {{outlet}}
</div> </div>

Datei anzeigen

@ -8,6 +8,10 @@
{{nav-item route="adminWizardsLogs" label="admin.wizard.log.nav_label"}} {{nav-item route="adminWizardsLogs" label="admin.wizard.log.nav_label"}}
{{nav-item route="adminWizardsManager" label="admin.wizard.manager.nav_label"}} {{nav-item route="adminWizardsManager" label="admin.wizard.manager.nav_label"}}
{{nav-item route="adminWizardsPro" label="admin.wizard.pro.nav_label"}} {{nav-item route="adminWizardsPro" label="admin.wizard.pro.nav_label"}}
<div class="admin-actions">
<a target="_blank" class="btn btn-pavilion-pro" rel="noreferrer noopener" href="https://thepavilion.io/w/support" title={{i18n "admin.wizard.pro_support_button.title"}}>{{d-icon "far-life-ring"}}{{i18n "admin.wizard.pro_support_button.label"}}</a>
</div>
{{/admin-nav}} {{/admin-nav}}
<div class="admin-container"> <div class="admin-container">

Datei anzeigen

@ -0,0 +1,163 @@
{{#if isText}}
{{value.value}}
{{/if}}
{{#if isTextArea}}
<div class="submission-long-text">
<p class="submission-long-text-content {{textState}}">
{{value.value}}
</p>
<a href {{action "expandText"}}>
{{toggleText}}
</a>
</div>
{{/if}}
{{#if isComposer}}
<div class="submission-long-text">
<p
class="submission-composer-text submission-long-text-content {{
textState
}}"
>
{{value.value}}
</p>
<a href {{action "expandText"}}>
{{toggleText}}
</a>
</div>
{{/if}}
{{#if isComposerPreview}}
{{d-icon "comment-alt"}}
<span class="submission-composer-text">
{{i18n "admin.wizard.submissions.composer_preview"}}: {{value.value}}
</span>
{{/if}}
{{#if isTextOnly}}
{{value.value}}
{{/if}}
{{#if isDate}}
<span class="submission-icon-item">
{{d-icon "calendar"}}{{value.value}}
</span>
{{/if}}
{{#if isTime}}
<span class="submission-icon-item">
{{d-icon "clock"}}{{value.value}}
</span>
{{/if}}
{{#if isDateTime}}
<span class="submission-icon-item" title={{value.value}}>
{{d-icon "calendar"}}{{format-date value.value format="medium"}}
</span>
{{/if}}
{{#if isNumber}}
{{value.value}}
{{/if}}
{{#if isCheckbox}}
{{#if checkboxValue}}
<span class="submission-icon-item submission-checkbox-true">
{{d-icon "check"}}{{value.value}}
</span>
{{else}}
<span class="submission-icon-item submission-checkbox-false">
{{d-icon "times"}}{{value.value}}
</span>
{{/if}}
{{/if}}
{{#if isUrl}}
<span class="submission-icon-item submission-url">
{{d-icon "link"}}
<a target="_blank" rel="noopener noreferrer" href={{value.value}}>
{{value.value}}
</a>
</span>
{{/if}}
{{#if isUpload}}
<a
target="_blank"
rel="noopener noreferrer"
class="attachment"
href={{file.url}}
download
>
{{file.original_filename}}
</a>
{{/if}}
{{#if isDropdown}}
<span class="submission-icon-item">
{{d-icon "check-square"}}
{{value.value}}
</span>
{{/if}}
{{#if isTag}}
{{#each value.value as |tag|}}
{{discourse-tag tag}}
{{/each}}
{{/if}}
{{#if isCategory}}
<strong>
{{i18n "admin.wizard.submissions.category_id"}}:
</strong>
<a
target="_blank"
rel="noopener noreferrer"
href={{categoryUrl}}
title={{value.value}}
>
{{value.value}}
</a>
{{/if}}
{{#if isGroup}}
<strong>
{{i18n "admin.wizard.submissions.group_id"}}:
</strong>
{{value.value}}
{{/if}}
{{#if isUserSelector}}
{{#each submittedUsers as |user|}}
{{d-icon "user"}}
<a
target="_blank"
rel="noopener noreferrer"
href={{user.url}}
title={{user.username}}
>
{{user.username}}
</a>
{{/each}}
{{/if}}
{{#if isUser}}
{{#link-to "user" value}}
{{avatar value imageSize="tiny"}}
{{/link-to}}
<a
target="_blank"
rel="noopener noreferrer"
href={{userProfileUrl}}
title={{value.name}}
>
{{value.username}}
</a>
{{/if}}
{{#if isSubmittedAt}}
<span class="submission-date" title={{value.value}}>
{{d-icon "clock"}}{{format-date value format="tiny"}}
</span>
{{/if}}

Datei anzeigen

@ -0,0 +1,32 @@
{{#d-modal-body title="directory.edit_columns.title"}}
{{#if loading}}
{{loading-spinner size="large"}}
{{else}}
<div class="edit-directory-columns-container">
{{#each model.fields as |field|}}
<div class="edit-directory-column">
<div class="left-content">
<label class="column-name">
{{input type="checkbox" checked=field.enabled}}
{{directory-table-header-title field=field.label translated=true}}
</label>
</div>
</div>
{{/each}}
</div>
{{/if}}
{{/d-modal-body}}
<div class="modal-footer">
{{d-button
class="btn-primary"
label="directory.edit_columns.save"
action=(action "save")
}}
{{d-button
class="btn-secondary reset-to-default"
label="directory.edit_columns.reset_to_default"
action=(action "resetToDefault")
}}
</div>

Datei anzeigen

@ -2,6 +2,7 @@
//= require discourse/app/mixins/singleton //= require discourse/app/mixins/singleton
//= require discourse/app/mixins/upload //= require discourse/app/mixins/upload
//= require discourse/app/mixins/composer-upload
//= require discourse/app/adapters/rest //= require discourse/app/adapters/rest

Datei anzeigen

@ -2,6 +2,7 @@
@import "wizard-manager"; @import "wizard-manager";
@import "wizard-api"; @import "wizard-api";
@import "common/components/buttons"; @import "common/components/buttons";
@import "wizard-variables";
.admin-wizard-controls { .admin-wizard-controls {
display: flex; display: flex;
@ -71,6 +72,51 @@
table td { table td {
min-width: 150px; min-width: 150px;
} }
table thead th {
text-transform: capitalize;
}
.submission-icon-item {
display: flex;
align-items: center;
svg {
margin-right: 5px;
}
}
.submission-checkbox-true {
text-transform: capitalize;
color: var(--success);
}
.submission-checkbox-false {
text-transform: capitalize;
color: var(--danger);
}
.submission-long-text {
&-content {
white-space: nowrap;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
width: 250px;
margin-bottom: 0;
&.text-expanded {
white-space: normal;
}
}
a {
font-size: var(--font-down-1);
}
}
.submission-composer-text {
font-family: monospace;
}
} }
.admin-wizards-logs { .admin-wizards-logs {
@ -203,6 +249,11 @@
&.underline { &.underline {
text-decoration: underline; text-decoration: underline;
} }
.controls {
margin-left: auto;
margin-right: 0.5rem;
}
} }
.admin-wizard-buttons { .admin-wizard-buttons {
@ -833,3 +884,22 @@
padding-top: .25em; padding-top: .25em;
} }
} }
.btn.btn-pavilion-pro {
background: var(--pavilion-primary);
color: var(--pavilion-secondary);
.d-icon {
color: var(--pavilion-secondary);
}
&:hover,
&:focus {
background: darken($pavilionPrimary, 5%);
&[href],
svg.d-icon {
color: darken($pavilionSecondary, 10%);
}
}
}

Datei anzeigen

@ -0,0 +1,7 @@
$pavilionPrimary: #3c1c8c;
$pavilionSecondary: #ffffff;
:root {
--pavilion-primary: #3c1c8c;
--pavilion-secondary: #ffffff;
}

Datei anzeigen

@ -59,6 +59,10 @@ en:
condition: "Condition" condition: "Condition"
index: "Index" index: "Index"
pro_support_button:
title: "Request Pro Support"
label: "Pro Support"
message: message:
wizard: wizard:
select: "Select a wizard, or create a new one" select: "Select a wizard, or create a new one"
@ -99,6 +103,13 @@ en:
subscription_inactive: "Your subscription is inactive on this forum. Read more in <a href='https://thepavilion.io/t/3652'>the documentation</a>." subscription_inactive: "Your subscription is inactive on this forum. Read more in <a href='https://thepavilion.io/t/3652'>the documentation</a>."
unauthorized: "You're unauthorized. If you have a subscription, it will become inactive in the next 48 hours." unauthorized: "You're unauthorized. If you have a subscription, it will become inactive in the next 48 hours."
unauthorize_failed: Failed to unauthorize. unauthorize_failed: Failed to unauthorize.
submissions:
select: "Select a wizard to see its submissions"
viewing: "You're viewing the submissions of the %{wizardName}. Click 'Download' on the right to download them."
documentation: "Check out the submissions documentation"
logs:
viewing: "View recent logs for wizards on the forum"
documentation: "Check out the logs documentation
editor: editor:
show: "Show" show: "Show"
@ -367,6 +378,12 @@ en:
nav_label: "Submissions" nav_label: "Submissions"
title: "{{name}} Submissions" title: "{{name}} Submissions"
download: "Download" download: "Download"
edit_columns: "Edit Columns"
expand_text: "Read More"
collapse_text: "Show Less"
group_id: "Group ID"
category_id: "Category ID"
composer_preview: "Composer Preview"
api: api:
label: "API" label: "API"

Datei anzeigen

@ -13,12 +13,16 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController
def show def show
render_json_dump( render_json_dump(
wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false), wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false),
submissions: ActiveModel::ArraySerializer.new(ordered_submissions, each_serializer: CustomWizard::SubmissionSerializer) submissions: ActiveModel::ArraySerializer.new(
submission_list.submissions,
each_serializer: CustomWizard::SubmissionSerializer
),
total: submission_list.total
) )
end end
def download def download
send_data ordered_submissions.to_json, send_data submission_list.submissions.to_json,
filename: "#{Discourse.current_hostname}-wizard-submissions-#{@wizard.name}.json", filename: "#{Discourse.current_hostname}-wizard-submissions-#{@wizard.name}.json",
content_type: "application/json", content_type: "application/json",
disposition: "attachment" disposition: "attachment"
@ -26,7 +30,7 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController
protected protected
def ordered_submissions def submission_list
CustomWizard::Submission.list(@wizard, order_by: 'id') CustomWizard::Submission.list(@wizard, page: params[:page].to_i)
end end
end end

Datei anzeigen

@ -69,7 +69,8 @@ class CustomWizard::WizardController < ::ApplicationController
result.merge!(redirect_to: submission.redirect_to) result.merge!(redirect_to: submission.redirect_to)
end end
wizard.final_cleanup! submission.remove if submission.present?
wizard.reset
end end
render json: result render json: result

Datei anzeigen

@ -0,0 +1,77 @@
# frozen_string_literal: true
class SplitCustomWizardLogFields < ActiveRecord::Migration[6.1]
def change
reversible do |dir|
dir.up do
# separate wizard/action/user into their own keys
wizard_logs = PluginStoreRow.where("
plugin_name = 'custom_wizard_log'
")
if wizard_logs.exists?
wizard_logs.each do |row|
begin
log_json = JSON.parse(row.value)
rescue TypeError, JSON::ParserError
next
end
if log_json.key?('message') && log_json['message'].is_a?(String)
attr_strs = []
# assumes no whitespace in the values
attr_strs << log_json['message'].slice!(/(wizard: \S*; )/, 1)
attr_strs << log_json['message'].slice!(/(action: \S*; )/, 1)
attr_strs << log_json['message'].slice!(/(user: \S*; )/, 1)
attr_strs.each do |attr_str|
if attr_str.is_a? String
attr_str.gsub!(/[;]/ , "")
key, value = attr_str.split(': ')
value.strip! if value
log_json[key] = value ? value : ''
end
end
row.value = log_json.to_json
row.save
end
end
end
end
dir.down do
wizard_logs = PluginStoreRow.where("
plugin_name = 'custom_wizard_log'
")
if wizard_logs.exists?
wizard_logs.each do |row|
begin
log_json = JSON.parse(row.value)
rescue TypeError, JSON::ParserError
next
end
# concatenate wizard/action/user to start of message
prefixes = log_json.extract!('wizard', 'action', 'user')
message_prefix = prefixes.map { |k, v| "#{k}: #{v}" }.join('; ')
if log_json.key?('message')
log_json['message'] = "#{message_prefix}; #{log_json['message']}"
else
log_json['message'] = message_prefix
end
row.value = log_json.to_json
row.save
end
end
end
end
end
end

Datei anzeigen

@ -742,15 +742,12 @@ class CustomWizard::Action
end end
def save_log def save_log
log = "wizard: #{@wizard.id}; action: #{action['type']}; user: #{user.username}" CustomWizard::Log.create(
@wizard.id,
if @log.any? action['type'],
@log.each do |item| user.username,
log += "; #{item.to_s}" @log.join('; ')
end )
end
CustomWizard::Log.create(log)
end end
def pro_actions def pro_actions

Datei anzeigen

@ -2,22 +2,28 @@
class CustomWizard::Log class CustomWizard::Log
include ActiveModel::Serialization include ActiveModel::Serialization
attr_accessor :message, :date attr_accessor :date, :wizard, :action, :user, :message
PAGE_LIMIT = 100 PAGE_LIMIT = 100
def initialize(attrs) def initialize(attrs)
@message = attrs['message']
@date = attrs['date'] @date = attrs['date']
@wizard = attrs['wizard']
@action = attrs['action']
@user = attrs['user']
@message = attrs['message']
end end
def self.create(message) def self.create(wizard, action, user, message)
log_id = SecureRandom.hex(12) log_id = SecureRandom.hex(12)
PluginStore.set('custom_wizard_log', PluginStore.set('custom_wizard_log',
log_id.to_s, log_id.to_s,
{ {
date: Time.now, date: Time.now,
wizard: wizard,
action: action,
user: user,
message: message message: message
} }
) )

Datei anzeigen

@ -2,6 +2,7 @@
class CustomWizard::Submission class CustomWizard::Submission
include ActiveModel::SerializerSupport include ActiveModel::SerializerSupport
PAGE_LIMIT = 50
KEY ||= "submissions" KEY ||= "submissions"
META ||= %w(updated_at submitted_at route_to redirect_on_complete redirect_to) META ||= %w(updated_at submitted_at route_to redirect_on_complete redirect_to)
@ -44,7 +45,7 @@ class CustomWizard::Submission
validate validate
submission_list = self.class.list(wizard, user_id: user.id) submission_list = self.class.list(wizard, user_id: user.id)
submissions = submission_list.select { |submission| submission.id != self.id } submissions = submission_list.submissions.select { |submission| submission.id != self.id }
self.updated_at = Time.now.iso8601 self.updated_at = Time.now.iso8601
submissions.push(self) submissions.push(self)
@ -93,14 +94,25 @@ class CustomWizard::Submission
end end
def self.get(wizard, user_id) def self.get(wizard, user_id)
data = PluginStore.get("#{wizard.id}_#{KEY}", user_id).first data = PluginStore.get("#{wizard.id}_#{KEY}", user_id).last
new(wizard, data, user_id) new(wizard, data, user_id)
end end
def remove
if present?
user_id = @user.id
wizard_id = @wizard.id
submission_id = @id
data = PluginStore.get("#{wizard_id}_#{KEY}", user_id)
data.delete_if { |sub| sub["id"] == submission_id }
PluginStore.set("#{wizard_id}_#{KEY}", user_id, data)
end
end
def self.cleanup_incomplete_submissions(wizard) def self.cleanup_incomplete_submissions(wizard)
user_id = wizard.user.id user_id = wizard.user.id
all_submissions = list(wizard, user_id: user_id) all_submissions = list(wizard, user_id: user_id)
sorted_submissions = all_submissions.sort_by do |submission| sorted_submissions = all_submissions.submissions.sort_by do |submission|
zero_epoch_time = DateTime.strptime("0", '%s') zero_epoch_time = DateTime.strptime("0", '%s')
[ [
submission.submitted_at ? Time.iso8601(submission.submitted_at) : zero_epoch_time, submission.submitted_at ? Time.iso8601(submission.submitted_at) : zero_epoch_time,
@ -120,23 +132,34 @@ class CustomWizard::Submission
PluginStore.set("#{wizard.id}_#{KEY}", user_id, valid_data) PluginStore.set("#{wizard.id}_#{KEY}", user_id, valid_data)
end end
def self.list(wizard, user_id: nil, order_by: nil) def self.list(wizard, user_id: nil, order_by: nil, page: nil)
params = { plugin_name: "#{wizard.id}_#{KEY}" } params = { plugin_name: "#{wizard.id}_#{KEY}" }
params[:key] = user_id if user_id.present? params[:key] = user_id if user_id.present?
query = PluginStoreRow.where(params) query = PluginStoreRow.where(params)
query = query.order("#{order_by} DESC") if order_by.present? result = OpenStruct.new(submissions: [], total: nil)
result = []
query.each do |record| query.each do |record|
if (submission_data = ::JSON.parse(record.value)).any? if (submission_data = ::JSON.parse(record.value)).any?
submission_data.each do |data| submission_data.each do |data|
result.push(new(wizard, data, record.key)) result.submissions.push(new(wizard, data, record.key))
end end
end end
end end
result.total = result.submissions.size
if !page.nil?
start = page * PAGE_LIMIT
length = PAGE_LIMIT
if result.submissions.length > start
result.submissions = result.submissions[start, length]
else
result.submissions = []
end
end
result result
end end
end end

Datei anzeigen

@ -32,7 +32,8 @@ class CustomWizard::Wizard
:actions, :actions,
:action_ids, :action_ids,
:user, :user,
:submissions :submissions,
:template
attr_reader :all_step_ids attr_reader :all_step_ids
@ -79,6 +80,7 @@ class CustomWizard::Wizard
@actions = attrs['actions'] || [] @actions = attrs['actions'] || []
@action_ids = @actions.map { |a| a['id'] } @action_ids = @actions.map { |a| a['id'] }
@template = attrs
end end
def cast_bool(val) def cast_bool(val)
@ -272,7 +274,7 @@ class CustomWizard::Wizard
def submissions def submissions
return nil unless user.present? return nil unless user.present?
@submissions ||= CustomWizard::Submission.list(self, user_id: user.id) @submissions ||= CustomWizard::Submission.list(self, user_id: user.id).submissions
end end
def current_submission def current_submission

Datei anzeigen

@ -33,6 +33,14 @@ if respond_to?(:register_svg_icon)
register_svg_icon "chevron-right" register_svg_icon "chevron-right"
register_svg_icon "chevron-left" register_svg_icon "chevron-left"
register_svg_icon "save" register_svg_icon "save"
register_svg_icon "sliders-h"
register_svg_icon "calendar"
register_svg_icon "check"
register_svg_icon "times"
register_svg_icon "clock"
register_svg_icon "link"
register_svg_icon "comment-alt"
register_svg_icon "far-life-ring"
register_svg_icon "arrow-right" register_svg_icon "arrow-right"
end end

Datei anzeigen

@ -1,4 +1,4 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomWizard::LogSerializer < ApplicationSerializer class CustomWizard::LogSerializer < ApplicationSerializer
attributes :message, :date attributes :date, :wizard, :action, :user, :message
end end

Datei anzeigen

@ -1,16 +1,32 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomWizard::SubmissionSerializer < ApplicationSerializer class CustomWizard::SubmissionSerializer < ApplicationSerializer
attributes :id, attributes :id,
:username,
:fields, :fields,
:submitted_at, :submitted_at
:route_to,
:redirect_on_complete,
:redirect_to
def username has_one :user, serializer: ::BasicUserSerializer, embed: :objects
object.user.present? ?
object.user.username : def include_user?
I18n.t('admin.wizard.submission.no_user', user_id: object.user_id) object.user.present?
end
def fields
@fields ||= begin
result = {}
object.wizard.template['steps'].each do |step|
step['fields'].each do |field|
if value = object.fields[field['id']]
result[field['id']] = {
value: value,
type: field['type'],
label: field['label']
}
end
end
end
result
end
end end
end end

Datei anzeigen

@ -3,9 +3,9 @@ require_relative '../../plugin_helper'
describe CustomWizard::Log do describe CustomWizard::Log do
before do before do
CustomWizard::Log.create("First log message") CustomWizard::Log.create('first-test-wizard', 'perform_first_action', 'first_test_user', 'First log message')
CustomWizard::Log.create("Second log message") CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message')
CustomWizard::Log.create("Third log message") CustomWizard::Log.create('third-test-wizard', 'perform_third_action', 'third_test_user', 'Third log message')
end end
it "creates logs" do it "creates logs" do

Datei anzeigen

@ -8,32 +8,44 @@ describe CustomWizard::Submission do
before do before do
CustomWizard::Template.save(template_json, skip_jobs: true) CustomWizard::Template.save(template_json, skip_jobs: true)
template_json_2 = template_json.dup
template_json_2["id"] = "super_mega_fun_wizard_2"
CustomWizard::Template.save(template_json_2, skip_jobs: true)
@wizard = CustomWizard::Wizard.create(template_json["id"], user) @wizard = CustomWizard::Wizard.create(template_json["id"], user)
@wizard2 = CustomWizard::Wizard.create(template_json["id"], user2) described_class.new(@wizard, step_1_field_1: "I am user submission").save
@wizard3 = CustomWizard::Wizard.create(template_json_2["id"], user)
described_class.new(@wizard, step_1_field_1: "I am a user submission").save
described_class.new(@wizard2, step_1_field_1: "I am another user's submission").save
described_class.new(@wizard3, step_1_field_1: "I am a user submission on another wizard").save
end end
it "saves a user's submission" do it "saves a user's submission" do
expect( expect(
described_class.get(@wizard, user.id).fields["step_1_field_1"] described_class.get(@wizard, user.id).fields["step_1_field_1"]
).to eq("I am a user submission") ).to eq("I am user submission")
end end
it "list submissions by wizard" do context "#list" do
expect(described_class.list(@wizard).size).to eq(2) before do
end template_json_2 = template_json.dup
template_json_2["id"] = "super_mega_fun_wizard_2"
CustomWizard::Template.save(template_json_2, skip_jobs: true)
it "list submissions by wizard and user" do @wizard2 = CustomWizard::Wizard.create(template_json["id"], user2)
expect(described_class.list(@wizard, user_id: user.id).size).to eq(1) @wizard3 = CustomWizard::Wizard.create(template_json_2["id"], user)
@count = CustomWizard::Submission::PAGE_LIMIT + 20
@count.times do |index|
described_class.new(@wizard, step_1_field_1: "I am user submission #{index + 1}").save
end
described_class.new(@wizard2, step_1_field_1: "I am another user's submission").save
described_class.new(@wizard3, step_1_field_1: "I am a user submission on another wizard").save
end
it "list submissions by wizard" do
expect(described_class.list(@wizard).total).to eq(@count + 2)
end
it "list submissions by wizard and user" do
expect(described_class.list(@wizard, user_id: user.id).total).to eq(@count + 1)
end
it "paginates submission lists" do
expect(described_class.list(@wizard, page: 1).submissions.size).to eq((@count + 2) - CustomWizard::Submission::PAGE_LIMIT)
end
end end
context "#cleanup_incomplete_submissions" do context "#cleanup_incomplete_submissions" do
@ -42,10 +54,10 @@ describe CustomWizard::Submission do
described_class.new(@wizard, step_1_field_1: "I am the second submission").save described_class.new(@wizard, step_1_field_1: "I am the second submission").save
builder = CustomWizard::Builder.new(@wizard.id, @wizard.user) builder = CustomWizard::Builder.new(@wizard.id, @wizard.user)
builder.build builder.build
sub_list = described_class.list(@wizard, user_id: @wizard.user.id) submissions = described_class.list(@wizard, user_id: @wizard.user.id).submissions
expect(sub_list.length).to eq(1) expect(submissions.length).to eq(1)
expect(sub_list.first.fields["step_1_field_1"]).to eq("I am the second submission") expect(submissions.first.fields["step_1_field_1"]).to eq("I am the second submission")
end end
it "handles submissions without 'updated_at' field correctly" do it "handles submissions without 'updated_at' field correctly" do
@ -58,10 +70,10 @@ describe CustomWizard::Submission do
PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, sub_data) PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, sub_data)
builder = CustomWizard::Builder.new(@wizard.id, @wizard.user) builder = CustomWizard::Builder.new(@wizard.id, @wizard.user)
builder.build builder.build
sub_list = described_class.list(@wizard, user_id: @wizard.user.id) submissions = described_class.list(@wizard, user_id: @wizard.user.id).submissions
expect(sub_list.length).to eq(1) expect(submissions.length).to eq(1)
expect(sub_list.first.fields["step_1_field_1"]).to eq("I am the third submission") expect(submissions.first.fields["step_1_field_1"]).to eq("I am the third submission")
end end
it "handles submissions with and without 'updated_at' field correctly" do it "handles submissions with and without 'updated_at' field correctly" do
@ -75,10 +87,10 @@ describe CustomWizard::Submission do
builder = CustomWizard::Builder.new(@wizard.id, @wizard.user) builder = CustomWizard::Builder.new(@wizard.id, @wizard.user)
builder.build builder.build
sub_list = described_class.list(@wizard, user_id: @wizard.user.id) submissions = described_class.list(@wizard, user_id: @wizard.user.id).submissions
expect(sub_list.length).to eq(1) expect(submissions.length).to eq(1)
expect(sub_list.first.fields["step_1_field_1"]).to eq("I am the third submission") expect(submissions.first.fields["step_1_field_1"]).to eq("I am the third submission")
end end
end end
end end

Datei anzeigen

@ -5,9 +5,9 @@ describe CustomWizard::AdminLogsController do
fab!(:admin_user) { Fabricate(:user, admin: true) } fab!(:admin_user) { Fabricate(:user, admin: true) }
before do before do
CustomWizard::Log.create("First log message") CustomWizard::Log.create('first-test-wizard', 'perform_first_action', 'first_test_user', 'First log message')
CustomWizard::Log.create("Second log message") CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message')
CustomWizard::Log.create("Third log message") CustomWizard::Log.create('third-test-wizard', 'perform_third_action', 'third_test_user', 'Third log message')
sign_in(admin_user) sign_in(admin_user)
end end

Datei anzeigen

@ -33,30 +33,55 @@ describe CustomWizard::WizardController do
expect(response.parsed_body["error"]).to eq("We couldn't find a wizard at that address.") expect(response.parsed_body["error"]).to eq("We couldn't find a wizard at that address.")
end end
it 'skips a wizard if user is allowed to skip' do context 'when user skips the wizard' do
put '/w/super-mega-fun-wizard/skip.json'
expect(response.status).to eq(200)
end
it 'lets user skip if user cant access wizard' do it 'skips a wizard if user is allowed to skip' do
@template["permitted"] = permitted_json["permitted"] put '/w/super-mega-fun-wizard/skip.json'
CustomWizard::Template.save(@template, skip_jobs: true) expect(response.status).to eq(200)
end
put '/w/super-mega-fun-wizard/skip.json' it 'lets user skip if user cant access wizard' do
expect(response.status).to eq(200) @template["permitted"] = permitted_json["permitted"]
end CustomWizard::Template.save(@template, skip_jobs: true)
it 'returns a no skip message if user is not allowed to skip' do put '/w/super-mega-fun-wizard/skip.json'
@template['required'] = 'true' expect(response.status).to eq(200)
CustomWizard::Template.save(@template) end
put '/w/super-mega-fun-wizard/skip.json'
expect(response.parsed_body['error']).to eq("Wizard can't be skipped")
end
it 'skip response contains a redirect_to if in users submissions' do it 'returns a no skip message if user is not allowed to skip' do
@wizard = CustomWizard::Wizard.create(@template["id"], user) @template['required'] = 'true'
CustomWizard::Submission.new(@wizard, redirect_to: "/t/2").save CustomWizard::Template.save(@template)
put '/w/super-mega-fun-wizard/skip.json' put '/w/super-mega-fun-wizard/skip.json'
expect(response.parsed_body['redirect_to']).to eq('/t/2') expect(response.parsed_body['error']).to eq("Wizard can't be skipped")
end
it 'skip response contains a redirect_to if in users submissions' do
@wizard = CustomWizard::Wizard.create(@template["id"], user)
CustomWizard::Submission.new(@wizard, redirect_to: "/t/2").save
put '/w/super-mega-fun-wizard/skip.json'
expect(response.parsed_body['redirect_to']).to eq('/t/2')
end
it "deletes the submission if user has filled up some data" do
@wizard = CustomWizard::Wizard.create(@template["id"], user)
CustomWizard::Submission.new(@wizard, step_1_field_1: "Hello World").save
current_submission = @wizard.current_submission
put '/w/super-mega-fun-wizard/skip.json'
submissions = CustomWizard::Submission.list(@wizard).submissions
expect(submissions.any? { |submission| submission.id == current_submission.id }).to eq(false)
end
it "starts from the first step if user visits after skipping the wizard" do
put '/w/super-mega-fun-wizard/steps/step_1.json', params: {
fields: {
step_1_field_1: "Text input"
}
}
put '/w/super-mega-fun-wizard/skip.json'
get '/w/super-mega-fun-wizard.json'
expect(response.parsed_body["start"]).to eq('step_1')
end
end end
end end

Datei anzeigen

@ -6,14 +6,17 @@ describe CustomWizard::LogSerializer do
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
it 'should return log attributes' do it 'should return log attributes' do
CustomWizard::Log.create("First log message") CustomWizard::Log.create('first-test-wizard', 'perform_first_action', 'first_test_user', 'First log message')
CustomWizard::Log.create("Second log message") CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message')
json_array = ActiveModel::ArraySerializer.new( json_array = ActiveModel::ArraySerializer.new(
CustomWizard::Log.list(0), CustomWizard::Log.list(0),
each_serializer: CustomWizard::LogSerializer each_serializer: CustomWizard::LogSerializer
).as_json ).as_json
expect(json_array.length).to eq(2) expect(json_array.length).to eq(2)
expect(json_array[0][:wizard]).to eq("second-test-wizard")
expect(json_array[0][:action]).to eq("perform_second_action")
expect(json_array[0][:user]).to eq("second_test_user")
expect(json_array[0][:message]).to eq("Second log message") expect(json_array[0][:message]).to eq("Second log message")
end end
end end

Datei anzeigen

@ -0,0 +1,51 @@
# frozen_string_literal: true
require_relative '../../plugin_helper'
describe CustomWizard::SubmissionSerializer do
fab!(:user) { Fabricate(:user) }
let(:template_json) {
JSON.parse(File.open(
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json"
).read)
}
before do
CustomWizard::Template.save(template_json, skip_jobs: true)
wizard = CustomWizard::Wizard.create(template_json["id"], user)
CustomWizard::Submission.new(wizard,
step_1_field_1: "I am user submission",
submitted_at: Time.now.iso8601
).save
@list = CustomWizard::Submission.list(wizard, page: 0)
end
it 'should return submission attributes' do
json_array = ActiveModel::ArraySerializer.new(
@list.submissions,
each_serializer: described_class
).as_json
expect(json_array.length).to eq(1)
expect(json_array[0][:id].present?).to eq(true)
expect(json_array[0][:user].present?).to eq(true)
expect(json_array[0][:submitted_at].present?).to eq(true)
end
it "should return field values, types and labels" do
json_array = ActiveModel::ArraySerializer.new(
@list.submissions,
each_serializer: described_class
).as_json
expect(json_array.length).to eq(1)
expect(json_array[0][:fields].as_json).to eq({
"step_1_field_1": {
"value": "I am user submission",
"type": "text",
"label": "Text"
}
}.as_json)
end
end