rails_execution 0.1.5 → 0.1.7

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -0
  3. data/app/assets/javascripts/executions/base.js +9 -0
  4. data/app/assets/javascripts/executions/flatpickr.min.js +2 -0
  5. data/app/assets/stylesheets/executions/flatpickr.min.css +13 -0
  6. data/app/assets/stylesheets/executions/modify.scss +11 -6
  7. data/app/assets/stylesheets/executions/style.scss +153 -0
  8. data/app/controllers/rails_execution/labels_controller.rb +29 -0
  9. data/app/controllers/rails_execution/tasks_controller.rb +84 -24
  10. data/app/helpers/rails_execution/base_helper.rb +4 -2
  11. data/app/helpers/rails_execution/policy_helper.rb +16 -0
  12. data/app/helpers/rails_execution/rendering_helper.rb +28 -4
  13. data/app/models/rails_execution/label.rb +15 -0
  14. data/app/models/rails_execution/task.rb +71 -3
  15. data/app/models/rails_execution/task_label.rb +6 -0
  16. data/app/services/rails_execution/tasks/create_service.rb +62 -0
  17. data/app/services/rails_execution/tasks/filter_service.rb +49 -0
  18. data/app/views/rails_execution/shared/_paging.html.haml +1 -1
  19. data/app/views/rails_execution/tasks/_actions.html.haml +3 -1
  20. data/app/views/rails_execution/tasks/_activities.html.haml +16 -10
  21. data/app/views/rails_execution/tasks/_advanced_filter.html.haml +115 -0
  22. data/app/views/rails_execution/tasks/_attachment_files.html.haml +2 -11
  23. data/app/views/rails_execution/tasks/_attachments.html.haml +13 -0
  24. data/app/views/rails_execution/tasks/_form.html.haml +50 -14
  25. data/app/views/rails_execution/tasks/_label_collection_select.html.haml +20 -0
  26. data/app/views/rails_execution/tasks/_labels.html.haml +2 -0
  27. data/app/views/rails_execution/tasks/_new_label_modal_form.html.haml +24 -0
  28. data/app/views/rails_execution/tasks/_quick_filter.html.haml +25 -0
  29. data/app/views/rails_execution/tasks/_schedule.html.haml +22 -0
  30. data/app/views/rails_execution/tasks/_task.html.haml +22 -18
  31. data/app/views/rails_execution/tasks/closed.html.haml +2 -1
  32. data/app/views/rails_execution/tasks/completed.html.haml +2 -1
  33. data/app/views/rails_execution/tasks/index.html.haml +2 -1
  34. data/app/views/rails_execution/tasks/show.html.haml +3 -1
  35. data/config/routes.rb +2 -1
  36. data/lib/generators/rails_execution/templates/config.rb.tt +8 -0
  37. data/lib/generators/rails_execution/templates/install.rb.tt +24 -7
  38. data/lib/rails_execution/config.rb +20 -7
  39. data/lib/rails_execution/services/background_execution.rb +56 -0
  40. data/lib/rails_execution/services/create_scheduled_job.rb +61 -0
  41. data/lib/rails_execution/services/remove_scheduled_job.rb +20 -0
  42. data/lib/rails_execution/services/task_scheduler.rb +35 -0
  43. data/lib/rails_execution/version.rb +1 -1
  44. data/lib/rails_execution.rb +4 -0
  45. metadata +21 -2
@@ -2,10 +2,12 @@
2
2
 
3
3
  module RailsExecution
4
4
  class TasksController < ::RailsExecution::BaseController
5
+ LIMITED_FILTER_LIST = 100
5
6
 
6
7
  def index
7
8
  paging = ::RailsExecution::Services::Paging.new(page: params[:page], per_page: params[:per_page])
8
- processing_tasks = ::RailsExecution::Task.processing.descending.includes(:owner)
9
+ processing_tasks = ::RailsExecution::Task.processing.preload(:owner, :labels)
10
+ processing_tasks = ::RailsExecution::Tasks::FilterService.new(processing_tasks, params, current_owner&.id).call
9
11
  @tasks = paging.call(processing_tasks)
10
12
  end
11
13
 
@@ -18,21 +20,11 @@ module RailsExecution
18
20
  def create
19
21
  raise(::RailsExecution::AccessDeniedError, 'Create task') unless can_create_task?
20
22
 
