editus 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3cec41e8e70a12bb3dd860f31729437bad4870ff201eb3a4892be778fdcf8558
4
+ data.tar.gz: 76b985c2519cb34198be4ce940128f1dd6265347e3ea48ec1cb1ceb5335b6028
5
+ SHA512:
6
+ metadata.gz: e705129944f1362d82e5dc9d749547ffd4ec0d59a4d315a0b90149f843f073f4dde0522ff3ec997b7d45c29b23780df1e4a56e94ac7a239b16e087c776e1a5c4
7
+ data.tar.gz: 4b7d829b9c4df393a913f9b05eaebfa149c6953189ad413204e47e358409a8d1e35dfbbb0398d566686e31461729b64185e0d12a5566fe023d78733ea2fa14a8
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 hungkieu
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Editus
2
+ Simplify code execution and database editing. Intuitive web interface. Run code snippets, modify databases in real-time.
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem "editus", git: "https://github.com/muoihai-com/editus.git"
9
+ ```
10
+
11
+ And then execute:
12
+ ```bash
13
+ $ bundle install
14
+ ```
15
+
16
+ Run the following code to create the config file:
17
+
18
+ ```bash
19
+ rake editus:install # It will create file config/initializer/editus.rb
20
+ ```
21
+
22
+ Add the following to your `config/routes.rb`:
23
+
24
+ ```ruby
25
+ Rails.application.routes.draw do
26
+ mount Editus::Engine => "/updater" unless Rails.env.production?
27
+ ...
28
+ end
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### Authentication
34
+
35
+ Editus supports two forms of authentication:
36
+
37
+ 1. Define the `editus_account` method in `ApplicationHelper`: This method should return the current account (e.g., `current_user` or `current_admin`). If this method is defined, Editus will use it to determine the current account.
38
+
39
+ ```ruby
40
+ module ApplicationHelper
41
+ def editus_account
42
+ current_user # or any other method that returns the current account
43
+ end
44
+ end
45
+ ```
46
+
47
+ 2. Configuration in the config/initializer/editus.rb file: Create an array containing accounts authenticated via HTTP Basic Authentication. Each account is a sub-array with two elements: the username and password.
48
+
49
+ ```ruby
50
+ config.auth = [%w[user@example.com Pass@123456], %w[manager@example.com Pass@123456]]
51
+ ```
52
+
53
+ Use one of the above authentication methods to secure access to Editus. Note that the `editus_account` method will take precedence if both methods are provided.
54
+
55
+ ### Models
56
+
57
+ Display a simple form interface that helps you update the fields of the selected model. The update will use `update_columns` so will ignore callback and validate
58
+
59
+ ### Add Script
60
+
61
+ To execute existing code create a directory `config/editus` in your code
62
+
63
+ Example:
64
+ `config/editus/update_nick_name_user.rb`
65
+
66
+ ```rb
67
+ Editus::Script.define :update_nick_name_user do
68
+ title "Update nick_name of user"
69
+ task :up do
70
+ user = User.find(1)
71
+ nick_name = user.nick_name
72
+ user.update_columns nick_name: "xatara"
73
+
74
+ [1, nick_name]
75
+ end
76
+
77
+ task :down do |id, nick_name|
78
+ User.find(id).update_columns(nick_name: nick_name)
79
+ end
80
+ end
81
+
82
+ ```
83
+
84
+ Make sure the filename and the defined name are the same. In the above code `title` to set the title of the code.
85
+ `task :up` is the code that will be executed when you run it.
86
+ `task :down` is the code that will be executed when you undo, if you don't need to undo you can skip it.
87
+
88
+ It can use the result returned from the `up` function to use as an input parameter
89
+
90
+ ## Contributing
91
+ Contribution directions go here.
92
+
93
+ ## License
94
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
9
+
10
+ require "rake/testtask"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = false
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,51 @@
1
+ module Editus
2
+ class ApplicationController < ActionController::Base
3
+ prepend ::ApplicationHelper
4
+
5
+ before_action :authenticate!, :set_locale
6
+
7
+ rescue_from "StandardError" do |exception|
8
+ @exception = exception
9
+
10
+ render "editus/supports/bug"
11
+ end
12
+
13
+ def authenticate!
14
+ return if editus_auth_account.present?
15
+ return head 401 if editus_account_needed?
16
+ return if authenticate(Editus.configuration.auth)
17
+
18
+ request_http_basic_authentication
19
+ end
20
+
21
+ def authenticate auth_configs
22
+ return true if auth_configs.blank?
23
+
24
+ authenticate_with_http_basic do |u, p|
25
+ session[:basic_auth_account] = u if auth_configs.include?([u, p])
26
+ end
27
+ end
28
+
29
+ def set_locale
30
+ I18n.locale = :en
31
+ end
32
+
33
+ private
34
+
35
+ def editus_account_needed?
36
+ ::ApplicationHelper.method_defined?(:editus_account)
37
+ end
38
+
39
+ def editus_auth_account
40
+ if editus_account_needed?
41
+ editus_account if editus_account.is_a?(ActiveRecord::Base)
42
+ else
43
+ session[:basic_auth_account]
44
+ end
45
+ end
46
+
47
+ def request_account
48
+ editus_auth_account.try(:email) || session[:basic_auth_account] || request.remote_ip
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,121 @@
1
+ require_dependency "editus/application_controller"
2
+
3
+ module Editus
4
+ class HomeController < ApplicationController
5
+ before_action :load_model, :load_record, :load_column
6
+
7
+ def index
8
+ @scripts = Editus::Script.all.values
9
+ @actions = Editus::Actions.all.reverse
10
+ end
11
+
12
+ def validate
13
+ @record.assign_attributes user_params
14
+ @changes = @record.changes.transform_values do |(from, to)|
15
+ "From #{from.nil? ? 'null' : from} to #{to}"
16
+ end
17
+ @validate_error_flag = !@record.valid?
18
+ @full_messages = @record.errors.full_messages
19
+
20
+ render "validate"
21
+ end
22
+
23
+ def update
24
+ @record.assign_attributes user_params
25
+ return redirect_to(root_path) unless @record.changed?
26
+
27
+ action = Editus::Action.new
28
+ action.changes = @record.changes
29
+ action.type = "models"
30
+ action.user = request_account
31
+ action.model_id = @record.id
32
+ action.model_name = @record.klass.class.name
33
+ action.save
34
+ @record.update_columns user_params
35
+
36
+ redirect_to root_path
37
+ end
38
+
39
+ def manual_update
40
+ @model_names = Editus::Client.models
41
+ end
42
+
43
+ def scripts
44
+ @scripts = Editus::Script.all.values
45
+ end
46
+
47
+ def run_script
48
+ script = Editus::Script.all[params[:id].to_sym]
49
+ return redirect_to(root_path) if script.blank?
50
+
51
+ action = Editus::Action.new
52
+ action.type = "scripts"
53
+ action.user = request_account
54
+ action.model_id = script.name
55
+ action.model_name = script.title
56
+ action.changes = script.proxy.up if script.proxy.respond_to?(:up)
57
+ action.save
58
+
59
+ redirect_to root_path
60
+ end
61
+
62
+ def undo
63
+ action = Editus::Actions.all.find{|act| act.id == params[:id]}
64
+ return redirect_to(root_path) if action.blank?
65
+
66
+ new_action = Editus::Action.new
67
+ new_action.type = "undo"
68
+ new_action.user = request_account
69
+ new_action.model_id = action.id
70
+ new_action.model_name = action.model_name
71
+ case action.type
72
+ when "models"
73
+ pr = action.changes.transform_values{|(from, _to)| from}
74
+ record = action.model_name.constantize.find(action.model_id)
75
+ record.assign_attributes pr
76
+ new_action.changes = record.changes
77
+ record.update_columns pr
78
+ when "scripts"
79
+ script = Editus::Script.all[action.model_id.to_sym]
80
+ return redirect_to(root_path) if action.blank?
81
+
82
+ if script.proxy.respond_to?(:down)
83
+ action.changes.empty? ? script.proxy.down : script.proxy.down(*action.changes)
84
+ end
85
+ end
86
+ new_action.save
87
+
88
+ redirect_to root_path
89
+ end
90
+
91
+ private
92
+
93
+ def load_model
94
+ return if params[:klass].blank?
95
+
96
+ @proxy = Editus::Client.model(params[:klass])
97
+ return if @proxy.blank?
98
+
99
+ @column_names = @proxy.column_names
100
+ rescue NameError
101
+ redirect_to manual_update_path
102
+ end
103
+
104
+ def load_record
105
+ return if @proxy.blank? || params[:attribute].blank? || params[:value].blank?
106
+
107
+ @record = @proxy.find_by(params[:attribute] => params[:value])
108
+ end
109
+
110
+ def load_column
111
+ return if params[:column].blank? || @record.try(params[:column]).blank?
112
+
113
+ @update_value = @record.try(params[:column])
114
+ end
115
+
116
+ def user_params
117
+ params[:user]&.each{|key, _| params[:user].delete(key) if params[:user][key].blank?}
118
+ (params[:user] || {}).as_json
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,4 @@
1
+ module Editus
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,18 @@
1
+ module Editus
2
+ module HomeHelper
3
+ def path_active? path
4
+ request.original_url.include?(path)
5
+ end
6
+
7
+ def field_tag record, col, *args
8
+ if record.try(col).is_a?(Time)
9
+ text_field_tag(*args, placeholder: "YYYY-MM-DD HH:mm:ss UTC")
10
+ elsif [true, false].include? record.try(col)
11
+ safe_join [hidden_field_tag(args.first, "false", id: nil),
12
+ check_box_tag(args.first, "true", record.try(col))]
13
+ else
14
+ text_field_tag(*args)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ module Editus
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,79 @@
1
+ <div class="ui main text container">
2
+ <h1 class="ui header">Scripts</h1>
3
+
4
+ <% if @scripts.blank? %>
5
+ <div class="ui placeholder segment">
6
+ <div class="ui icon header">
7
+ <i class="file code outline icon"></i>
8
+ No scripts are listed.
9
+ </div>
10
+ <%= link_to 'https://github.com/muoihai-com/editus#add-script', target: "_blank" do %>
11
+ <div class="ui primary button">
12
+ Tutorial - Add Script
13
+ </div>
14
+ <% end %>
15
+ </div>
16
+ <% else %>
17
+ <div class="ui relaxed divided list">
18
+ <% @scripts.each do |script| %>
19
+ <div class="item">
20
+ <div class="content">
21
+ <%= link_to "#{scripts_path}##{script.name}", class: "header" do %>
22
+ <div class="ui horizontal label">#<%= script.name %></div>
23
+ <%= script.title %>
24
+ <% end %>
25
+ </div>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+ <% end %>
30
+
31
+ <h1 class="ui header">Histories</h1>
32
+ <div class="ui cards flex-column">
33
+ <% if @actions.blank? %>
34
+ <div class="ui placeholder segment">
35
+ <div class="ui icon header">
36
+ <i class="calendar alternate outline icon"></i>
37
+ No action has been taken yet
38
+ </div>
39
+ </div>
40
+ <% else %>
41
+ <% @actions.each do |action| %>
42
+ <div class="ui card w-full" id="action-<%= action.id %>">
43
+ <div class="content">
44
+ <div class="header"><%= action.user %></div>
45
+ <% if action.type == "models" %>
46
+ <div class="meta">updated <b><%= action.model_name %>#<%= action.model_id %></b> <%= time_ago_in_words(action.created_at) %> ago</div>
47
+ <% elsif action.type == "scripts"%>
48
+ <div class="meta">used script <b><%= link_to action.model_name, "#{scripts_path}##{action.model_id}" %></b> <%= time_ago_in_words(action.created_at) %> ago</div>
49
+ <% elsif action.type == "undo"%>
50
+ <div class="meta">undo action <b><%= link_to action.model_id, "##{action.model_id}", class: "action" %></b> <%= time_ago_in_words(action.created_at) %> ago</div>
51
+ <% end %>
52
+ <div class="description">
53
+ <% if (action.type == "models" || action.type == "undo") && action.changes %>
54
+ <pre class="bg-neutral-300 rounded p-4">
55
+ <%= action.changes.transform_values{|(from, to)| "From #{from.nil? ? 'null' : from} to #{to}"} %>
56
+ </pre>
57
+ <% elsif action.type == "scripts" && action.changes %>
58
+ <pre class="bg-neutral-300 rounded p-4">
59
+ <%= action.changes %>
60
+ </pre>
61
+ <% end %>
62
+ </div>
63
+ </div>
64
+ <% if action.type != "undo" %>
65
+ <div class="extra content">
66
+ <button class="ui labeled icon button red confirmation" data-method="undo" data-id="<%= action.id %>">
67
+ <i class="undo icon"></i>
68
+ Undo
69
+ </button>
70
+
71
+ <%= form_tag undo_path(id: action.id), method: :post, id: action.id do %>
72
+ <% end %>
73
+ </div>
74
+ <% end %>
75
+ </div>
76
+ <% end %>
77
+ <% end %>
78
+ </div>
79
+ </div>
@@ -0,0 +1,91 @@
1
+ <div class="ui main text container">
2
+ <h1 class="ui header">Models</h1>
3
+ </div>
4
+
5
+ <%= form_tag validate_path, class: 'ui form warning' do %>
6
+ <div class="ui mt-4 text container">
7
+ <p>
8
+ <%= select_tag "klass", options_for_select(@model_names.sort, params[:klass]), prompt: "Select a model", id: 'select-klass' %>
9
+ </p>
10
+
11
+ <% if params[:klass].present? %>
12
+ <p>
13
+ find by <%= text_field_tag "attribute", params[:attribute] || 'id', placeholder: "Id, Email,..." %>
14
+ </p>
15
+ <p>
16
+ with value <%= text_field_tag "value", params[:value], placeholder: "value..." %>
17
+ </p>
18
+ <p>
19
+ <button class="ui button blue" id="find">Find</button>
20
+ </p>
21
+ <% end %>
22
+ </div>
23
+ <% if @record.present? %>
24
+ <div class="ui mt-4 px-4 container">
25
+ <table class="ui celled table">
26
+ <thead>
27
+ <tr>
28
+ <th>Attribute</th>
29
+ <th>Value</th>
30
+ <th>Update</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ <% @column_names.each do |col| %>
35
+ <tr>
36
+ <td><%= col %></td>
37
+ <td class="w-3/5"><%= @record.try(col) %></td>
38
+ <td>
39
+ <% if [true, false].include? @record.try(col) %>
40
+ <%= label_tag "user[#{col}]", class: "flex items-center" do %>
41
+ <%= field_tag @record, col, "user[#{col}]", nil %><span class="ml-2">True</span>
42
+ <% end %>
43
+ <% else %>
44
+ <%= field_tag @record, col, "user[#{col}]", nil %>
45
+ <% end %>
46
+ </td>
47
+ </tr>
48
+ <% end %>
49
+ </tbody>
50
+ </table>
51
+ </div>
52
+
53
+ <div class="ui mt-4 mb-4 text container">
54
+ <%= submit_tag "Validate", class: "ui button blue" %>
55
+ </div>
56
+
57
+ <% elsif params[:klass] && params[:attribute] && params[:value] %>
58
+ <div class="ui mt-4 text container">
59
+ <div class="ui warning message">
60
+ <i class="warning icon"></i>
61
+ Record Not Found
62
+ </div>
63
+ </div>
64
+ <% end %>
65
+ <% end %>
66
+
67
+ <script>
68
+ function selectClassChange(e) {
69
+ const value = e.target.value
70
+
71
+ window.location.href = `${window.location.origin}${window.location.pathname}?klass=${value}`
72
+ }
73
+
74
+ function findBy(e) {
75
+ e.preventDefault()
76
+
77
+ const attribute = document.getElementById('attribute').value
78
+ const value = document.getElementById('value').value
79
+
80
+ const search = new URLSearchParams()
81
+ search.set('klass', '<%= params[:klass] %>')
82
+ search.set('attribute', attribute)
83
+ search.set('value', value)
84
+ window.location.href = `${window.location.origin}${window.location.pathname}?${search}`
85
+ }
86
+
87
+ document.addEventListener('DOMContentLoaded', function(event) {
88
+ document.getElementById('select-klass').addEventListener('change', selectClassChange)
89
+ document.getElementById('find').addEventListener('click', findBy)
90
+ });
91
+ </script>
@@ -0,0 +1,50 @@
1
+ <div class="ui main text container">
2
+ <h1 class="ui header">Scripts</h1>
3
+
4
+ <% if @scripts.blank? %>
5
+ <div class="ui placeholder segment">
6
+ <div class="ui icon header">
7
+ <i class="file code outline icon"></i>
8
+ No scripts are listed.
9
+ </div>
10
+ <%= link_to 'https://github.com/muoihai-com/editus#add-script', target: "_blank" do %>
11
+ <div class="ui primary button">
12
+ Tutorial - Add Script
13
+ </div>
14
+ <% end %>
15
+ </div>
16
+ <% end %>
17
+
18
+ <% @scripts.each do |script| %>
19
+ <section id="<%= script.name %>">
20
+ <h1 class="ui header"><%= script.title %></h1>
21
+ <div>
22
+ <button class="ui labeled mini icon red button confirmation" data-method="run">
23
+ <i class="plus icon"></i>
24
+ Run
25
+ </button>
26
+
27
+ <%= form_tag run_path(id: script.name), method: :post, id: "run" do %>
28
+ <% end %>
29
+ </div>
30
+ <div class="ui accordion">
31
+ <div class="title">
32
+ <i class="dropdown icon"></i>
33
+ Up
34
+ </div>
35
+ <div class="content">
36
+ <div class="transition hidden">
37
+ <div class="editor"><%= script.up %></div>
38
+ </div>
39
+ </div>
40
+ <div class="title">
41
+ <i class="dropdown icon"></i>
42
+ Down
43
+ </div>
44
+ <div class="content">
45
+ <div class="editor"><%= script.down %></div>
46
+ </div>
47
+ </section>
48
+ <div class="ui divider"></div>
49
+ <% end %>
50
+ </div>
@@ -0,0 +1,39 @@
1
+ <div class="ui main text container">
2
+ You have made the following changes:
3
+ <pre class="bg-neutral-300 rounded p-4">
4
+ <%= @changes %>
5
+ </pre>
6
+
7
+ <div class="ui warning message">
8
+ <div class="header">
9
+ <%= "Verify that these changes are #{@validate_error_flag ? "invalid" : "valid"}." %>
10
+ </div>
11
+ <% if @full_messages.present? %>
12
+ <pre class="bg-neutral-300 rounded p-4 mx-4">
13
+ <%= @full_messages.join("\n") %>
14
+ </pre>
15
+ <% end %>
16
+
17
+ Please be aware that: These updates will skip the validation of the record and may potentially cause data errors.
18
+ </div>
19
+
20
+ <%= form_tag update_path, class: 'pure-form' do %>
21
+ <%= hidden_field_tag "klass", @record.klass.class %>
22
+ <%= hidden_field_tag "attribute", 'id' %>
23
+ <%= hidden_field_tag "value", @record.id %>
24
+ <% @column_names.each do |col| %>
25
+ <% next unless @record.try("#{col}_changed?") %>
26
+
27
+ <%= hidden_field_tag "user[#{col}]", @record.try(col) %>
28
+ <% end %>
29
+
30
+ <div class="ui buttons">
31
+ <%= link_to manual_update_path, class: "ui button" do %>
32
+ Back
33
+ <% end %>
34
+ <div class="or"></div>
35
+
36
+ <%= submit_tag "Save", class: "ui button red" %>
37
+ </div>
38
+ <% end %>
39
+ </div>
@@ -0,0 +1,18 @@
1
+ <div class="ui text main container">
2
+ <h1 class="ui center aligned icon header">
3
+ <i class="circular inverted bug icon"></i>
4
+ Server Error
5
+ </h1>
6
+
7
+ <div class="ui negative message">
8
+ <div class="header">
9
+ <%= @exception.class %> - <%= @exception.message %>
10
+ </div>
11
+ <p class="break-words">
12
+ <%= @exception.backtrace.first(10).join("\n") %>
13
+ </p>
14
+ </div>
15
+
16
+ <div style="height: 2rem;" />
17
+ </div>
18
+