administration-one 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.dockerignore +51 -0
- data/.gitattributes +9 -0
- data/.gitignore +37 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +4 -0
- data/administration_one.gemspec +22 -0
- data/lib/administration-one.rb +1 -0
- data/lib/administration_one/version.rb +3 -0
- data/lib/administration_one.rb +4 -0
- data/lib/generators/admin/install/USAGE +5 -0
- data/lib/generators/admin/install/install_generator.rb +142 -0
- data/lib/generators/admin/install/templates/controllers/admin/base_controller.rb +20 -0
- data/lib/generators/admin/install/templates/controllers/admin/home_controller.rb +4 -0
- data/lib/generators/admin/install/templates/controllers/admin/passwords_controller.rb +35 -0
- data/lib/generators/admin/install/templates/controllers/admin/profile_controller.rb +23 -0
- data/lib/generators/admin/install/templates/controllers/admin/sessions_controller.rb +24 -0
- data/lib/generators/admin/install/templates/controllers/admin/users_controller.rb +54 -0
- data/lib/generators/admin/install/templates/css/custom.css +85 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_ejs_tags.html.erb +8 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_flash_messages.html.erb +9 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_footer.html.erb +34 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_form_errors_messages.html.erb +10 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_javascript_tags.html.erb +5 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_page_header.html.erb +7 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_page_header_actions.html.erb +5 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_page_header_breadcrumb.html.erb +12 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_primary_navbar.html.erb +63 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_secondary_navbar.html.erb +12 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_secondary_navbar_links.html.erb +20 -0
- data/lib/generators/admin/install/templates/erb/admin/base/_stylesheet_link_tags.html.erb +5 -0
- data/lib/generators/admin/install/templates/erb/admin/home/index.html.erb +8 -0
- data/lib/generators/admin/install/templates/erb/admin/passwords/edit.html.erb +16 -0
- data/lib/generators/admin/install/templates/erb/admin/passwords/new.html.erb +11 -0
- data/lib/generators/admin/install/templates/erb/admin/profile/edit.html.erb +48 -0
- data/lib/generators/admin/install/templates/erb/admin/profile/show.html.erb +22 -0
- data/lib/generators/admin/install/templates/erb/admin/sessions/new.html.erb +21 -0
- data/lib/generators/admin/install/templates/erb/admin/users/_form.html.erb +48 -0
- data/lib/generators/admin/install/templates/erb/admin/users/edit.html.erb +17 -0
- data/lib/generators/admin/install/templates/erb/admin/users/index.html.erb +78 -0
- data/lib/generators/admin/install/templates/erb/admin/users/new.html.erb +16 -0
- data/lib/generators/admin/install/templates/erb/admin/users/show.html.erb +33 -0
- data/lib/generators/admin/install/templates/erb/layouts/admin/authentication.html.erb +32 -0
- data/lib/generators/admin/install/templates/erb/layouts/admin/base.html.erb +31 -0
- data/lib/generators/admin/install/templates/helpers/admin/application_helper.rb +25 -0
- data/lib/generators/admin/install/templates/images/admin/default_avatar.png +0 -0
- data/lib/generators/admin/install/templates/images/admin/logo.svg +4 -0
- data/lib/generators/admin/install/templates/jobs/attach_avatar_to_user_job.rb +21 -0
- data/lib/generators/admin/install/templates/js/editor.js +101 -0
- data/lib/generators/admin/install/templates/js/flash_message.js +5 -0
- data/lib/generators/admin/install/templates/js/stimulus.js +3 -0
- data/lib/generators/admin/install/templates/js/theme_switcher.js +93 -0
- data/lib/generators/admin/install/templates/js/time_zone.js +3 -0
- data/lib/generators/admin/install/templates/migrations/create_users.rb.tt +15 -0
- data/lib/generators/admin/install/templates/models/admin/application_record.rb +3 -0
- data/lib/generators/admin/install/templates/models/application_record.rb +3 -0
- data/lib/generators/admin/install/templates/models/user.rb +34 -0
- data/lib/generators/admin/install/templates/modules/ejs_parser.rb +119 -0
- data/lib/generators/admin/install/templates/passwords_mailer/reset_admin.html.erb +4 -0
- data/lib/generators/admin/install/templates/passwords_mailer/reset_admin.text.erb +2 -0
- data/lib/generators/admin/install/templates/seeds.rb +12 -0
- data/lib/generators/admin/install/templates/test_unit/controllers/admin/home_controller_test.rb +12 -0
- data/lib/generators/admin/install/templates/test_unit/controllers/admin/profile_controller_test.rb +26 -0
- data/lib/generators/admin/install/templates/test_unit/controllers/admin/sessions_controller_test.rb +36 -0
- data/lib/generators/admin/install/templates/test_unit/controllers/admin/users_controller_test.rb +55 -0
- data/lib/generators/admin/install/templates/test_unit/models/user_test.rb +34 -0
- data/lib/generators/admin/install/templates/test_unit/test_helper.rb +25 -0
- data/lib/generators/admin/install/templates/test_unit/users.yml +13 -0
- data/lib/generators/admin/scaffold/USAGE +5 -0
- data/lib/generators/admin/scaffold/scaffold_generator.rb +132 -0
- data/lib/generators/admin/scaffold/templates/controller.rb.tt +58 -0
- data/lib/generators/admin/scaffold/templates/erb/_form.html.erb.tt +48 -0
- data/lib/generators/admin/scaffold/templates/erb/edit.html.erb.tt +17 -0
- data/lib/generators/admin/scaffold/templates/erb/index.html.erb.tt +76 -0
- data/lib/generators/admin/scaffold/templates/erb/new.html.erb.tt +16 -0
- data/lib/generators/admin/scaffold/templates/erb/show.html.erb.tt +39 -0
- data/lib/generators/admin/scaffold/templates/functional_test.rb.tt +53 -0
- metadata +124 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
require "pagy/extras/bootstrap"
|
2
|
+
|
3
|
+
module Admin::ApplicationHelper
|
4
|
+
include Pagy::Frontend
|
5
|
+
|
6
|
+
def title
|
7
|
+
content_for(:title) || Rails.application.class.to_s.split("::").first
|
8
|
+
end
|
9
|
+
|
10
|
+
def active_nav_item(*names)
|
11
|
+
names.include?(controller_path) ? "active" : ""
|
12
|
+
end
|
13
|
+
|
14
|
+
def true_or_false_icon(statement)
|
15
|
+
if statement == true
|
16
|
+
'<span class="true"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-check"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l5 5l10 -10" /></svg></span>'.html_safe
|
17
|
+
else
|
18
|
+
'<span class="false"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-x"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg></span>'.html_safe
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def path_to_avatar(user)
|
23
|
+
user.avatar.attached? ? rails_blob_path(user.avatar, only_path: true) : "admin/default_avatar.png"
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,4 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232 68">
|
2
|
+
<path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#206bc4"/>
|
3
|
+
<path d="M105.8 46.1c.4 0 .9.2 1.2.6s.6 1 .6 1.7c0 .9-.5 1.6-1.4 2.2s-2 .9-3.2.9c-2 0-3.7-.4-5-1.3s-2-2.6-2-5.4V31.6h-2.2c-.8 0-1.4-.3-1.9-.8s-.9-1.1-.9-1.9c0-.7.3-1.4.8-1.8s1.2-.7 1.9-.7h2.2v-3.1c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v3.1h3.4c.8 0 1.4.3 1.9.8s.8 1.2.8 1.9-.3 1.4-.8 1.8-1.2.7-1.9.7h-3.4v13c0 .7.2 1.2.5 1.5s.8.5 1.4.5c.3 0 .6-.1 1.1-.2.5-.2.8-.3 1.2-.3zm28-20.7c.8 0 1.5.3 2.1.8.5.5.8 1.2.8 2.1v20.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2-.8-.8-1.2-.8-2.1c-.8.9-1.9 1.7-3.2 2.4-1.3.7-2.8 1-4.3 1-2.2 0-4.2-.6-6-1.7-1.8-1.1-3.2-2.7-4.2-4.7s-1.6-4.3-1.6-6.9c0-2.6.5-4.9 1.5-6.9s2.4-3.6 4.2-4.8c1.8-1.1 3.7-1.7 5.9-1.7 1.5 0 3 .3 4.3.8 1.3.6 2.5 1.3 3.4 2.1 0-.8.3-1.5.8-2.1.5-.5 1.2-.7 2-.7zm-9.7 21.3c2.1 0 3.8-.8 5.1-2.3s2-3.4 2-5.7-.7-4.2-2-5.8c-1.3-1.5-3-2.3-5.1-2.3-2 0-3.7.8-5 2.3-1.3 1.5-2 3.5-2 5.8s.6 4.2 1.9 5.7 3 2.3 5.1 2.3zm32.1-21.3c2.2 0 4.2.6 6 1.7 1.8 1.1 3.2 2.7 4.2 4.7s1.6 4.3 1.6 6.9-.5 4.9-1.5 6.9-2.4 3.6-4.2 4.8c-1.8 1.1-3.7 1.7-5.9 1.7-1.5 0-3-.3-4.3-.9s-2.5-1.4-3.4-2.3v.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.5-.8-1.2-.8-2.1V18.9c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v10c.8-1 1.8-1.8 3.2-2.5 1.3-.7 2.8-1 4.3-1zm-.7 21.3c2 0 3.7-.8 5-2.3s2-3.5 2-5.8-.6-4.2-1.9-5.7-3-2.3-5.1-2.3-3.8.8-5.1 2.3-2 3.4-2 5.7.7 4.2 2 5.8c1.3 1.6 3 2.3 5.1 2.3zm23.6 1.9c0 .8-.3 1.5-.8 2.1s-1.3.8-2.1.8-1.5-.3-2-.8-.8-1.3-.8-2.1V18.9c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v29.7zm29.3-10.5c0 .8-.3 1.4-.9 1.9-.6.5-1.2.7-2 .7h-15.8c.4 1.9 1.3 3.4 2.6 4.4 1.4 1.1 2.9 1.6 4.7 1.6 1.3 0 2.3-.1 3.1-.4.7-.2 1.3-.5 1.8-.8.4-.3.7-.5.9-.6.6-.3 1.1-.4 1.6-.4.7 0 1.2.2 1.7.7s.7 1 .7 1.7c0 .9-.4 1.6-1.3 2.4-.9.7-2.1 1.4-3.6 1.9s-3 .8-4.6.8c-2.7 0-5-.6-7-1.7s-3.5-2.7-4.6-4.6-1.6-4.2-1.6-6.6c0-2.8.6-5.2 1.7-7.2s2.7-3.7 4.6-4.8 3.9-1.7 6-1.7 4.1.6 6 1.7 3.4 2.7 4.5 4.7c.9 1.9 1.5 4.1 1.5 6.3zm-12.2-7.5c-3.7 0-5.9 1.7-6.6 5.2h12.6v-.3c-.1-1.3-.8-2.5-2-3.5s-2.5-1.4-4-1.4zm30.3-5.2c1 0 1.8.3 2.4.8.7.5 1 1.2 1 1.9 0 1-.3 1.7-.8 2.2-.5.5-1.1.8-1.8.7-.5 0-1-.1-1.6-.3-.2-.1-.4-.1-.6-.2-.4-.1-.7-.1-1.1-.1-.8 0-1.6.3-2.4.8s-1.4 1.3-1.9 2.3-.7 2.3-.7 3.7v11.4c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.6-.8-1.3-.8-2.1V28.8c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v.6c.7-1.3 1.8-2.3 3.2-3 1.3-.7 2.8-1 4.3-1z" fill-rule="evenodd" clip-rule="evenodd" fill="#4a4a4a"/>
|
4
|
+
</svg>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "open-uri"
|
2
|
+
class AttachAvatarToUserJob < ApplicationJob
|
3
|
+
queue_as :default
|
4
|
+
|
5
|
+
def perform(**args)
|
6
|
+
user = args[:user]
|
7
|
+
size = 512
|
8
|
+
colors = %w[ 066fd1 ]
|
9
|
+
|
10
|
+
unless user.nil?
|
11
|
+
seed = URI.encode_uri_component user.username
|
12
|
+
url = "https://api.dicebear.com/9.x/initials/png?size=#{size}&seed=#{seed}&backgroundColor=#{colors.sample}&scale=120"
|
13
|
+
|
14
|
+
begin
|
15
|
+
user.avatar.attach(io: OpenURI.open_uri(url), filename: "#{user.id}_avatar.png") unless user.avatar.attached?
|
16
|
+
rescue
|
17
|
+
AttachAvatarToUserJob.set(wait: 1.minutes).perform_later(user: user)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import { Controller } from "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/+esm";
|
2
|
+
|
3
|
+
// data-controller = "editorjs"
|
4
|
+
Stimulus.register("editorjs", class extends Controller {
|
5
|
+
connect() {
|
6
|
+
const initialData = this.getinitialData();
|
7
|
+
|
8
|
+
this.editorJS = new EditorJS({
|
9
|
+
data: initialData,
|
10
|
+
holder: document.getElementById("editorjs_content"),
|
11
|
+
tools: {
|
12
|
+
image: {
|
13
|
+
class: ImageTool,
|
14
|
+
config: {
|
15
|
+
uploader: {
|
16
|
+
uploadByFile(file) {
|
17
|
+
return getBase64(file, function (e) { }).then((data) => {
|
18
|
+
return {
|
19
|
+
success: 1,
|
20
|
+
file: {
|
21
|
+
url: data
|
22
|
+
}
|
23
|
+
}
|
24
|
+
})
|
25
|
+
}
|
26
|
+
}
|
27
|
+
},
|
28
|
+
},
|
29
|
+
header: {
|
30
|
+
class: Header,
|
31
|
+
},
|
32
|
+
quote: {
|
33
|
+
class: Quote,
|
34
|
+
},
|
35
|
+
list: {
|
36
|
+
class: EditorjsList,
|
37
|
+
inlineToolbar: true,
|
38
|
+
toolbox: [
|
39
|
+
{
|
40
|
+
data: {
|
41
|
+
style: 'unordered',
|
42
|
+
}
|
43
|
+
},
|
44
|
+
{
|
45
|
+
data: {
|
46
|
+
style: 'ordered',
|
47
|
+
}
|
48
|
+
}
|
49
|
+
]
|
50
|
+
},
|
51
|
+
paragraph: {
|
52
|
+
class: Paragraph,
|
53
|
+
config: {
|
54
|
+
inlineToolbar: true,
|
55
|
+
},
|
56
|
+
},
|
57
|
+
code: CodeTool,
|
58
|
+
delimiter: Delimiter,
|
59
|
+
},
|
60
|
+
});
|
61
|
+
|
62
|
+
this.element.addEventListener("submit", this.saveEditorData.bind(this));
|
63
|
+
}
|
64
|
+
csrfToken() {
|
65
|
+
const metaTag = document.querySelector("meta[name='csrf-token']");
|
66
|
+
|
67
|
+
return metaTag ? metaTag.content : "";
|
68
|
+
}
|
69
|
+
|
70
|
+
getinitialData() {
|
71
|
+
const hiddenContentField = document.getElementById(
|
72
|
+
"editorjs_content_hidden"
|
73
|
+
);
|
74
|
+
if (hiddenContentField && hiddenContentField.value) {
|
75
|
+
return JSON.parse(hiddenContentField.value);
|
76
|
+
}
|
77
|
+
return {};
|
78
|
+
}
|
79
|
+
|
80
|
+
async saveEditorData(event) {
|
81
|
+
event.preventDefault();
|
82
|
+
|
83
|
+
const outputData = await this.editorJS.save();
|
84
|
+
const postForm = this.element;
|
85
|
+
|
86
|
+
const hiddenInput = document.getElementById("editorjs_content_hidden");
|
87
|
+
|
88
|
+
hiddenInput.value = JSON.stringify(outputData);
|
89
|
+
postForm.submit();
|
90
|
+
};
|
91
|
+
});
|
92
|
+
|
93
|
+
function getBase64(file, onLoadCallback) {
|
94
|
+
return new Promise(function (resolve, reject) {
|
95
|
+
var reader = new FileReader();
|
96
|
+
reader.onload = function () { return resolve(reader.result); };
|
97
|
+
reader.onerror = reject;
|
98
|
+
reader.readAsDataURL(file);
|
99
|
+
});
|
100
|
+
};
|
101
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import { Controller } from "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/+esm"
|
2
|
+
|
3
|
+
Stimulus.register("theme-switcher", class extends Controller {
|
4
|
+
connect() {
|
5
|
+
// Auto-switcher part
|
6
|
+
|
7
|
+
const getStoredTheme = () => localStorage.getItem('theme');
|
8
|
+
|
9
|
+
function getPreferredTheme() {
|
10
|
+
const storedTheme = getStoredTheme();
|
11
|
+
|
12
|
+
if (storedTheme === 'light') {
|
13
|
+
return 'light';
|
14
|
+
} else if (storedTheme === 'dark') {
|
15
|
+
return 'dark';
|
16
|
+
} else if (storedTheme === 'auto') {
|
17
|
+
return 'auto';
|
18
|
+
} else {
|
19
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
20
|
+
};
|
21
|
+
};
|
22
|
+
|
23
|
+
function setTheme(theme) {
|
24
|
+
if (theme !== 'light' && theme !== 'dark') {
|
25
|
+
const theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
26
|
+
document.documentElement.setAttribute('data-bs-theme', theme);
|
27
|
+
} else {
|
28
|
+
document.documentElement.setAttribute('data-bs-theme', theme);
|
29
|
+
};
|
30
|
+
};
|
31
|
+
|
32
|
+
// Switch by hand
|
33
|
+
|
34
|
+
const themeSwitcher = document.getElementsByClassName('theme-switcher')[0];
|
35
|
+
const switcherIcon = themeSwitcher.getElementsByClassName('switcher')[0];
|
36
|
+
|
37
|
+
const lightThemeIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-sun"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /></svg>';
|
38
|
+
const darkThemeIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-moon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" /></svg>';
|
39
|
+
const autoThemeIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-percentage-50"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 21a9 9 0 0 0 0 -18m0 0v18" fill="currentColor" stroke="none" /><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /></svg>';
|
40
|
+
|
41
|
+
function setSwitcherIconAndTooltip(theme) {
|
42
|
+
if (theme == 'light') {
|
43
|
+
themeSwitcher.innerHTML = lightThemeIcon;
|
44
|
+
themeSwitcher.setAttribute('data-bs-original-title', 'Switch theme to dark');
|
45
|
+
} else if (theme == 'dark') {
|
46
|
+
themeSwitcher.innerHTML = darkThemeIcon;
|
47
|
+
themeSwitcher.setAttribute('data-bs-original-title', 'Switch theme to system');
|
48
|
+
} else {
|
49
|
+
themeSwitcher.innerHTML = autoThemeIcon;
|
50
|
+
themeSwitcher.setAttribute('data-bs-original-title', 'Switch theme to light');
|
51
|
+
};
|
52
|
+
};
|
53
|
+
|
54
|
+
function switchTheme() {
|
55
|
+
let theme = getPreferredTheme()
|
56
|
+
if (theme === 'light') {
|
57
|
+
localStorage.setItem('theme', 'dark');
|
58
|
+
setTheme('dark');
|
59
|
+
setSwitcherIconAndTooltip('dark');
|
60
|
+
} else if (theme === 'dark') {
|
61
|
+
localStorage.setItem('theme', 'auto');
|
62
|
+
setTheme(getPreferredTheme());
|
63
|
+
setSwitcherIconAndTooltip('auto');
|
64
|
+
} else {
|
65
|
+
localStorage.setItem('theme', 'light');
|
66
|
+
setTheme('light');
|
67
|
+
setSwitcherIconAndTooltip('light');
|
68
|
+
};
|
69
|
+
};
|
70
|
+
|
71
|
+
// Triggers
|
72
|
+
|
73
|
+
setTheme(getPreferredTheme());
|
74
|
+
setSwitcherIconAndTooltip(getPreferredTheme());
|
75
|
+
|
76
|
+
window.addEventListener('turbo:load', () => {
|
77
|
+
setTheme(getPreferredTheme());
|
78
|
+
setSwitcherIconAndTooltip(getPreferredTheme());
|
79
|
+
});
|
80
|
+
|
81
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
82
|
+
const storedTheme = getStoredTheme();
|
83
|
+
if (storedTheme !== 'light' && storedTheme !== 'dark') {
|
84
|
+
setTheme(getPreferredTheme());
|
85
|
+
setSwitcherIconAndTooltip(getPreferredTheme());
|
86
|
+
};
|
87
|
+
});
|
88
|
+
|
89
|
+
themeSwitcher.onclick = () => {
|
90
|
+
switchTheme();
|
91
|
+
};
|
92
|
+
};
|
93
|
+
});
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
|
+
def change
|
3
|
+
create_table :users do |t|
|
4
|
+
t.string :username, null: false
|
5
|
+
t.string :email_address, null: false
|
6
|
+
t.string :password_digest, null: false
|
7
|
+
t.boolean :is_admin, default: false
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
|
12
|
+
add_index :users, :email_address, unique: true
|
13
|
+
add_index :users, :username, unique: true
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class User < ApplicationRecord
|
2
|
+
has_secure_password
|
3
|
+
has_many :sessions, dependent: :destroy
|
4
|
+
has_one_attached :avatar, service: :avatars_local_storage
|
5
|
+
after_create -> { AttachAvatarToUserJob.perform_later(user: self) }
|
6
|
+
after_update :reattach_avatar_to_user
|
7
|
+
|
8
|
+
validates :username,
|
9
|
+
length: { minimum: 1, maximum: 255 },
|
10
|
+
format: { with: /\A([A-Za-z0-9\-\_\.\s]+)+\z/ },
|
11
|
+
uniqueness: true
|
12
|
+
|
13
|
+
validates :email_address,
|
14
|
+
presence: true, length: { maximum: 255 },
|
15
|
+
format: { with: URI::MailTo::EMAIL_REGEXP },
|
16
|
+
uniqueness: true
|
17
|
+
|
18
|
+
normalizes :email_address, with: ->(e) { e.strip.downcase }
|
19
|
+
|
20
|
+
def self.ransackable_attributes(auth_object = nil)
|
21
|
+
%w[email_address created_at is_admin]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def reattach_avatar_to_user
|
27
|
+
if saved_change_to_attribute?(:username)
|
28
|
+
self.avatar.purge
|
29
|
+
AttachAvatarToUserJob.perform_now(user: self)
|
30
|
+
self.avatar.purge
|
31
|
+
AttachAvatarToUserJob.perform_later(user: self)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
class EJSParser
|
2
|
+
# EJSParser::build_html_from_ejs_content(content: @post.content, classes: { header: "foo", paragraph: "bar" })
|
3
|
+
# Classes are optional, method applies @default_classes if none are passed as an arg.
|
4
|
+
# "Otherwise, the classes passed as an argument will be used for the specified elements. For example,
|
5
|
+
# classes: { header: "foo" } will apply the 'foo' class to the 'header' element, while the remaining elements will
|
6
|
+
# still use the classes defined in @default_classes."
|
7
|
+
|
8
|
+
def self.build_html_from_ejs_content(**args)
|
9
|
+
begin
|
10
|
+
content_blocks = JSON.parse(args[:content])["blocks"]
|
11
|
+
rescue
|
12
|
+
return "Unable to show content."
|
13
|
+
end
|
14
|
+
|
15
|
+
@classes = args[:classes] || {}
|
16
|
+
|
17
|
+
html_content = ""
|
18
|
+
|
19
|
+
content_blocks.each do |block|
|
20
|
+
type = block["type"]
|
21
|
+
data = block["data"]
|
22
|
+
|
23
|
+
@classes[type.to_sym].nil? ? @css_classes = @default_classes[type.to_sym] : @css_classes = @classes[type.to_sym]
|
24
|
+
|
25
|
+
begin
|
26
|
+
parsed_data = eval("parse_#{type}_block(#{data}, \"#{@css_classes}\")")
|
27
|
+
rescue
|
28
|
+
parsed_data = ""
|
29
|
+
end
|
30
|
+
|
31
|
+
html_content += parsed_data
|
32
|
+
end
|
33
|
+
|
34
|
+
html_content.html_safe
|
35
|
+
end
|
36
|
+
|
37
|
+
# EJSParser::summary_for_ejs_content(content: @post.content, truncate: 500)
|
38
|
+
# Truncate is optional.
|
39
|
+
def self.summary_for_ejs_content(**args)
|
40
|
+
begin
|
41
|
+
first_paragraph = JSON.parse(args[:content])["blocks"].find {|block| block["type"] == "paragraph"}
|
42
|
+
brief = first_paragraph["data"]["text"]
|
43
|
+
truncate = args[:truncate]
|
44
|
+
truncate.nil? ? brief : brief.truncate(truncate)
|
45
|
+
rescue
|
46
|
+
return "Unable to show content."
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# E.g.: { "text"=>"Header", "level"=>2 }
|
51
|
+
def self.parse_header_block(*args)
|
52
|
+
data = args[0]
|
53
|
+
text, level = data["text"], data["level"]
|
54
|
+
# For Bulma CSS.
|
55
|
+
classes = "is-size-#{level} " + args[1] || "is-size-#{level}"
|
56
|
+
|
57
|
+
"<h#{level} class=\"#{classes}\">#{text}</h#{level}>"
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.parse_paragraph_block(*args)
|
61
|
+
text = args[0]["text"]
|
62
|
+
classes = args[1]
|
63
|
+
"<p class = \"#{classes}\">" + text + "</p>"
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.parse_image_block(*args)
|
67
|
+
data, classes = args[0], args[1]
|
68
|
+
caption, url = data["caption"], data["file"]["url"]
|
69
|
+
|
70
|
+
# The following image parameters from EditorJS are currently ignored:
|
71
|
+
# bordered, stretched, with_background = data["withBorder"], data["stretched"], data["withBackground"]
|
72
|
+
|
73
|
+
"<figure class=\"image #{classes}\">" + "<img src=#{url} loading=\"lazy\">" + "</figure>"
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.parse_quote_block(*args)
|
77
|
+
data, classes = args[0], args[1]
|
78
|
+
text, caption, alignment = data["text"], data["caption"], data["alignment"]
|
79
|
+
classes += " has-text-centered" if alignment == "center" # Bulma CSS
|
80
|
+
|
81
|
+
"<blockquote class=\"#{classes}\">" + text +
|
82
|
+
"<p class=\"is-size-7 is-italic pt-3 has-text-grey\">— #{caption}</p>" +
|
83
|
+
"</blockquote>"
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.parse_list_block(*args)
|
87
|
+
data, classes = args[0], args[1]
|
88
|
+
list_items = data["items"]
|
89
|
+
list_items.map! { "<li>" + it["content"] + "</li>" }
|
90
|
+
list_content = "" ; list_items.each { list_content += it }
|
91
|
+
|
92
|
+
if data["style"] == "ordered"
|
93
|
+
"<ol class=\"#{classes}\">" + list_content + "</ol>"
|
94
|
+
else
|
95
|
+
"<ul class=\"#{classes}\">" + list_content + "</ul>"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.parse_delimiter_block(*args)
|
100
|
+
classes = args[1]
|
101
|
+
"<hr class=\"#{classes}\">"
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.parse_code_block(*args)
|
105
|
+
data, classes = args[0], args[1]
|
106
|
+
escaped_code = CGI.escapeHTML(data["code"])
|
107
|
+
"<pre><code class=\"#{classes}\">#{escaped_code}</code></pre>"
|
108
|
+
end
|
109
|
+
|
110
|
+
@default_classes = {
|
111
|
+
header: "",
|
112
|
+
paragraph: "",
|
113
|
+
image: "",
|
114
|
+
quote: "",
|
115
|
+
list: "",
|
116
|
+
delimiter: "",
|
117
|
+
code: ""
|
118
|
+
}
|
119
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
User.create(username: "admin",
|
2
|
+
email_address: "admin@example.com",
|
3
|
+
password: "Password1234",
|
4
|
+
password_confirmation: "Password1234",
|
5
|
+
is_admin: true)
|
6
|
+
|
7
|
+
User.create(username: "guest",
|
8
|
+
email_address: "guest@example.com",
|
9
|
+
password: "Password1234",
|
10
|
+
password_confirmation: "Password1234")
|
11
|
+
|
12
|
+
User.all.each { |user| AttachAvatarToUserJob.perform_now(user: user) }
|
data/lib/generators/admin/install/templates/test_unit/controllers/admin/profile_controller_test.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class Admin::ProfileControllerTest < ActionDispatch::IntegrationTest
|
4
|
+
def setup
|
5
|
+
sign_in_as_admin_user users(:admin)
|
6
|
+
end
|
7
|
+
|
8
|
+
test "should get show" do
|
9
|
+
get admin_profile_path
|
10
|
+
assert_response :success
|
11
|
+
end
|
12
|
+
|
13
|
+
test "should get edit" do
|
14
|
+
get admin_edit_profile_path
|
15
|
+
assert_response :success
|
16
|
+
end
|
17
|
+
|
18
|
+
test "should update profile" do
|
19
|
+
@user = users(:admin)
|
20
|
+
patch admin_profile_path params: { user: { username: "administrator" } }
|
21
|
+
@user.reload
|
22
|
+
|
23
|
+
assert @user.username == "administrator"
|
24
|
+
assert_redirected_to admin_profile_path
|
25
|
+
end
|
26
|
+
end
|
data/lib/generators/admin/install/templates/test_unit/controllers/admin/sessions_controller_test.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class Admin::SessionsControllerTest < ActionDispatch::IntegrationTest
|
4
|
+
setup do
|
5
|
+
@user = users(:admin)
|
6
|
+
end
|
7
|
+
|
8
|
+
test "should get new" do
|
9
|
+
get admin_sign_in_url
|
10
|
+
assert_response :success
|
11
|
+
end
|
12
|
+
|
13
|
+
test "should sign in" do
|
14
|
+
post admin_sign_in_url, params: { email_address: @user.email_address, password: "Password1234" }
|
15
|
+
assert_redirected_to admin_root_url
|
16
|
+
|
17
|
+
get admin_root_url
|
18
|
+
assert_response :success
|
19
|
+
end
|
20
|
+
|
21
|
+
test "should not sign in with wrong credentials" do
|
22
|
+
post admin_sign_in_url, params: { email_address: @user.email_address, password: "Incorect4321" }
|
23
|
+
assert_redirected_to admin_sign_in_url
|
24
|
+
assert_equal "Try another email address or password.", flash[:alert]
|
25
|
+
|
26
|
+
get admin_root_url
|
27
|
+
assert_redirected_to admin_sign_in_url
|
28
|
+
end
|
29
|
+
|
30
|
+
test "should sign out" do
|
31
|
+
sign_in_as_admin_user users(:admin)
|
32
|
+
|
33
|
+
post admin_sign_out_url
|
34
|
+
assert_redirected_to admin_sign_in_url
|
35
|
+
end
|
36
|
+
end
|
data/lib/generators/admin/install/templates/test_unit/controllers/admin/users_controller_test.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class Admin::UsersControllerTest < ActionDispatch::IntegrationTest
|
4
|
+
setup do
|
5
|
+
@user = sign_in_as_admin_user users(:admin)
|
6
|
+
end
|
7
|
+
|
8
|
+
test "should get index" do
|
9
|
+
get admin_users_url
|
10
|
+
assert_response :success
|
11
|
+
end
|
12
|
+
|
13
|
+
test "should get new" do
|
14
|
+
get new_admin_user_url
|
15
|
+
assert_response :success
|
16
|
+
end
|
17
|
+
|
18
|
+
test "should create user" do
|
19
|
+
assert_difference("User.all.count") do
|
20
|
+
post admin_users_url, params: { user: {
|
21
|
+
username: "Jim",
|
22
|
+
email_address: "jimdoe@example.com",
|
23
|
+
password: "Password1234",
|
24
|
+
password_confirmation: "Password1234" } }
|
25
|
+
end
|
26
|
+
|
27
|
+
assert_redirected_to admin_user_url(User.last)
|
28
|
+
end
|
29
|
+
|
30
|
+
test "should show admin_user" do
|
31
|
+
get admin_user_url(@user)
|
32
|
+
assert_response :success
|
33
|
+
end
|
34
|
+
|
35
|
+
test "should get edit" do
|
36
|
+
get edit_admin_user_url(@user)
|
37
|
+
assert_response :success
|
38
|
+
end
|
39
|
+
|
40
|
+
test "should update admin_user" do
|
41
|
+
patch admin_user_url(@user), params: { user: { username: "James" } }
|
42
|
+
@user.reload
|
43
|
+
|
44
|
+
assert_redirected_to admin_user_url(@user)
|
45
|
+
assert @user.username == "James"
|
46
|
+
end
|
47
|
+
|
48
|
+
test "should destroy admin_user" do
|
49
|
+
assert_difference("User.count", -1) do
|
50
|
+
delete admin_user_url(@user)
|
51
|
+
end
|
52
|
+
|
53
|
+
assert_redirected_to admin_users_url
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class UserTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@user = users(:admin)
|
6
|
+
end
|
7
|
+
|
8
|
+
test "valid usernames" do
|
9
|
+
usernames = %w[ John Gorlock_The_Destroyer Stalin-3000 Max.Pain ]
|
10
|
+
usernames.each do |u|
|
11
|
+
@user.username = u
|
12
|
+
assert @user.valid?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
test "invalid usernames" do
|
17
|
+
usernames = %w[ 松本行弘 $uperM@n_2025 Поручик_Ржевский ]
|
18
|
+
usernames.append ("a" * 256)
|
19
|
+
usernames.each do |u|
|
20
|
+
@user.username = u
|
21
|
+
assert_not @user.valid?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
test "username should be uniq" do
|
26
|
+
@user.username = "Jane Doe"
|
27
|
+
assert_not @user.valid?
|
28
|
+
end
|
29
|
+
|
30
|
+
test "email should be uniq" do
|
31
|
+
@user.email_address = "guest@example.com"
|
32
|
+
assert_not @user.valid?
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
ENV["RAILS_ENV"] ||= "test"
|
2
|
+
require_relative "../config/environment"
|
3
|
+
require "rails/test_help"
|
4
|
+
|
5
|
+
class ActiveSupport::TestCase
|
6
|
+
parallelize(workers: :number_of_processors)
|
7
|
+
|
8
|
+
fixtures :all
|
9
|
+
|
10
|
+
def sign_in_as_admin_user(user)
|
11
|
+
post admin_sign_in_url, params: {
|
12
|
+
email_address: user.email_address,
|
13
|
+
password: "Password1234"
|
14
|
+
}
|
15
|
+
user
|
16
|
+
end
|
17
|
+
|
18
|
+
def sign_in_as(user)
|
19
|
+
post session_path, params: {
|
20
|
+
email_address: user.email_address,
|
21
|
+
password: "Password1234"
|
22
|
+
}
|
23
|
+
user
|
24
|
+
end
|
25
|
+
end
|