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.
@@ -0,0 +1,186 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5
+
6
+ <title>Model updater</title>
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+
10
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
+ <link
13
+ href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
14
+ rel="stylesheet"
15
+ />
16
+ <link
17
+ rel="stylesheet"
18
+ href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css"
19
+ integrity="sha512-KXol4x3sVoO+8ZsWPFI/r5KBVB/ssCGB5tsv2nVOKwLg33wTFP3fmnXa47FdSVIshVTgsYk/1734xSk9aFIa4A=="
20
+ crossorigin="anonymous"
21
+ referrerpolicy="no-referrer"
22
+ />
23
+ <script
24
+ src="https://code.jquery.com/jquery-3.1.1.min.js"
25
+ integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
26
+ crossorigin="anonymous"
27
+ ></script>
28
+ <script
29
+ src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js"
30
+ integrity="sha512-Xo0Jh8MsOn72LGV8kU5LsclG7SUzJsWGhXbWcYs2MAmChkQzwiW/yTQwdJ8w6UA9C6EVG18GHb/TrYpYCjyAQw=="
31
+ crossorigin="anonymous"
32
+ referrerpolicy="no-referrer"
33
+ ></script>
34
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.23.0/ace.min.js" integrity="sha512-H7NE0Mw3ElsV/iE8dqG8hNkRkKkQ4C8l1i66QouYmeJZ0jRH/EvtMIOiIYlP6TxktZoExjy8Y+uvzTsJtwPdUQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
35
+ <style>
36
+ html,body {
37
+ overflow: auto;
38
+ }
39
+ html,
40
+ body,
41
+ .ui.text.container {
42
+ font-family: "Noto Sans", sans-serif;
43
+ }
44
+ .break-words{
45
+ overflow-wrap: break-word;
46
+ }
47
+ .main.container {
48
+ margin-top: 7em;
49
+ }
50
+ .flex{
51
+ display: flex;
52
+ }
53
+ items-center{
54
+ align-items: center;
55
+ }
56
+ .flex-column {
57
+ flex-direction: column;
58
+ }
59
+ .w-full {
60
+ width: 100% !important;
61
+ }
62
+ .w-3\/5 {
63
+ width: 60%;
64
+ }
65
+ .bg-neutral-300 {
66
+ background-color: rgb(212 212 212);
67
+ }
68
+ .rounded {
69
+ border-radius: 0.25rem;
70
+ }
71
+ .p-4 {
72
+ padding: 1rem;
73
+ }
74
+ .px-4 {
75
+ padding: 0 1rem;
76
+ }
77
+ .mb-4 {
78
+ margin-bottom: 1rem;
79
+ }
80
+ .ml-2 {
81
+ margin-left: 0.5rem;
82
+ }
83
+ .ml-4 {
84
+ margin-left: 1rem;
85
+ }
86
+ .mt-4 {
87
+ margin-top: 1rem;
88
+ }
89
+ .mx-4 {
90
+ margin-bottom: 1rem;
91
+ margin-top: 1rem;
92
+ }
93
+ .editor {
94
+ height: 420px;
95
+ }
96
+ pre {
97
+ white-space: pre-line;
98
+ }
99
+ .ui.cards>.orange.card, .ui.orange.card, .ui.orange.cards>.card {
100
+ box-shadow: 0 0 0 1px #d4d4d5, 0 0px 3px 1px #f2711c, 0 1px 3px 0 #d4d4d5;
101
+ }
102
+ .ui.cards>.orange.card:hover, .ui.orange.card:hover, .ui.orange.cards>.card:hover {
103
+ box-shadow: 0 0 0 1px #d4d4d5, 0 0px 5px 1px #f26202, 0 1px 3px 0 #bcbdbd;
104
+ }
105
+ .tablewrap {
106
+ position: relative
107
+ }
108
+ .ui.celled.table tr td, .ui.celled.table tr th {
109
+ word-break: break-all;
110
+ }
111
+ </style>
112
+ </head>
113
+ <body>
114
+ <div class="ui fixed inverted menu">
115
+ <div class="ui container">
116
+ <%= link_to 'Model Updater', root_path, class: "header item" %>
117
+ <%= link_to 'Home', root_path, class: "item" %>
118
+ <div class="ui simple dropdown item">
119
+ Menu <i class="dropdown icon"></i>
120
+ <div class="menu">
121
+ <%= link_to 'Scripts', scripts_path, class: "item" %>
122
+ <%= link_to 'Models', manual_update_path, class: "item" %>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <%= yield %>
129
+
130
+
131
+ <div class="ui modal confirmation">
132
+ <div class="header">Confirmation</div>
133
+ <div class="content">
134
+ <p>
135
+ These changes may cause data errors, do you still want to continue?
136
+ </p>
137
+ </div>
138
+ <div class="actions">
139
+ <div class="ui cancel button">Cancel</div>
140
+ <div class="ui ok red button">OK</div>
141
+ </div>
142
+ </div>
143
+
144
+ <script>
145
+ $("button.confirmation").on("click", function (e) {
146
+ $(".ui.confirmation.modal")
147
+ .modal({
148
+ onApprove: function () {
149
+ console.log($(e.currentTarget).data('method'))
150
+ switch ($(e.currentTarget).data('method')) {
151
+ case 'run':
152
+ $("#" + $(e.currentTarget).data('method')).submit()
153
+ break;
154
+ case 'undo':
155
+ console.log($(e.currentTarget).data('id'))
156
+ $("#" + $(e.currentTarget).data('id')).submit()
157
+ break;
158
+ default:
159
+ break;
160
+ }
161
+ }
162
+ })
163
+ .modal("show");
164
+ });
165
+
166
+ $('section .ui.accordion').accordion();
167
+
168
+ $('.editor').each(function(_, ele) {
169
+ var editor = ace.edit(ele);
170
+ editor.setTheme("ace/theme/monokai");
171
+ editor.session.setMode("ace/mode/ruby");
172
+ editor.setReadOnly(true);
173
+ })
174
+
175
+ if (window.location.hash) {
176
+ $('#action-' + window.location.hash.replace('#', '')).addClass('orange')
177
+ }
178
+
179
+ $('a.action').on("click", function () {
180
+ setTimeout(() => {
181
+ $('#action-' + window.location.hash.replace('#', '')).addClass('orange')
182
+ }, 1000);
183
+ })
184
+ </script>
185
+ </body>
186
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,11 @@
1
+ Editus::Engine.routes.draw do
2
+ root "home#index"
3
+
4
+ get "manual_update", to: "home#manual_update"
5
+ get "scripts", to: "home#scripts"
6
+ post "/update", to: "home#update", as: :update
7
+ post "validate", to: "home#validate", as: :validate
8
+
9
+ post "scripts/:id/run", to: "home#run_script", as: :run
10
+ post "undo/:id", to: "home#undo", as: :undo
11
+ end
@@ -0,0 +1,51 @@
1
+ module Editus
2
+ class Actions
3
+ class << self
4
+ def all
5
+ con = Editus::FileLib.read(
6
+ Rails.root.join(Editus.configuration.actions_file_path)
7
+ )
8
+ json_parse(con)
9
+ end
10
+
11
+ def create action
12
+ actions = all.push(action)
13
+ path = Rails.root.join(Editus.configuration.actions_file_path)
14
+ Editus::FileLib.write(path, actions.to_json)
15
+ end
16
+
17
+ def remove action
18
+ actions = all.push(action)
19
+ path = Rails.root.join(Editus.configuration.actions_file_path)
20
+ Editus::FileLib.write(path, actions.to_json)
21
+ end
22
+
23
+ def json_parse con
24
+ arr = JSON.parse(con)
25
+ arr.map do |haction|
26
+ Action.new(**haction.symbolize_keys)
27
+ end
28
+ rescue StandardError
29
+ []
30
+ end
31
+ end
32
+ end
33
+
34
+ class Action
35
+ attr_accessor :id, :user, :created_at, :model_id, :model_name, :changes, :type
36
+
37
+ def initialize **args
38
+ @id = args[:id] || SecureRandom.uuid
39
+ @created_at = args[:created_at] || Time.now.utc
40
+ @model_id = args[:model_id]
41
+ @model_name = args[:model_name]
42
+ @changes = args[:changes]
43
+ @type = args[:type]
44
+ @user = args[:user]
45
+ end
46
+
47
+ def save
48
+ Editus::Actions.create self
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ module Editus
2
+ class Client
3
+ class << self
4
+ def models
5
+ all_models = ::ApplicationRecord.descendants.map(&:name)
6
+ if all_models.blank?
7
+ Rails.application.eager_load!
8
+ all_models = ::ApplicationRecord.descendants.map(&:name)
9
+ end
10
+ if valid_model_names.blank?
11
+ all_models
12
+ else
13
+ all_models & valid_model_names
14
+ end
15
+ end
16
+
17
+ def model name
18
+ klass = Object.const_get(name)
19
+ if valid_model_names.present? && !valid_model_names.include?(klass.name)
20
+ raise Editus::InvalidModelError
21
+ end
22
+
23
+ Editus::Proxy.new(klass)
24
+ end
25
+
26
+ def valid_model_names
27
+ Editus::Cop.valid_model_names
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/editus/cop.rb ADDED
@@ -0,0 +1,43 @@
1
+ module Editus
2
+ class Cop
3
+ def self.instance
4
+ @instance = new
5
+ end
6
+
7
+ def self.valid_model_names
8
+ instance.valid_model_names
9
+ end
10
+
11
+ def initialize
12
+ @models = Editus.configuration.models
13
+ @mapping = @models.map{|model| get_values(model)}.compact
14
+ end
15
+
16
+ def valid_model_names
17
+ @valid_model_names ||= @mapping.map{|m| m[:name]}
18
+ end
19
+
20
+ def info model
21
+ result = if model.is_a?(String)
22
+ @mapping.find{|x| x[:name] == model}
23
+ elsif model.respond_to?(:name)
24
+ @mapping.find{|x| x[:name] == model.name}
25
+ end
26
+
27
+ result || {}
28
+ end
29
+
30
+ private
31
+
32
+ def get_values model
33
+ return nil if model.blank?
34
+ return nil unless model.is_a?(String) || model.is_a?(Hash)
35
+
36
+ name = model.is_a?(String) ? model : model["name"]
37
+ fields = model.is_a?(Hash) ? model["fields"] : []
38
+ exclude_fields = model.is_a?(Hash) ? model["exclude_fields"] : []
39
+
40
+ {name: name, fields: fields, exclude_fields: exclude_fields}
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,7 @@
1
+ module Editus
2
+ class DefinitionProxy
3
+ def initialize name
4
+ @name = name
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ module Editus
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Editus
4
+
5
+ config.generators do |g|
6
+ g.test_framework :rspec
7
+ end
8
+
9
+ config.after_initialize do
10
+ Editus.load_file_config
11
+ Editus.find_definitions
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ module Editus
2
+ class FileLib
3
+ class << self
4
+ def instance
5
+ @instance ||= new
6
+ end
7
+
8
+ def read path
9
+ instance.create_file_if_not_exists(path)
10
+ end
11
+
12
+ def write path, content
13
+ instance.write_file_if_not_exists path, content
14
+ end
15
+ end
16
+
17
+ def write_file_if_not_exists path, content
18
+ if File.exist?(path)
19
+ File.write(path, content)
20
+ else
21
+ FileUtils.mkdir_p(File.dirname(path))
22
+ File.open(path, "w") do |file|
23
+ file.puts content
24
+ end
25
+ end
26
+ end
27
+
28
+ def create_file_if_not_exists path
29
+ if File.exist?(path)
30
+ File.read(path)
31
+ else
32
+ data = JSON.generate([])
33
+ FileUtils.mkdir_p(File.dirname(path))
34
+ File.open(path, "w") do |file|
35
+ file.puts data
36
+ end
37
+
38
+ data
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,64 @@
1
+ module Editus
2
+ class Proxy
3
+ attr_reader :klass
4
+
5
+ def initialize klass
6
+ @klass = klass
7
+ end
8
+
9
+ def column_names
10
+ proxied_columns
11
+ end
12
+
13
+ def update_columns attributes; end
14
+
15
+ def find_by *args
16
+ record = klass.find_by(*args)
17
+ return record if record.blank?
18
+
19
+ RecordProxy.new(record)
20
+ end
21
+
22
+ def try *args
23
+ klass.try(*args)
24
+ end
25
+
26
+ private
27
+
28
+ def proxied_columns
29
+ info = Editus::Cop.instance.info(klass)
30
+ all = cols
31
+ exclude_fields = info[:exclude_fields] || []
32
+ fields = info[:fields] ? (all & info[:fields]) : all
33
+
34
+ fields - %w[id] - exclude_fields
35
+ end
36
+
37
+ def method_missing method, *args, &block
38
+ klass.try(method, *args, &block)
39
+ end
40
+
41
+ def respond_to_missing? *args
42
+ klass.send(:respond_to_missing?, *args)
43
+ end
44
+
45
+ def cols
46
+ klass.column_names
47
+ end
48
+ end
49
+
50
+ class RecordProxy < Proxy
51
+ def update_columns attributes
52
+ update_fields = attributes.keys
53
+ raise Editus::UpdateFieldError if (update_fields - proxied_columns).present?
54
+
55
+ klass.update_columns attributes
56
+ end
57
+
58
+ private
59
+
60
+ def cols
61
+ klass.class.column_names
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,77 @@
1
+ module Editus
2
+ module Script
3
+ class << self
4
+ def define name, &block
5
+ Editus::Script::DSL.run(name, &block)
6
+ end
7
+
8
+ def all
9
+ Editus::Script::Internal.scripts
10
+ end
11
+ end
12
+
13
+ class Internal
14
+ @scripts = {}
15
+
16
+ class << self
17
+ attr_reader :scripts
18
+ end
19
+
20
+ def self.find_or_create name
21
+ @scripts[name.to_sym] || @scripts[name.to_sym] = new(name.to_sym)
22
+ end
23
+
24
+ attr_accessor :name, :title, :path, :proxy, :content
25
+
26
+ def initialize name = nil
27
+ @name = name
28
+ end
29
+
30
+ def up
31
+ content[/task\s.*up.*do.*\n[\s\S]*?\n\s*end/]
32
+ end
33
+
34
+ def down
35
+ content[/task\s.*down.*do.*\n[\s\S]*?\n\s*end/]
36
+ end
37
+ end
38
+
39
+ class DSL
40
+ TASKS = %w[up down].freeze
41
+
42
+ def self.run name, &block
43
+ new(name).instance_eval(&block)
44
+ end
45
+
46
+ def initialize name
47
+ @name = name
48
+ end
49
+
50
+ def task method, &block
51
+ return unless TASKS.include?(method.to_s)
52
+
53
+ internal = Internal.find_or_create @name
54
+ internal.proxy ||= Editus::DefinitionProxy.new(@name)
55
+ internal.proxy.define_singleton_method(method, &block)
56
+ end
57
+
58
+ def title txt
59
+ internal = Internal.find_or_create @name
60
+ internal.title = txt
61
+ end
62
+ end
63
+
64
+ class Reader
65
+ def self.read path
66
+ return unless File.exist?(path)
67
+
68
+ load(path)
69
+ filename = File.basename(path, ".rb")
70
+ internal = Internal.find_or_create filename
71
+ internal.content = File.read(path)
72
+ internal.path = path
73
+ true
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,11 @@
1
+ module Editus
2
+ VERSION = "1.0.0"
3
+ SUMMARY = "This application is a simple web interface integrated with an existing Ruby on Rails application. It allows users to directly edit models, run predefined scripts, and easily undo changes."
4
+ DESCRIPTION = <<~DOC
5
+ This application is a user-friendly web interface designed to work with a Ruby on Rails application. With a simple and intuitive web interface, users can conveniently access the features of the application.
6
+
7
+ One of the key features of this application is the ability to directly edit models. This enables users to make changes directly to the data without the need to learn and use complex tools. Through the web interface, users can access and modify the attributes of models with ease.
8
+
9
+ Additionally, the application provides the capability to run predefined scripts. This allows users to perform automated tasks or handle complex data processing through prebuilt scripts. Running these scripts via the web interface saves users time and effort compared to alternative methods.
10
+ DOC
11
+ end
data/lib/editus.rb ADDED
@@ -0,0 +1,109 @@
1
+ require_relative "editus/actions"
2
+ require_relative "editus/client"
3
+ require_relative "editus/cop"
4
+ require_relative "editus/definition_proxy"
5
+ require_relative "editus/engine"
6
+ require_relative "editus/file"
7
+ require_relative "editus/proxy"
8
+ require_relative "editus/script"
9
+ require_relative "editus/version"
10
+
11
+ module Editus
12
+ class InvalidModelError < StandardError; end
13
+ class UpdateFieldError < StandardError; end
14
+
15
+ class Configuration
16
+ CONFIG_PATH = "config/editus.yml"
17
+ CONFIG_KEYS = %w[models auth actions_file_path]
18
+ INITIALIZER_FILE_PATH = "config/initializers/editus.rb"
19
+
20
+ def initialize
21
+ @table = {
22
+ models: [],
23
+ actions_file_path: "tmp/editus/actions.json"
24
+ }
25
+ end
26
+
27
+ def [] name
28
+ @table[name.to_sym]
29
+ end
30
+
31
+ def []= name, value
32
+ name = name.to_sym
33
+ @table[name] = value
34
+ end
35
+
36
+ private
37
+
38
+ def respond_to_missing? mid, include_private = false
39
+ mid[/.*(?==\z)/m].present? || super
40
+ end
41
+
42
+ def method_missing mid, *args
43
+ len = args.length
44
+ mname = mid[/.*(?==\z)/m]
45
+ if mname
46
+ if len != 1
47
+ raise ArgumentError, "wrong number of arguments (given #{len}, expected 1)", caller(1)
48
+ end
49
+
50
+ @table[mname.to_sym] = args[0]
51
+ elsif len.zero?
52
+ @table[mid]
53
+ else
54
+ begin
55
+ super
56
+ rescue NoMethodError => e
57
+ e.backtrace.shift
58
+ raise
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ class << self
65
+ attr_writer :configuration
66
+ attr_accessor :definition_file_paths
67
+ end
68
+
69
+ def self.configuration
70
+ @configuration ||= Editus::Configuration.new
71
+ end
72
+
73
+ def self.configure
74
+ yield(configuration)
75
+ end
76
+
77
+ self.definition_file_paths = %w[config/editus]
78
+
79
+ def self.find_definitions
80
+ absolute_definition_file_paths = definition_file_paths.map{|path| File.expand_path(path)}
81
+
82
+ absolute_definition_file_paths.uniq.each do |path|
83
+ next unless File.directory? path
84
+
85
+ Dir[File.join(path, "*.rb")].sort.each do |file|
86
+ Editus::Script::Reader.read(file)
87
+ end
88
+ end
89
+ end
90
+
91
+ def self.load_file_config
92
+ if File.exist?(Rails.root.join(Editus::Configuration::CONFIG_PATH))
93
+ yaml = File.read(Rails.root.join(Editus::Configuration::CONFIG_PATH))
94
+ config = YAML.safe_load(yaml)
95
+
96
+ configure do |c|
97
+ config.each do |key, value|
98
+ next unless Editus::Configuration::CONFIG_KEYS.include?(key)
99
+
100
+ c[key] = value
101
+ end
102
+ end
103
+ else
104
+ false
105
+ end
106
+ rescue StandardError
107
+ false
108
+ end
109
+ end
@@ -0,0 +1,15 @@
1
+ namespace :editus do
2
+ desc "Generate config file for Editus"
3
+ task :install do
4
+ initializer_file_path = Rails.root.join(Editus::Configuration::INITIALIZER_FILE_PATH)
5
+ content = <<~DOC
6
+ # Editus.configure do |config|
7
+ # config.auth = [%w[user@example.com Pass@123456], %w[manager@example.com Pass@123456]]
8
+ # config.models = ["User", {name: "Admin", fields: %w[name], exclude_fields: %w[id]}]
9
+ # config.actions_file_path = "tmp/editus/actions.json"
10
+ # end
11
+ DOC
12
+ puts ">>> #{Editus::Configuration::INITIALIZER_FILE_PATH}"
13
+ File.write(initializer_file_path, content)
14
+ end
15
+ end