21
- @task = ::RailsExecution::Task.new({
22
- status: :created,
23
- owner_id: current_owner&.id,
24
- owner_type: ::RailsExecution.configuration.owner_model.to_s,
25
- title: params.dig(:task, :title),
26
- description: params.dig(:task, :description),
27
- script: params.dig(:task, :script),
28
- })
29
- @task.assign_reviewers(params.dig(:task, :task_review_ids).to_a)
30
- @task.syntax_status = ::RailsExecution::Services::SyntaxChecker.new(@task.script).call ? 'good' : 'bad'
31
-
32
- if @task.save
33
- @task.add_files(params[:attachments]&.permit!.to_h, current_owner) if ::RailsExecution.configuration.file_upload
34
- ::RailsExecution.configuration.notifier.new(@task).after_create
35
- flash[:notice] = 'Create the request is successful!'
23
+ result = ::RailsExecution::Tasks::CreateService.new(params, current_owner).call
24
+ @task = result.task
25
+ if result.error.nil?
26
+ ::RailsExecution::Services::CreateScheduledJob.new(@task).call if in_solo_mode? && can_schedule_task?(@task)
27
+ flash[:notice] = 'Create request successfully!'
36
28
  redirect_to action: :index
37
29
  else
38
30
  render action: :new
@@ -47,9 +39,12 @@ module RailsExecution
47
39
  owner_id: current_owner&.id,
48
40
  owner_type: ::RailsExecution.configuration.owner_model.to_s,
49
41
  title: current_task.title,
42
+ scheduled_at: current_task.scheduled_at,
43
+ repeat_mode: current_task.repeat_mode,
50
44
  description: current_task.description,
51
45
  script: current_task.script,
52
46
  })
47
+ @task.labels = current_task.labels
53
48
  @task.syntax_status = ::RailsExecution::Services::SyntaxChecker.new(@task.script).call ? 'good' : 'bad'
54
49
 
55
50
  render action: :new
@@ -65,6 +60,7 @@ module RailsExecution
65
60
  end
66
61
 
67
62
  if current_task.update(status: :closed)
63
+ ::RailsExecution::Services::RemoveScheduledJob.new(current_task).call if can_remove_scheduled_job?(current_task)
68
64
  current_task.activities.create(owner: current_owner, message: 'Closed the task')
69
65
  ::RailsExecution.configuration.notifier.new(current_task).after_close
70
66
  redirect_to(action: :show) and return
@@ -84,20 +80,26 @@ module RailsExecution
84
80
  raise(::RailsExecution::AccessDeniedError, 'Edit task') unless can_edit_task?(current_task)
85
81
 
86
82
  @task = current_task
83
+
87
84
  old_script = @task.script
88
85
  old_reviewer_ids = @task.task_reviews.pluck(:owner_id)
89
86
  checked_owner_ids = @task.task_reviews.checked.pluck(:owner_id)
87
+ old_scheduled_at = @task.scheduled_at
90
88
 
91
89
  update_data = {
92
90
  title: params.dig(:task, :title),
93
91
  description: params.dig(:task, :description),
92
+ scheduled_at: Time.zone.parse(params.dig(:task, :scheduled_at).to_s),
93
+ repeat_mode: params.dig(:task, :repeat_mode),
94
94
  }
95
95
 
96
96
  update_data[:script] = params.dig(:task, :script) if @task.in_processing?
97
97
  @task.assign_reviewers(params.dig(:task, :task_review_ids).to_a)
98
+ @task.assign_labels(params[:task_label_ids].to_a) if params.key?(:task_label_ids)
98
99
  @task.syntax_status = ::RailsExecution::Services::SyntaxChecker.new(update_data[:script]).call ? 'good' : 'bad'
99
100
 
100
101
  if @task.update(update_data)
102
+ ::RailsExecution::Services::RemoveScheduledJob.new(@task).call if (old_script != @task.script || @task.scheduled_at != old_scheduled_at) && can_remove_scheduled_job?(@task)
101
103
  @task.add_files(params[:attachments]&.permit!.to_h, current_owner) if ::RailsExecution.configuration.file_upload
102
104
  @task.activities.create(owner: current_owner, message: 'Updated the Task')
103
105
  ::RailsExecution.configuration.notifier.new(@task).after_update_script(current_owner, checked_owner_ids) if old_script != @task.script
@@ -110,18 +112,21 @@ module RailsExecution
110
112
 
111
113
  def completed
112
114
  paging = ::RailsExecution::Services::Paging.new(page: params[:page], per_page: params[:per_page])
