rails_execution 0.1.5 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
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')