113
- completed_tasks = ::RailsExecution::Task.is_completed.descending.includes(:owner)
115
+ completed_tasks = ::RailsExecution::Task.is_completed.preload(:owner, :labels)
116
+ completed_tasks = ::RailsExecution::Tasks::FilterService.new(completed_tasks, params, current_owner&.id).call
114
117
  @tasks = paging.call(completed_tasks)
115
118
  end
116
119
 
117
120
  def closed
118
121
  paging = ::RailsExecution::Services::Paging.new(page: params[:page], per_page: params[:per_page])
119
- closed_tasks = ::RailsExecution::Task.is_closed.descending.includes(:owner)
122
+ closed_tasks = ::RailsExecution::Task.is_closed.preload(:owner, :labels)
123
+ closed_tasks = ::RailsExecution::Tasks::FilterService.new(closed_tasks, params, current_owner&.id).call
120
124
  @tasks = paging.call(closed_tasks)
121
125
  end
122
126
 
123
127
  def reopen
124
128
  if current_task.update(status: :created)
129
+ ::RailsExecution::Services::CreateScheduledJob.new(current_task).call if in_solo_mode? && can_schedule_task?(current_task)
125
130
  current_task.activities.create(owner: current_owner, message: 'Re-opened the Task')
126
131
  ::RailsExecution.configuration.notifier.new(current_task).after_reopen
127
132
  flash[:notice] = 'Your task is re-opened'
@@ -133,6 +138,7 @@ module RailsExecution
133
138
 
134
139
  def reject
135
140
  if ::RailsExecution::Services::Approvement.new(current_task, reviewer: current_owner).reject
141
+ ::RailsExecution::Services::RemoveScheduledJob.new(current_task).call if can_remove_scheduled_job?(current_task)
136
142
  ::RailsExecution.configuration.notifier.new(current_task).after_reject(current_owner)
137
143
  flash[:notice] = 'Your decision is updated!'
138
144
  else
@@ -143,10 +149,11 @@ module RailsExecution
143
149
 
144
150
  def approve
145
151
  if ::RailsExecution::Services::Approvement.new(current_task, reviewer: current_owner).approve
152
+ ::RailsExecution::Services::CreateScheduledJob.new(current_task).call if can_schedule_task?(current_task)
146
153
  ::RailsExecution.configuration.notifier.new(current_task).after_approve(current_owner)
147
154
  flash[:notice] = 'Your decision is updated!'
148
155
  else
149
- flash[:alert] = "Your decision is can't update!"
156
+ flash[:alert] = "Your decision isn't updated!"
150
157
  end
151
158
  redirect_to action: :show
152
159
  end
@@ -159,7 +166,7 @@ module RailsExecution
159
166
 
160
167
  execute_service = ::RailsExecution::Services::Execution.new(current_task)
161
168
  if execute_service.call
162
- current_task.update(status: :completed)
169
+ current_task.update(status: :completed) unless current_task.repeatable?
163
170
  current_task.activities.create(owner: current_owner, message: 'Execute: The task is completed')
164
171
  ::RailsExecution.configuration.notifier.new(current_task).after_execute_success(current_owner)
165
172
  flash[:notice] = 'This task is executed'
@@ -171,6 +178,15 @@ module RailsExecution
171
178
  redirect_to(action: :show)
172
179
  end
173
180
 
181
+ def execute_in_background
182
+ unless can_execute_task?(current_task)
183
+ flash[:alert] = "This task can't execute: #{how_to_executable(current_task)}"
184
+ redirect_to(action: :show) and return
185
+ end
186
+ RailsExecution::Services::BackgroundExecution.new(current_task, current_owner).setup
187
+ redirect_to(action: :show)
188
+ end
189
+
174
190
  private
175
191
 
176
192
  def current_task
@@ -179,14 +195,28 @@ module RailsExecution
179
195
  helper_method :current_task
180
196
 
181
197
  def reviewers
182
- @reviewers ||= ::RailsExecution.configuration.reviewers.call.map do |reviewer|
183
- ::OpenStruct.new(reviewer)
184
- end
198
+ return @reviewers if defined?(@reviewers)
199
+
200
+ list = ::RailsExecution.configuration.reviewers.call
201
+ list = list.map { |reviewer| reviewer[:id] == current_owner.id ? nil : ::OpenStruct.new(reviewer) }
202
+ @reviewers = list.compact
185
203
  end
186
204
  helper_method :reviewers
187
205
 
206
+ def repeat_mode_options
207
+ @repeat_mode_options ||= {
208
+ 'Does not repeat': :none,
209
+ 'Daily': :daily,
210
+ 'Weekly': :weekly,
211
+ 'Monthly': :monthly,
212
+ 'Anually': :anually,
213
+ 'Every Weekday': :weekdays,
214
+ }
215
+ end
216
+ helper_method :repeat_mode_options
217
+
188
218
  def task_logs
189
- @task_logs ||= ::RailsExecution.configuration.logging_files.call(current_task).select(&:present?)
219
+ @task_logs ||= ::RailsExecution.configuration.logging_files.call(current_task)
190
220
  end
191
221
  helper_method :task_logs
192
222
 
@@ -200,5 +230,35 @@ module RailsExecution
200
230
  end
201
231
  helper_method :reviewing_accounts
202
232
 
233
+ def filter_owners
234
+ @filter_owners ||= query_filter_owners
235
+ end
236
+ helper_method :filter_owners
237
+
238
+ def query_filter_owners
239
+ return ::RailsExecution.configuration.owner_model.constantize
240
+ .where(id: RailsExecution::Task.select(:owner_id).distinct)
241
+ .limit(LIMITED_FILTER_LIST)
242
+ end
243
+
244
+ def filter_labels
245
+ @filter_labels ||= RailsExecution::Label.order(:name)
246
+ end
247
+ helper_method :filter_labels
248
+
249
+ def current_filtered_owner
250
+ @current_filtered_owner ||= ::RailsExecution.configuration.owner_model.constantize.find_by(id: params[:owner_id])
251
+ end
252
+ helper_method :current_filtered_owner
253
+
254
+ def current_filtered_label
255
+ @current_filtered_label ||= ::RailsExecution::Label.find_by(id: params[:label_id])
256
+ end
257
+ helper_method :current_filtered_label
258
+
259
+ def comments_count_by_task
260
+ @comments_count_by_task ||= ::RailsExecution::Comment.where(task_id: @tasks.ids).group(:task_id).count
261
+ end
262
+ helper_method :comments_count_by_task
203
263
  end
204
264
  end
@@ -3,12 +3,14 @@ module RailsExecution
3
3
  include ActionView::Helpers::AssetUrlHelper
4
4
 
5
5
  def current_owner
6
- return nil if in_solo_mode?
7
6
  return @current_owner if defined?(@current_owner)
8
7
  return nil if ::RailsExecution.configuration.owner_method.blank?
9
-
10
8
  @current_owner = self.send(::RailsExecution.configuration.owner_method)
11
9
  end
12
10
 
11
+ def normal_labels
12
+ @normal_labels ||= RailsExecution::Label.normal
13
+ end
14
+
13
15
  end
14
16
  end
@@ -51,6 +51,7 @@ module RailsExecution
51
51
  return 'Script is empty' if task.script.blank?
52
52
  return "Task is closed" if task.is_closed?
53
53
  return "It's bad Syntax" if task.syntax_status_bad?
54
+ return "This task is being processed" if task.is_processing?
54
55
 
55
56
  unless in_solo_mode?
56
57
  return 'This task is not approved' unless task.is_approved?
@@ -60,5 +61,20 @@ module RailsExecution
60
61
  return "Can't executable by app policy" unless ::RailsExecution.configuration.task_executable.call(current_owner, task)
61
62
  end
62
63
 
64
+ def can_schedule_task?(task)
65
+ how_to_schedulable(task).blank?
66
+ end
67
+
68
+ def how_to_schedulable(task)
69
+ execute_message = how_to_executable(task)
70
+ return execute_message if execute_message
71
+ return "Time is not set for schedule" if task.scheduled_at.nil?
72
+ return "Scheduled at time in the past" if task.scheduled_at.past?
73
+ end
74
+
75
+ def can_remove_scheduled_job?(task)
76
+ task.jid.present?
77
+ end
78
+
63
79
  end
64
80
  end
@@ -1,5 +1,7 @@
1
1
  module RailsExecution
2
2
  module RenderingHelper
3
+ AVATAR_CLASS = 'bd-placeholder-img flex-shrink-0 me-2 rounded'
4
+ TRUNCATE_FILE_NAME_LENGTH = 20
3
5
 
4
6
  def render_user_info(user, avatar_size: '40x40')
5
7
  content_tag :div, class: 'user-info' do
@@ -9,16 +11,16 @@ module RailsExecution
9
11
  end
10
12
 
11
13
  def render_owner_avatar(owner, size: '32x32')
12
- return nil if owner.blank?
14
+ return image_tag(asset_path('executions/robot.png'), size: size, class: AVATAR_CLASS) if owner.blank?
13
15
 
14
16
  avatar_url = RailsExecution.configuration.owner_avatar.call(owner)
15
17
  return nil if avatar_url.blank?
16
18
 
17
- image_tag avatar_url, size: size, class: 'bd-placeholder-img flex-shrink-0 me-2 rounded'
19
+ image_tag avatar_url, size: size, class: AVATAR_CLASS
18
20
  end
19
21
 
20
22
  def render_owner_name(owner)
21
- return nil if owner.blank?
23
+ return 'System' if owner.blank?
22
24
  return nil if RailsExecution.configuration.owner_name_method.blank?
23
25
 
24
26
  content_tag :span, owner.public_send(RailsExecution.configuration.owner_name_method)
@@ -39,6 +41,27 @@ module RailsExecution
39
41
  end
40
42
  end
41
43
 
44
+ def render_label(task_label)
45
+ content_tag :div do
46
+ concat content_tag :i, '', class: "bi bi-tag label-colors lc-#{task_label.color} bg-none"
47
+ concat content_tag :span, task_label.name, class: "badge rounded-pill mx-1 label-colors lc-#{task_label.color}"
48
+ end
49
+ end
50
+
51
+ def render_task_labels(task)
52
+ # to_a and sort_by instead of .order for avoid N+1 query to sort task labels for each task
53
+ task.labels.to_a.sort_by(&:name).reduce(''.html_safe) do |result, label|
54
+ case label.name
55
+ when 'repeat'
56
+ result + content_tag(:span, "repeat: #{task.repeat_mode}", class: "badge rounded-pill mx-1 label-tag label-colors lc-#{label.color}", data: { id: label.id })
57
+ when 'scheduled'
58
+ result + content_tag(:span, 'scheduled', class: "badge rounded-pill mx-1 label-tag label-colors lc-#{label.color}", data: { id: label.id })
59
+ else
60
+ result + content_tag(:span, label.name, class: "badge rounded-pill mx-1 label-tag label-colors lc-#{label.color}", data: { id: label.id })
61
+ end
62
+ end
63
+ end
64
+
42
65
  def task_reviewed_status(task)
43
66
  @task_reviewed_status ||= {}
44
67
  @task_reviewed_status[task] ||= task.task_reviews.find_by(owner_id: current_owner&.id)&.status&.inquiry
@@ -67,7 +90,7 @@ module RailsExecution
67
90
  end
68
91
 
69
92
  def re_get_file_name(url)
70
- URI(url).path.split('/').last
93
+ URI(url).path.split('/').last&.truncate(TRUNCATE_FILE_NAME_LENGTH)
71
94
  end
72
95
 
73
96
  def re_badge_color(status)
@@ -86,6 +109,7 @@ module RailsExecution
86
109
  rejected: 'danger',
87
110
  approved: 'success',
88
111
  reviewing: 'secondary',
112
+ processing: 'warning',
89
113
  completed: 'success',
90
114
  }[text] || 'secondary'
91
115
  end
@@ -0,0 +1,15 @@
1
+ class RailsExecution::Label < RailsExecution::AppModel
2
+ SPECIAL_LABLES = ['scheduled', 'repeat']
3
+ COLORS = 'red pink purple deep-purple indigo blue light-blue cyan teal green orange deep-orange brown grey blue-grey'.split
4
+
5
+ has_many :task_labels, class_name: 'RailsExecution::TaskLabel'
6
+ has_many :tasks, through: :task_labels
7
+
8
+ validates :name, presence: true, uniqueness: { case_sensitive: false }
9
+ scope :normal, -> { where.not(name: SPECIAL_LABLES) }
10
+
11
+ def color
12
+ color_index = self.name.to_s.chars.sum(&:ord) % COLORS.size
13
+ COLORS[color_index]
14
+ end
15
+ end
@@ -1,21 +1,29 @@
1
1
  class RailsExecution::Task < RailsExecution::AppModel
2
2
 
3
- PROCESSING_STATUSES = %w(created reviewing approved rejected)
3
+ PROCESSING_STATUSES = %w(created reviewing approved processing rejected)
4
+ ACTIVE_REPEAT_MODE = %w(hourly daily weekly monthly annually weekdays)
4
5
 
5
6
  has_many :activities, class_name: 'RailsExecution::Activity'
6
7
  has_many :comments, class_name: 'RailsExecution::Comment'
7
8
  has_many :task_reviews, class_name: 'RailsExecution::TaskReview'
9
+ has_many :task_labels, class_name: 'RailsExecution::TaskLabel'
10
+ has_many :labels, through: :task_labels
8
11
  attr_accessor :reviewer_ids
9
12
 
10
13
  validates :title, presence: true
11
14
  validates :status, presence: true
15
+ validate :validate_scheduled_at, if: :scheduled_at_changed?
16
+
17
+ before_validation :auto_assign_labels
12
18
 
13
19
  enum status: {
14
20
  created: 'created',
15
21
  reviewing: 'reviewing',
16
22
  approved: 'approved',
17
- rejected: 'rejected',
23
+ processing: 'processing', # Processing by background job
18
24
  completed: 'completed',
25
+ failed: 'failed',
26
+ rejected: 'rejected',
19
27
  closed: 'closed',
20
28
  }, _prefix: :is
21
29
 
@@ -24,6 +32,16 @@ class RailsExecution::Task < RailsExecution::AppModel
24
32
  good: 'good',
25
33
  }, _prefix: true
26
34
 
35
+ enum repeat_mode: {
36
+ none: 'Does not repeat',
37
+ hourly: 'Hourly',
38
+ daily: 'Daily',
39
+ weekly: 'Weekly',
40
+ monthly: 'Monthly',
41
+ annually: 'Anually',
42
+ weekdays: 'Every weekday (Monday to Friday)',
43
+ }, _prefix: :repeat
44
+
27
45
  scope :processing, -> { where(status: PROCESSING_STATUSES) }
28
46
 
29
47
  before_update :re_assign_status
@@ -33,6 +51,14 @@ class RailsExecution::Task < RailsExecution::AppModel
33
51
  PROCESSING_STATUSES.include?(self.status)
34
52
  end
35
53
 
54
+ def scheduled?
55
+ self.scheduled_at? && self.repeat_none?
56
+ end
57
+
58
+ def repeatable?
59
+ ACTIVE_REPEAT_MODE.include?(self.repeat_mode)
60
+ end
61
+
36
62
  def assign_reviewers(ids)
37
63
  ids.each do |id|
38
64
  next if id.blank?
@@ -45,6 +71,10 @@ class RailsExecution::Task < RailsExecution::AppModel
45
71
  end
46
72
  end
47
73
 
74
+ def assign_labels(ids)
75
+ self.labels = RailsExecution::Label.where(id: ids.uniq)
76
+ end
77
+
48
78
  def reviewer_ids
49
79
  self.task_reviews.pluck(:owner_id)
50
80
  end
@@ -53,6 +83,23 @@ class RailsExecution::Task < RailsExecution::AppModel
53
83
  ::RailsExecution.configuration.file_uploader.new(self, attachments, owner: current_owner).call
54
84
  end
55
85
 
86
+ def repeat_mode_select_options
87
+ return RailsExecution::Task.repeat_modes if scheduled_at.nil?
88
+
89
+ options = RailsExecution::Task.repeat_modes.dup
90
+ options['hourly'] = self.scheduled_at.strftime('Hourly at minute %-M')
91
+ options['daily'] = self.scheduled_at.strftime('Daily at %H:%M')
92
+ options['weekly'] = self.scheduled_at.strftime('Weekly on %A at %H:%M')
93
+ options['monthly'] = self.scheduled_at.strftime('Monthly on %-d at %H:%M')
94
+ options['annually'] = self.scheduled_at.strftime('Annually on %B %-d at %H:%M')
95
+ options['weekdays'] = self.scheduled_at.strftime('Every weekday (Monday to Friday) at %H:%M')
96
+ return options
97
+ end
98
+
99
+ def next_time_at
100
+ ::RailsExecution::Services::CreateScheduledJob.new(self).calculate_next_time_at.strftime("%Y-%m-%d %H:%M")
101
+ end
102
+
56
103
  private
57
104
 
58
105
  def create_activity
@@ -62,10 +109,31 @@ class RailsExecution::Task < RailsExecution::AppModel
62
109
  def re_assign_status
63
110
  return if self.is_completed? || self.is_closed?
64
111
 
65
- if self.is_approved? && self.script_changed?
112
+ if self.is_approved? && (self.script_changed? || self.scheduled_at_changed?)
66
113
  self.status = :reviewing
67
114
  self.task_reviews.update_all(status: :reviewing)
68
115
  end
69
116
  end
70
117
 
118
+ def validate_scheduled_at
119
+ return if self.scheduled_at.blank?
120
+
121
+ errors.add(:scheduled_at, "Input schedule time #{scheduled_at} is in the past for non-repeat mode") if scheduled_at.past? && repeat_none?
122
+ end
123
+
124
+ def auto_assign_labels
125
+ scheduled_label = RailsExecution::Label.find_or_create_by(name: :scheduled)
126
+ repeat_label = RailsExecution::Label.find_or_create_by(name: :repeat)
127
+
128
+ final_label_ids = self.label_ids - [scheduled_label.id, repeat_label.id]
129
+
130
+ if self.scheduled?
131
+ final_label_ids << scheduled_label.id
132
+ elsif self.repeatable?
133
+ final_label_ids << repeat_label.id
134
+ end
135
+
136
+ self.label_ids = final_label_ids
137
+ end
138
+
71
139
  end
@@ -0,0 +1,6 @@
1
+ class RailsExecution::TaskLabel < RailsExecution::AppModel
2
+ belongs_to :task, class_name: 'RailsExecution::Task'
3
+ belongs_to :label, class_name: 'RailsExecution::Label'
4
+
5
+ validates :task, uniqueness: { scope: :label }
6
+ end
@@ -0,0 +1,62 @@
1
+ class RailsExecution::Tasks::CreateService
2
+
3
+ def initialize(params, current_owner)
4
+ @params = params
5
+ @current_owner = current_owner
6
+ @task = nil
7
+ @error = nil
8
+ end
9
+
10
+ def call
11
+ initialize_task
12
+ create_task
13
+ rescue => e
14
+ @error = e.to_s
15
+ ensure
16
+ return self
17
+ end
18
+
19
+ attr_reader :error
20
+ attr_reader :task
21
+
22
+ private
23
+
24
+ attr_reader :params
25
+ attr_reader :current_owner
26
+
27
+ def initialize_task
28
+ @task = ::RailsExecution::Task.new({
29
+ status: :created,
30
+ owner_id: current_owner&.id,
31
+ owner_type: ::RailsExecution.configuration.owner_model.to_s,
32
+ title: params.dig(:task, :title),
33
+ description: params.dig(:task, :description),
34
+ script: params.dig(:task, :script),
35
+ scheduled_at: parsed_scheduled_at,
36
+ repeat_mode: params.dig(:task, :repeat_mode)
37
+ })
38
+
39
+ @task.assign_reviewers(params.dig(:task, :task_review_ids).to_a)
40
+ @task.assign_labels(params[:task_label_ids].to_a)
41
+ @task.syntax_status = ::RailsExecution::Services::SyntaxChecker.new(@task.script).call ? 'good' : 'bad'
42
+ end
43
+
44
+ def scheduled_at
45
+ @scheduled_at ||= params.dig(:task, :scheduled_at)
46
+ end
47
+
48
+ def parsed_scheduled_at
49
+ @parsed_scheduled_at ||= scheduled_at.blank? ? nil : Time.zone.parse(scheduled_at)
50
+ rescue
51
+ raise 'Invalid input for scheduling time'
52
+ end
53
+
54
+ def create_task
55
+ if @task.save
56
+ @task.add_files(params[:attachments]&.permit!.to_h, current_owner) if ::RailsExecution.configuration.file_upload
57
+ ::RailsExecution.configuration.notifier.new(@task).after_create
58
+ else
59
+ raise @task.errors.messages.values.flatten.first
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,49 @@
1
+ class RailsExecution::Tasks::FilterService
2
+
3
+ def initialize(tasks_query_object, params, current_user_id = nil)
4
+ @tasks_query_object = tasks_query_object
5
+ @params = params
6
+ @current_user_id = current_user_id
7
+ end
8
+
9
+ def call
10
+ filter_by_owner
11
+ filter_by_label
12
+ filter_by_keyword
13
+ order_by_recently_updated
14
+
15
+ return @tasks_query_object.descending
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :params
21
+ attr_reader :current_user_id
22
+
23
+ def filter_by_owner
24
+ if params[:my_task] == '1' && current_user_id
25
+ @tasks_query_object = @tasks_query_object.where(owner_id: current_user_id)
26
+ elsif params[:owner_id].present?
27
+ @tasks_query_object = @tasks_query_object.where(owner_id: params[:owner_id])
28
+ end
29
+ end
30
+
31
+ def filter_by_label
32
+ return if params[:label_id].blank?
33
+
34
+ filtered_task_ids = RailsExecution::TaskLabel.where(label_id: params[:label_id]).select(:task_id)
35
+ @tasks_query_object = @tasks_query_object.where(id: filtered_task_ids)
36
+ end
37
+
38
+ def filter_by_keyword
39
+ return if params[:keyword].blank?
40
+
41
+ @tasks_query_object = @tasks_query_object.where("rails_execution_tasks.title LIKE ?", "%#{params[:keyword]}%")
42
+ end
43
+
44
+ def order_by_recently_updated
45
+ sort_by = params[:recently_updated] == '1' ? { updated_at: :desc } : { updated_at: :asc }
46
+ @tasks_query_object = @tasks_query_object.order(sort_by)
47
+ end
48
+
49
+ end
@@ -1,7 +1,7 @@
1
1
  - return if total_pages <= 1
2
2
  - original_params = params.permit!.to_h
3
3
 
4
- - gap_number = 2
4
+ - gap_number = 1
5
5
  - page_numbers = (1..total_pages).to_a
6
6
  - begin_paging = (1..(1 + gap_number)).to_a
7
7
  - middle_paging = page_numbers & ((page - gap_number)..(page + gap_number)).to_a
@@ -2,7 +2,9 @@
2
2
  = re_card_content do
3
3
  .text-center
4
4
  - if can_execute_task?(current_task)
5
- = link_to 'Execute NOW!!!', execute_task_path(current_task), class: 'btn btn-primary', method: :patch, data: { confirm: 'Are you sure?' }
5
+ = link_to 'Execute NOW!!!', execute_task_path(current_task), class: 'btn btn-block btn-primary', method: :patch, data: { confirm: 'Are you sure?' }
6
+ - if RailsExecution.configuration.task_background
7
+ = link_to 'Execute in background!', execute_in_background_task_path(current_task), class: 'btn btn-block btn-secondary', method: :patch, data: { confirm: 'Are you sure?' }
6
8
  - else
7
9
  %p.text-danger.mb-0.fw-bold
8
10
  %i.bi.bi-x-octagon.me-1
@@ -2,21 +2,27 @@
2
2
  = re_card_content do
3
3
  %h5.section-title Logs
4
4
  %ul.list-unstyled#task-logs
5
- - task_logs.each do |task_log|
6
- %li.fs-13.row.mb-3
7
- .col.text-muted
8
- %i.bi.bi-journal-text
9
- = re_get_file_name(task_log)
10
- .col-3.text-end= link_to 'Download', task_log, target: :_blank, class: 'text-muted click2download'
5
+ - task_logs.each do |task_log, created_at|
6
+ %li.row.g-0.vetical-align-middle
7
+ .col.d-flex.align-items-center= re_get_file_name(task_log)
8
+ - if created_at
9
+ .col-auto.pe-3
10
+ .activitiy-time.text-muted.text-end
11
+ %span.fs13px= created_at.strftime('%Y/%m/%d')
12
+ %p.mb-0= created_at.strftime('%H:%M')
13
+ .col-auto.text-center
14
+ = link_to task_log, target: :_blank, class: 'text-muted click2download' do
15
+ %i.bi.bi-download.fs-5
16
+ %p.small.mb-0 Download
11
17
 
12
18
  = re_card_content id: 'activities-section' do
13
19
  %h5.section-title Activities
14
20
  %ul.list-unstyled
15
21
  - current_task.activities.descending.preload(:owner).each do |activitiy|
16
- %li.row
22
+ %li.row.g-0.vetical-align-middle.mb-3
17
23
  .col
18
24
  = render_user_info(activitiy.owner, avatar_size: '20x20')
19
- %p.normal-text.mt-1= activitiy.message
20
- .col-3.text-end.activitiy-time
25
+ %p.mb-0.normal-text.mt-1= activitiy.message
26
+ .col-auto.text-end.activitiy-time
21
27
  %span.fs13px.text-muted= activitiy.created_at.strftime('%Y/%m/%d')
22
- %p.text-muted= activitiy.created_at.strftime('%H:%M')
28
+ %p.mb-0.text-muted= activitiy.created_at.strftime('%H:%M')