marty 11.0.0 → 13.0.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintignore +2 -0
  3. data/.eslintrc.js +11 -6
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +15 -0
  6. data/.schemalintrc.js +41 -0
  7. data/CHANGELOG.md +12 -0
  8. data/app/channels/marty/notification_channel.rb +1 -0
  9. data/app/components/marty/postings/new_form.rb +12 -6
  10. data/app/components/marty/postings/new_form/client/new_form.js +1 -1
  11. data/app/components/marty/postings/summary_grid.rb +3 -3
  12. data/app/helpers/marty/application_helper.rb +17 -0
  13. data/app/jobs/marty/cron_job.rb +3 -2
  14. data/app/models/marty/posting.rb +9 -11
  15. data/app/models/marty/posting_type.rb +2 -3
  16. data/app/models/marty/promise.rb +9 -2
  17. data/app/views/layouts/marty/application.html.erb +4 -0
  18. data/db/migrate/527_use_pg_enum_for_posting_types.rb +61 -0
  19. data/db/migrate/600_replace_varchars_with_text.rb +89 -0
  20. data/db/migrate/601_add_posting_type_index_to_postings.rb +7 -0
  21. data/db/migrate/602_replace_text_with_varchars_without_size_limit.rb +89 -0
  22. data/db/seeds.rb +4 -5
  23. data/docker-compose.dummy.yml +1 -0
  24. data/lib/marty/aws/request.rb +7 -5
  25. data/lib/marty/diagnostic/version.rb +3 -2
  26. data/lib/marty/migrations.rb +10 -0
  27. data/lib/marty/version.rb +1 -1
  28. data/make-lint.mk +5 -1
  29. data/package.json +10 -5
  30. data/spec/controllers/diagnostic/controller_spec.rb +6 -2
  31. data/spec/dummy/app/assets/config/manifest.js +1 -0
  32. data/spec/dummy/app/assets/javascripts/application.js +14 -0
  33. data/spec/dummy/db/migrate/20200402150405_add_posting_types.rb +14 -0
  34. data/spec/lib/migrations/vw_marty_postings.sql.expected +2 -4
  35. data/spec/models/posting_spec.rb +0 -2
  36. data/spec/models/promise_spec.rb +33 -0
  37. data/spec/services/background_job/update_schedule_spec.rb +34 -0
  38. metadata +11 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 119afa3c55c47a985cce5d53a9868f605e2fe401f90bd66bcf3465b03eaf8c5d
4
- data.tar.gz: 83dfb56e30a009281111f825a05d603fef30830cf3c67e69c9e0399124d7e5a6
3
+ metadata.gz: 9af04e8e76cf3023af1aa02e26000f68ecfe47ded9811345921b01a266a79e8f
4
+ data.tar.gz: 4846444b2028e4f757082647c453cae88e6d034e1eea15c4b14eadee0eedb632
5
5
  SHA512:
6
- metadata.gz: 1dfadeffa2456aa43340ba6a6848d78dcf8f938e7e500b78a262c9ae5886145884921ef58c08c9e8edc5451a01d0d21e1eabbd37a175065f988c402f63af8e11
7
- data.tar.gz: 35e5a24aa4475cd027cc2111f64fba44510aeac35040ea0d13e84f64bbc6f4e3df45120ec9101499cf13c9b1676036a9dcd6e3f8a6b4fb58416f6311091a9ea0
6
+ metadata.gz: 590a21ed0061f1a29fc294f5600ce409c2f0c762f481373a42552559c20f5f8ea2149b213afe38c3dfae46a532f180ab8853aaf65dc96a338283f04291973eec
7
+ data.tar.gz: ad4a62998ce1d912205c118c8403cf5ef008a29e60427871fd75b595631b5ab4309460e6ebd4619459cd9b028e10d64daf98c2190cd52ef724c0c1c7ecffce2f
@@ -1 +1,3 @@
1
1
  app/assets/javascripts/marty/codemirror/*
2
+ !.schemalintrc.js
3
+ !.eslintrc.js
@@ -1,26 +1,31 @@
1
1
  module.exports = {
2
2
  env: {
3
3
  browser: true,
4
- es6: true,
4
+ es6: true
5
5
  },
6
6
  extends: ["eslint:recommended", "prettier"],
7
7
  globals: {
8
8
  RailsApp: "writable",
9
9
  ActionCable: "readonly",
10
10
  Ext: "readonly",
11
- CodeMirror: "readonly"
11
+ CodeMirror: "readonly",
12
+ module: "writable",
13
+ process: "readonly"
12
14
  },
13
15
  parserOptions: {
14
- ecmaVersion: 6,
16
+ ecmaVersion: 6
15
17
  },
16
18
  plugins: ["prettier"],
17
19
  rules: {
18
20
  "no-var": ["error"],
19
21
  "prefer-const": ["error"],
20
22
  "linebreak-style": ["error", "unix"],
21
- "quotes": [2, "double", { "avoidEscape": true }],
22
- "no-unused-vars": ["error", { "args": "after-used", "argsIgnorePattern": "^_" }],
23
+ "quotes": [2, "double", { avoidEscape: true }],
24
+ "no-unused-vars": [
25
+ "error",
26
+ { args: "after-used", argsIgnorePattern: "^_" }
27
+ ],
23
28
  "object-shorthand": ["error", "always"],
24
- "no-constant-condition": ["error", { "checkLoops": false }]
29
+ "no-constant-condition": ["error", { checkLoops: false }]
25
30
  }
26
31
  };
data/.gitignore CHANGED
@@ -26,6 +26,7 @@ spec/dummy/.sass-cache
26
26
  *.swp
27
27
 
28
28
  /spec/dummy/public/extjs
29
+ /spec/dummy/public/assets
29
30
  .rspec
30
31
  .rspec-results
31
32
 
@@ -87,3 +87,18 @@ Rails/ApplicationController:
87
87
 
88
88
  Rails/IndexWith:
89
89
  Enabled: false
90
+
91
+ Lint/RaiseException:
92
+ Enabled: false
93
+
94
+ Lint/StructNewOverride:
95
+ Enabled: false
96
+
97
+ Style/HashEachMethods:
98
+ Enabled: false
99
+
100
+ Style/HashTransformKeys:
101
+ Enabled: false
102
+
103
+ Style/HashTransformValues:
104
+ Enabled: false
@@ -0,0 +1,41 @@
1
+ module.exports = {
2
+ connection: {
3
+ host: process.env["POSTGRES_HOST"] || "localhost",
4
+ user: process.env["POSTGRES_USER"] || "postgres",
5
+ password: process.env["POSTGRES_PASSWORD"] || "postgres",
6
+ database: process.env["POSTGRES_DB_NAME"] || "marty_dev",
7
+ charset: "utf8"
8
+ },
9
+
10
+ // plugins: ['./custom-rules'],
11
+
12
+ rules: {
13
+ "name-casing": ["error", "snake"],
14
+ "name-inflection": ["error", "plural"],
15
+ "prefer-jsonb-to-json": ["error"]
16
+ // FIXME: user varchar with no size limit instead
17
+ // We would need to update lib so it would return column size info
18
+ // And create our own rule that checks that
19
+ // "prefer-text-to-varchar": ["error"]
20
+ },
21
+
22
+ schemas: [{ name: "public" }],
23
+
24
+ ignores: [
25
+ {
26
+ identifierPattern: ".*_rules.computed_guards.*",
27
+ rulePattern: "prefer-jsonb-to-json"
28
+ },
29
+ {
30
+ identifierPattern: ".*_rules.results.*",
31
+ rulePattern: "prefer-jsonb-to-json"
32
+ },
33
+ { identifierPattern: ".*ar_internal_metadata.*", rulePattern: ".*" },
34
+ { identifierPattern: ".*schema_migrations.*", rulePattern: ".*" },
35
+ { identifierPattern: ".*gemini_.*", rulePattern: ".*" },
36
+ { identifierPattern: "entities.*", rulePattern: ".*" },
37
+ { identifierPattern: "groupings.*", rulePattern: ".*" },
38
+ { identifierPattern: "heads.*", rulePattern: ".*" },
39
+ { identifierPattern: "head_versions.*", rulePattern: ".*" }
40
+ ]
41
+ };
@@ -0,0 +1,12 @@
1
+ 13.0.2 - 2020-04-14
2
+ =================
3
+ * Use VARCHAR without size limit instead of recently added TEXT columns, since default field for TEXT column is textarea.
4
+
5
+ 13.0.1 - 2020-04-14
6
+ =================
7
+ * Add missing index by `posting_type` for `marty_postings` table.
8
+
9
+ 12.0.0 - 2020-04-02
10
+ =================
11
+ * Marty::PostingType converted to PgEnum. AR methods (find_by, all, etc...) will no longer work.
12
+
@@ -5,6 +5,7 @@ module Marty
5
5
  Rails.application.config.marty.enable_action_cable
6
6
 
7
7
  reject && return if current_user.blank?
8
+
8
9
  stream_from "marty_notifications_#{current_user.id}"
9
10
  end
10
11
  end
@@ -31,17 +31,23 @@ module Marty
31
31
 
32
32
  c.model = 'Marty::Posting'
33
33
  c.items = [
34
- {
35
- name: :posting_type__name,
36
- scope: lambda { |r|
37
- r.where(name: Marty::Postings::NewForm.can_perform_actions)
38
- },
39
- },
34
+ :posting_type,
40
35
  :comment,
41
36
  :summary_grid
42
37
  ]
43
38
  end
44
39
 
40
+ attribute :posting_type do |c|
41
+ store = Marty::Postings::NewForm.can_perform_actions
42
+
43
+ c.editor_config = {
44
+ multi_select: false,
45
+ store: store,
46
+ type: :string,
47
+ xtype: :combo,
48
+ }
49
+ end
50
+
45
51
  component :summary_grid do |c|
46
52
  c.klass = Marty::Postings::SummaryGrid
47
53
  c.data_store = { auto_load: false }
@@ -7,7 +7,7 @@
7
7
  initComponent() {
8
8
  this.callParent();
9
9
 
10
- const postingType = this.getForm().findField("posting_type__name");
10
+ const postingType = this.getForm().findField("posting_type");
11
11
  const me = this;
12
12
 
13
13
  me.serverConfig.selected_posting_type = null;
@@ -41,13 +41,13 @@ module Marty
41
41
  return [] if config[:selected_posting_type].to_i.negative?
42
42
 
43
43
  last_posting = Marty::Posting.where(
44
- posting_type_id: config[:selected_posting_type]
44
+ posting_type: config[:selected_posting_type]
45
45
  ).where.not(created_dt: 'infinity').order(:created_dt).last
46
46
 
47
47
  start_dt = last_posting&.created_dt || 1.year.ago
48
48
  end_dt = Time.zone.now
49
49
 
50
- posting_type = Marty::PostingType.find(config[:selected_posting_type])
50
+ posting_type = Marty::PostingType[config[:selected_posting_type]]
51
51
 
52
52
  summary_records = class_list(posting_type).map do |klass|
53
53
  summary = Marty::DataChange.change_summary(start_dt, end_dt, klass)
@@ -69,7 +69,7 @@ module Marty
69
69
  end
70
70
 
71
71
  def class_list(posting_type)
72
- method_name = "class_list_#{posting_type.name}".downcase.to_sym
72
+ method_name = "class_list_#{posting_type}".downcase.to_sym
73
73
 
74
74
  if Marty::DataChange.respond_to?(method_name)
75
75
  Marty::DataChange.send(method_name)
@@ -1,4 +1,21 @@
1
1
  module Marty
2
2
  module ApplicationHelper
3
+ DEFAULT_ASSETS_PATH = 'app/assets'
4
+
5
+ def asset_exists?(file, file_extension, default_path)
6
+ path = Rails.configuration.marty.send("assets_#{file_extension}_path") ||
7
+ default_path
8
+
9
+ asset_path = Rails.root.join("#{path}/#{file}.#{file_extension}")
10
+ File.exist?(asset_path)
11
+ end
12
+
13
+ def javascript_exists?(file)
14
+ asset_exists?(file, :js, DEFAULT_ASSETS_PATH + '/javascript')
15
+ end
16
+
17
+ def stylesheet_exists?(file)
18
+ asset_exists?(file, :css, DEFAULT_ASSETS_PATH + '/stylesheets')
19
+ end
3
20
  end
4
21
  end
@@ -43,7 +43,7 @@ class Marty::CronJob < ActiveJob::Base
43
43
  def schedule(schedule_obj:)
44
44
  dj = schedule_obj.delayed_job
45
45
 
46
- return reschedule_obj(schedule_obj: schedule_obj) if dj.present?
46
+ return reschedule(schedule_obj: schedule_obj) if dj.present?
47
47
 
48
48
  cron = schedule_obj.cron
49
49
 
@@ -57,7 +57,8 @@ class Marty::CronJob < ActiveJob::Base
57
57
  return dj.update(cron: schedule_obj.cron) if dj.locked_by?
58
58
 
59
59
  remove(dj)
60
- schedule(schedule_obj: schedule_obj)
60
+ set(cron: schedule_obj.cron, schedule_id: schedule_obj.id).
61
+ perform_later(*schedule_obj.arguments)
61
62
  end
62
63
 
63
64
  def remove(dj)
@@ -2,10 +2,9 @@ class Marty::Posting < Marty::Base
2
2
  has_mcfly append_only: true
3
3
 
4
4
  mcfly_validates_uniqueness_of :name
5
- validates :name, :posting_type_id, :comment, presence: true
5
+ validates :name, :posting_type, :comment, presence: true
6
6
 
7
7
  belongs_to :user, class_name: 'Marty::User'
8
- belongs_to :posting_type
9
8
 
10
9
  def self.make_name(posting_type, dt)
11
10
  return 'NOW' if Mcfly.is_infinity(dt)
@@ -17,19 +16,17 @@ class Marty::Posting < Marty::Base
17
16
  # of using the host's timezone. i.e. since we're in PST8PDT, names
18
17
  # will be based off of the Pacific TZ.
19
18
  dt ||= Time.zone.now
20
- "#{posting_type.name}-#{dt.strftime('%Y%m%d-%H%M')}"
19
+ "#{posting_type}-#{dt.strftime('%Y%m%d-%H%M')}"
21
20
  end
22
21
 
23
22
  before_validation :set_posting_name
23
+
24
24
  def set_posting_name
25
- posting_type = Marty::PostingType.find_by(id: posting_type_id)
26
25
  self.name = self.class.make_name(posting_type, created_dt)
27
26
  true
28
27
  end
29
28
 
30
- def self.do_create(type_name, dt, comment)
31
- posting_type = Marty::PostingType.find_by(name: type_name)
32
-
29
+ def self.do_create(posting_type, dt, comment)
33
30
  raise "unknown posting type #{name}" unless posting_type
34
31
 
35
32
  o = new
@@ -60,10 +57,10 @@ class Marty::Posting < Marty::Base
60
57
 
61
58
  delorean_fn :first_match, sig: [1, 2] do |dt, posting_type = nil|
62
59
  raise 'bad posting type' if
63
- posting_type && !posting_type.is_a?(Marty::PostingType)
60
+ posting_type && !posting_type[posting_type]
64
61
 
65
62
  q = where('created_dt <= ?', dt)
66
- q = q.where(posting_type_id: posting_type.id) if posting_type
63
+ q = q.where(posting_type: posting_type) if posting_type
67
64
  q.order('created_dt DESC').first&.attributes
68
65
  end
69
66
 
@@ -71,10 +68,11 @@ class Marty::Posting < Marty::Base
71
68
  raise 'missing posting types list' unless posting_types
72
69
  raise 'bad posting types list' unless posting_types.is_a?(Array)
73
70
 
74
- q = joins(:posting_type).where("created_dt <> 'infinity'").
75
- where(marty_posting_types: { name: posting_types }).
71
+ q = where("created_dt <> 'infinity'").
72
+ where(posting_type: posting_types).
76
73
  select(get_struct_attrs).
77
74
  order('created_dt DESC').limit(limit || 1)
75
+
78
76
  q.map(&:attributes)
79
77
  end
80
78
  end
@@ -1,6 +1,5 @@
1
1
  class Marty::PostingType < Marty::Base
2
- extend Marty::Enum
2
+ extend Marty::PgEnum
3
3
 
4
- validates :name, presence: true
5
- validates :name, uniqueness: true
4
+ VALUES = ['BASE']
6
5
  end
@@ -44,8 +44,12 @@ class Marty::Promise < Marty::Base
44
44
  # log "SETRES #{Process.pid} #{self}"
45
45
 
46
46
  reload
47
+ # If exception happened before the promise was started
48
+ # we should still update the record
49
+ if res['error'].present? && !start_dt
50
+ self.start_dt ||= DateTime.now
47
51
  # promise must have been started and not yet ended
48
- if !start_dt || end_dt || result != {}
52
+ elsif !start_dt || end_dt || result != {}
49
53
  # log "SETERR #{Process.pid} #{self}"
50
54
  Marty::Util.logger.error("unexpected promise state: #{self}")
51
55
  return
@@ -136,7 +140,10 @@ class Marty::Promise < Marty::Base
136
140
  work_off_job(job)
137
141
  rescue StandardError => e
138
142
  # log "OFFERR #{exc}"
139
- error = exception_to_result(e)
143
+ error = self.class.exception_to_result(
144
+ promise: self,
145
+ exception: e
146
+ )
140
147
  last.set_result(error)
141
148
  end
142
149
  # log "OFF1 #{Process.pid} #{last}"
@@ -1,3 +1,5 @@
1
+ <% cookies[:dark_mode] ||= Rails.configuration.marty.dark_mode_default.to_s %>
2
+
1
3
  <!DOCTYPE html>
2
4
  <html>
3
5
  <head>
@@ -10,6 +12,8 @@
10
12
  <%= csrf_meta_tag %>
11
13
  <%= javascript_include_tag 'marty/application' %>
12
14
  <%= stylesheet_link_tag 'marty/application' %>
15
+ <%= javascript_include_tag 'application' if javascript_exists?('application') %>
16
+ <%= stylesheet_link_tag 'application' if stylesheet_exists?('application') %>
13
17
  <% if cookies[:dark_mode] == 'true' %>
14
18
  <%= stylesheet_link_tag 'marty/dark_mode', media: 'all', 'data-turbolinks-track': 'reload' %>
15
19
  <% end %>
@@ -0,0 +1,61 @@
1
+ class UsePgEnumForPostingTypes < ActiveRecord::Migration[5.1]
2
+ include Marty::Migrations
3
+
4
+ def up
5
+ disable_triggers 'marty_postings' do
6
+ posting_types = Marty::PostingType.all.to_a
7
+ rename_table :marty_posting_types, :marty_posting_types_old
8
+
9
+ new_enum(Marty::PostingType, 'keep_marty_prefix_here')
10
+ add_column :marty_postings, :posting_type, :marty_posting_types
11
+
12
+ posting_types.each do |posting_type|
13
+ Marty::Posting.where(posting_type_id: posting_type.id).update_all(posting_type: posting_type.name)
14
+ end
15
+
16
+ update_views_up
17
+ remove_column :marty_postings, :posting_type_id
18
+
19
+ drop_table :marty_posting_types_old
20
+ change_column_null :marty_postings, :posting_type, false
21
+ end
22
+ end
23
+
24
+ def down
25
+ disable_triggers 'marty_postings' do
26
+ posting_types = Marty::Posting.pluck(:posting_type).uniq
27
+
28
+ execute <<-SQL
29
+ ALTER TYPE marty_posting_types RENAME TO marty_posting_types_old;
30
+ SQL
31
+
32
+ create_table :marty_posting_types do |t|
33
+ t.string :name, null: false, limit: 255
34
+ end
35
+
36
+ add_column :marty_postings, :posting_type_id, :integer
37
+
38
+ posting_types.each do |posting_type|
39
+ new_record = Marty::PostingType.create!(name: posting_type)
40
+ Marty::Posting.where('posting_type = ?', posting_type).update_all(posting_type_id: new_record.id)
41
+ end
42
+
43
+ update_views_down
44
+ remove_column :marty_postings, :posting_type
45
+
46
+ execute <<-SQL
47
+ DROP TYPE marty_posting_types_old
48
+ SQL
49
+
50
+ change_column_null :marty_postings, :posting_type_id, false
51
+ end
52
+ end
53
+
54
+ def update_views_up
55
+ # Add your code here
56
+ end
57
+
58
+ def update_views_down
59
+ # Add your code here
60
+ end
61
+ end
@@ -0,0 +1,89 @@
1
+ class ReplaceVarcharsWithText < ActiveRecord::Migration[5.1]
2
+ def up
3
+ drop_views
4
+ execute('
5
+ ALTER TABLE "delayed_jobs" ALTER COLUMN "locked_by" TYPE TEXT;
6
+ ALTER TABLE "delayed_jobs" ALTER COLUMN "queue" TYPE TEXT;
7
+ ALTER TABLE "delayed_jobs" ALTER COLUMN "cron" TYPE TEXT;
8
+ ALTER TABLE "marty_api_auths" ALTER COLUMN "app_name" TYPE TEXT;
9
+ ALTER TABLE "marty_api_auths" ALTER COLUMN "api_key" TYPE TEXT;
10
+ ALTER TABLE "marty_api_auths" ALTER COLUMN "script_name" TYPE TEXT;
11
+ ALTER TABLE "marty_api_configs" ALTER COLUMN "script" TYPE TEXT;
12
+ ALTER TABLE "marty_api_configs" ALTER COLUMN "node" TYPE TEXT;
13
+ ALTER TABLE "marty_api_configs" ALTER COLUMN "attr" TYPE TEXT;
14
+ ALTER TABLE "marty_api_configs" ALTER COLUMN "api_class" TYPE TEXT;
15
+ ALTER TABLE "marty_background_job_logs" ALTER COLUMN "job_class" TYPE TEXT;
16
+ ALTER TABLE "marty_background_job_logs" ALTER COLUMN "status" TYPE TEXT;
17
+ ALTER TABLE "marty_background_job_schedules" ALTER COLUMN "job_class" TYPE TEXT;
18
+ ALTER TABLE "marty_background_job_schedules" ALTER COLUMN "cron" TYPE TEXT;
19
+ ALTER TABLE "marty_background_job_schedules" ALTER COLUMN "state" TYPE TEXT;
20
+ ALTER TABLE "marty_configs" ALTER COLUMN "key" TYPE TEXT;
21
+ ALTER TABLE "marty_data_grids" ALTER COLUMN "name" TYPE TEXT;
22
+ ALTER TABLE "marty_data_grids" ALTER COLUMN "data_type" TYPE TEXT;
23
+ ALTER TABLE "marty_data_grids" ALTER COLUMN "constraint" TYPE TEXT;
24
+ ALTER TABLE "marty_grid_index_booleans" ALTER COLUMN "attr" TYPE TEXT;
25
+ ALTER TABLE "marty_grid_index_int4ranges" ALTER COLUMN "attr" TYPE TEXT;
26
+ ALTER TABLE "marty_grid_index_integers" ALTER COLUMN "attr" TYPE TEXT;
27
+ ALTER TABLE "marty_grid_index_numranges" ALTER COLUMN "attr" TYPE TEXT;
28
+ ALTER TABLE "marty_grid_index_strings" ALTER COLUMN "attr" TYPE TEXT;
29
+ ALTER TABLE "marty_import_types" ALTER COLUMN "name" TYPE TEXT;
30
+ ALTER TABLE "marty_import_types" ALTER COLUMN "db_model_name" TYPE TEXT;
31
+ ALTER TABLE "marty_import_types" ALTER COLUMN "synonym_fields" TYPE TEXT;
32
+ ALTER TABLE "marty_import_types" ALTER COLUMN "cleaner_function" TYPE TEXT;
33
+ ALTER TABLE "marty_import_types" ALTER COLUMN "validation_function" TYPE TEXT;
34
+ ALTER TABLE "marty_import_types" ALTER COLUMN "preprocess_function" TYPE TEXT;
35
+ ALTER TABLE "marty_logs" ALTER COLUMN "message_type" TYPE TEXT;
36
+ ALTER TABLE "marty_logs" ALTER COLUMN "message" TYPE TEXT;
37
+ ALTER TABLE "marty_notifications" ALTER COLUMN "state" TYPE TEXT;
38
+ ALTER TABLE "marty_notifications_configs" ALTER COLUMN "delivery_type" TYPE TEXT;
39
+ ALTER TABLE "marty_notifications_configs" ALTER COLUMN "state" TYPE TEXT;
40
+ ALTER TABLE "marty_notifications_deliveries" ALTER COLUMN "delivery_type" TYPE TEXT;
41
+ ALTER TABLE "marty_notifications_deliveries" ALTER COLUMN "state" TYPE TEXT;
42
+ ALTER TABLE "marty_notifications_deliveries" ALTER COLUMN "error_text" TYPE TEXT;
43
+ ALTER TABLE "marty_postings" ALTER COLUMN "name" TYPE TEXT;
44
+ ALTER TABLE "marty_postings" ALTER COLUMN "comment" TYPE TEXT;
45
+ ALTER TABLE "marty_promises" ALTER COLUMN "title" TYPE TEXT;
46
+ ALTER TABLE "marty_promises" ALTER COLUMN "cformat" TYPE TEXT;
47
+ ALTER TABLE "marty_scripts" ALTER COLUMN "name" TYPE TEXT;
48
+ ALTER TABLE "marty_tags" ALTER COLUMN "name" TYPE TEXT;
49
+ ALTER TABLE "marty_tags" ALTER COLUMN "comment" TYPE TEXT;
50
+ ALTER TABLE "marty_tokens" ALTER COLUMN "value" TYPE TEXT;
51
+ ALTER TABLE "marty_users" ALTER COLUMN "login" TYPE TEXT;
52
+ ALTER TABLE "marty_users" ALTER COLUMN "firstname" TYPE TEXT;
53
+ ALTER TABLE "marty_users" ALTER COLUMN "lastname" TYPE TEXT;
54
+ ')
55
+ recreate_views
56
+ end
57
+
58
+ def down
59
+ announce("No-op on ReplaceVarcharsWithText.down")
60
+ end
61
+
62
+ def drop_views
63
+ execute <<SQL
64
+ DROP VIEW IF EXISTS marty_vw_promises;
65
+ SQL
66
+ end
67
+
68
+ def recreate_views
69
+ execute <<SQL
70
+ CREATE OR REPLACE VIEW marty_vw_promises
71
+ AS
72
+ SELECT
73
+ id,
74
+ title,
75
+ user_id,
76
+ cformat,
77
+ parent_id,
78
+ job_id,
79
+ status,
80
+ start_dt,
81
+ end_dt,
82
+ priority,
83
+ timeout
84
+ FROM marty_promises;
85
+
86
+ GRANT SELECT ON marty_vw_promises TO public;
87
+ SQL
88
+ end
89
+ end
@@ -0,0 +1,7 @@
1
+ class AddPostingTypeIndexToPostings < ActiveRecord::Migration[5.1]
2
+ include Marty::Migrations
3
+
4
+ def change
5
+ add_index :marty_postings, :posting_type
6
+ end
7
+ end
@@ -0,0 +1,89 @@
1
+ class ReplaceTextWithVarcharsWithoutSizeLimit < ActiveRecord::Migration[5.1]
2
+ def up
3
+ drop_views
4
+ execute <<SQL
5
+ ALTER TABLE "delayed_jobs" ALTER COLUMN "locked_by" TYPE VARCHAR;
6
+ ALTER TABLE "delayed_jobs" ALTER COLUMN "queue" TYPE VARCHAR;
7
+ ALTER TABLE "delayed_jobs" ALTER COLUMN "cron" TYPE VARCHAR;
8
+ ALTER TABLE "marty_api_auths" ALTER COLUMN "app_name" TYPE VARCHAR;
9
+ ALTER TABLE "marty_api_auths" ALTER COLUMN "api_key" TYPE VARCHAR;
10
+ ALTER TABLE "marty_api_auths" ALTER COLUMN "script_name" TYPE VARCHAR;
11
+ ALTER TABLE "marty_api_configs" ALTER COLUMN "script" TYPE VARCHAR;
12
+ ALTER TABLE "marty_api_configs" ALTER COLUMN "node" TYPE VARCHAR;
13
+ ALTER TABLE "marty_api_configs" ALTER COLUMN "attr" TYPE VARCHAR;
14
+ ALTER TABLE "marty_api_configs" ALTER COLUMN "api_class" TYPE VARCHAR;
15
+ ALTER TABLE "marty_background_job_logs" ALTER COLUMN "job_class" TYPE VARCHAR;
16
+ ALTER TABLE "marty_background_job_logs" ALTER COLUMN "status" TYPE VARCHAR;
17
+ ALTER TABLE "marty_background_job_schedules" ALTER COLUMN "job_class" TYPE VARCHAR;
18
+ ALTER TABLE "marty_background_job_schedules" ALTER COLUMN "cron" TYPE VARCHAR;
19
+ ALTER TABLE "marty_background_job_schedules" ALTER COLUMN "state" TYPE VARCHAR;
20
+ ALTER TABLE "marty_configs" ALTER COLUMN "key" TYPE VARCHAR;
21
+ ALTER TABLE "marty_data_grids" ALTER COLUMN "name" TYPE VARCHAR;
22
+ ALTER TABLE "marty_data_grids" ALTER COLUMN "data_type" TYPE VARCHAR;
23
+ ALTER TABLE "marty_data_grids" ALTER COLUMN "constraint" TYPE VARCHAR;
24
+ ALTER TABLE "marty_grid_index_booleans" ALTER COLUMN "attr" TYPE VARCHAR;
25
+ ALTER TABLE "marty_grid_index_int4ranges" ALTER COLUMN "attr" TYPE VARCHAR;
26
+ ALTER TABLE "marty_grid_index_integers" ALTER COLUMN "attr" TYPE VARCHAR;
27
+ ALTER TABLE "marty_grid_index_numranges" ALTER COLUMN "attr" TYPE VARCHAR;
28
+ ALTER TABLE "marty_grid_index_strings" ALTER COLUMN "attr" TYPE VARCHAR;
29
+ ALTER TABLE "marty_import_types" ALTER COLUMN "name" TYPE VARCHAR;
30
+ ALTER TABLE "marty_import_types" ALTER COLUMN "db_model_name" TYPE VARCHAR;
31
+ ALTER TABLE "marty_import_types" ALTER COLUMN "synonym_fields" TYPE VARCHAR;
32
+ ALTER TABLE "marty_import_types" ALTER COLUMN "cleaner_function" TYPE VARCHAR;
33
+ ALTER TABLE "marty_import_types" ALTER COLUMN "validation_function" TYPE VARCHAR;
34
+ ALTER TABLE "marty_import_types" ALTER COLUMN "preprocess_function" TYPE VARCHAR;
35
+ ALTER TABLE "marty_logs" ALTER COLUMN "message_type" TYPE VARCHAR;
36
+ ALTER TABLE "marty_logs" ALTER COLUMN "message" TYPE VARCHAR;
37
+ ALTER TABLE "marty_notifications" ALTER COLUMN "state" TYPE VARCHAR;
38
+ ALTER TABLE "marty_notifications_configs" ALTER COLUMN "delivery_type" TYPE VARCHAR;
39
+ ALTER TABLE "marty_notifications_configs" ALTER COLUMN "state" TYPE VARCHAR;
40
+ ALTER TABLE "marty_notifications_deliveries" ALTER COLUMN "delivery_type" TYPE VARCHAR;
41
+ ALTER TABLE "marty_notifications_deliveries" ALTER COLUMN "state" TYPE VARCHAR;
42
+ ALTER TABLE "marty_notifications_deliveries" ALTER COLUMN "error_text" TYPE VARCHAR;
43
+ ALTER TABLE "marty_postings" ALTER COLUMN "name" TYPE VARCHAR;
44
+ ALTER TABLE "marty_postings" ALTER COLUMN "comment" TYPE VARCHAR;
45
+ ALTER TABLE "marty_promises" ALTER COLUMN "title" TYPE VARCHAR;
46
+ ALTER TABLE "marty_promises" ALTER COLUMN "cformat" TYPE VARCHAR;
47
+ ALTER TABLE "marty_scripts" ALTER COLUMN "name" TYPE VARCHAR;
48
+ ALTER TABLE "marty_tags" ALTER COLUMN "name" TYPE VARCHAR;
49
+ ALTER TABLE "marty_tags" ALTER COLUMN "comment" TYPE VARCHAR;
50
+ ALTER TABLE "marty_tokens" ALTER COLUMN "value" TYPE VARCHAR;
51
+ ALTER TABLE "marty_users" ALTER COLUMN "login" TYPE VARCHAR;
52
+ ALTER TABLE "marty_users" ALTER COLUMN "firstname" TYPE VARCHAR;
53
+ ALTER TABLE "marty_users" ALTER COLUMN "lastname" TYPE VARCHAR;
54
+ SQL
55
+ recreate_views
56
+ end
57
+
58
+ def down
59
+ announce("No-op on ReplaceVarcharsWithText.down")
60
+ end
61
+
62
+ def drop_views
63
+ execute <<SQL
64
+ DROP VIEW IF EXISTS marty_vw_promises;
65
+ SQL
66
+ end
67
+
68
+ def recreate_views
69
+ execute <<SQL
70
+ CREATE OR REPLACE VIEW marty_vw_promises
71
+ AS
72
+ SELECT
73
+ id,
74
+ title,
75
+ user_id,
76
+ cformat,
77
+ parent_id,
78
+ job_id,
79
+ status,
80
+ start_dt,
81
+ end_dt,
82
+ priority,
83
+ timeout
84
+ FROM marty_promises;
85
+
86
+ GRANT SELECT ON marty_vw_promises TO public;
87
+ SQL
88
+ end
89
+ end
@@ -1,5 +1,6 @@
1
1
  # create system account if not there
2
2
  system_login = Rails.configuration.marty.system_account || 'marty'
3
+
3
4
  unless Marty::User.find_by_login(system_login)
4
5
  user = Marty::User.new
5
6
  user.login = system_login
@@ -13,22 +14,20 @@ end
13
14
  Mcfly.whodunnit = Marty::User.find_by_login(system_login)
14
15
 
15
16
  # Give system account all roles
16
- Marty::RoleType.get_all.map { |role|
17
+ Marty::RoleType.get_all.map do |role|
17
18
  ur = Marty::UserRole.new
18
19
  ur.user = Mcfly.whodunnit
19
20
  ur.role = role
20
21
  ur.save
21
- }
22
+ end
22
23
 
23
24
  # Create default PostingType from configuration
24
25
  default_p_type = Rails.configuration.marty.default_posting_type
25
26
 
26
- Marty::PostingType.create(name: default_p_type)
27
-
28
27
  # Create NOW posting
29
28
  unless Marty::Posting.find_by_name('NOW')
30
29
  sn = Marty::Posting.new
31
- sn.posting_type_id = Marty::PostingType[default_p_type].id
30
+ sn.posting_type = Marty::PostingType[default_p_type]
32
31
  sn.comment = '---'
33
32
  sn.created_dt = 'infinity'
34
33
  sn.save!
@@ -18,6 +18,7 @@ services:
18
18
  - "PGTZ=America/Los_Angeles"
19
19
  - "BUNDLER_VERSION=2.0.1"
20
20
  - "MARTY_REDIS_URL=redis:6379/1"
21
+ - "POSTGRES_HOST=postgres"
21
22
  # env_file: ".rbenv-vars"
22
23
  depends_on:
23
24
  - "postgres"
@@ -16,11 +16,13 @@ class Marty::Aws::Request < Marty::Aws::Base
16
16
  url += '?' + (default + params).map { |a, v| "#{a}=#{v}" }.join('&') unless
17
17
  params.empty?
18
18
 
19
- sig = Aws::Sigv4::Signer.new(service: @service,
20
- region: @doc[:region],
21
- access_key_id: @creds[:access_key_id],
22
- secret_access_key: @creds[:secret_access_key],
23
- session_token: @creds[:token])
19
+ sig = ::Aws::Sigv4::Signer.new(
20
+ service: @service,
21
+ region: @doc[:region],
22
+ access_key_id: @creds[:access_key_id],
23
+ secret_access_key: @creds[:secret_access_key],
24
+ session_token: @creds[:token]
25
+ )
24
26
  signed_url = sig.presign_url(http_method: 'GET', url: url)
25
27
 
26
28
  http = Net::HTTP.new(host, 443)
@@ -11,8 +11,9 @@ module Marty::Diagnostic
11
11
  }
12
12
  end.reduce(&:merge) || {}
13
13
 
14
- git_tag = `cd #{Rails.root}; git describe --tags --always;`.strip
15
- git = { 'Root Git' => git_tag }.merge(submodules)
14
+ git_tag = `cd #{Rails.root}; git describe --tags --always --abbrev=7;`.strip
15
+ git_datetime = `cd #{Rails.root}; git log -1 --format=%cd;`.strip
16
+ git = { 'Root Git' => "#{git_tag} (#{git_datetime})" }.merge(submodules)
16
17
  rescue StandardError
17
18
  git = { 'Root Git' => error('Failed accessing git') }
18
19
  end
@@ -304,6 +304,16 @@ OUT
304
304
  end.map(&:to_sym)
305
305
  end
306
306
 
307
+ def disable_triggers(table_name)
308
+ ActiveRecord::Base.connection.
309
+ execute("ALTER TABLE #{table_name} DISABLE TRIGGER USER;")
310
+
311
+ yield
312
+ ensure
313
+ ActiveRecord::Base.connection.
314
+ execute("ALTER TABLE #{table_name} ENABLE TRIGGER USER;")
315
+ end
316
+
307
317
  def self.get_plv8_migration(file)
308
318
  fnname = %r(/([^/]+)_v[0-9]+\.js\z).match(file)[1]
309
319
  lines = File.readlines(file)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Marty
4
- VERSION = '11.0.0'
4
+ VERSION = '13.0.2'
5
5
  end
@@ -1,6 +1,7 @@
1
1
  lint:
2
2
  make lint-ruby
3
3
  make lint-js
4
+ make lint-schema
4
5
 
5
6
  lint-fix:
6
7
  make lint-ruby-fix
@@ -16,4 +17,7 @@ lint-js:
16
17
  yarn lint
17
18
 
18
19
  lint-js-fix:
19
- yarn lintfix
20
+ yarn lint-fix
21
+
22
+ lint-schema:
23
+ yarn lint-schema
@@ -1,16 +1,21 @@
1
1
  {
2
2
  "private": true,
3
3
  "scripts": {
4
- "lint": "eslint 'app/**/*.{js,jsx}' && prettier --check \"app/**/*.{js,jsx,css,scss}\"",
5
- "lintfix": "eslint --fix 'app/**/*.{js,jsx}' && prettier --write \"app/**/*.{js,jsx,css,scss}\""
6
- },
7
- "dependencies": {
4
+ "eslint-check": "eslint 'app/**/*.{js,jsx}' ./.schemalintrc.js ./.eslintrc.js ./prettier.config.js",
5
+ "eslint-write": "eslint --fix 'app/**/*.{js,jsx}' ./.schemalintrc.js ./.eslintrc.js ./prettier.config.js",
6
+ "prettier-check": "prettier --check \"app/**/*.{js,jsx,css,scss}\" ./.schemalintrc.js ./.eslintrc.js ./prettier.config.js",
7
+ "prettier-write": "prettier --write \"app/**/*.{js,jsx,css,scss}\" ./.schemalintrc.js ./.eslintrc.js ./prettier.config.js",
8
+ "lint": "yarn run eslint-check && yarn run prettier-check",
9
+ "lint-fix": "yarn run eslint-write && yarn run prettier-write",
10
+ "lint-schema": "schemalint"
8
11
  },
12
+ "dependencies": {},
9
13
  "devDependencies": {
10
14
  "babel-eslint": "^10.0.1",
11
15
  "eslint": "^6.0.0",
12
16
  "eslint-config-prettier": "^6.0.0",
13
17
  "eslint-plugin-prettier": "^3.1.0",
14
- "prettier": "^1.17.1"
18
+ "prettier": "^1.17.1",
19
+ "schemalint": "^0.2.2"
15
20
  }
16
21
  }
@@ -14,8 +14,12 @@ module Marty::Diagnostic
14
14
  end
15
15
 
16
16
  def git
17
- `cd #{Rails.root}; git describe --tags --always;`.
18
- strip rescue 'Failed accessing git'
17
+ tag = `cd #{Rails.root}; git describe --tags --always;`.strip
18
+ git_datetime = `cd #{Rails.root}; git log -1 --format=%cd;`.strip
19
+
20
+ "#{tag} (#{git_datetime})"
21
+ rescue StandardError
22
+ 'Failed accessing git'
19
23
  end
20
24
 
21
25
  describe 'GET #op' do
@@ -0,0 +1 @@
1
+ // = link_directory ../javascripts .js
@@ -0,0 +1,14 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
5
+ // vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require rails-ujs
14
+ //= require_tree .
@@ -0,0 +1,14 @@
1
+ class AddPostingTypes < ActiveRecord::Migration[5.2]
2
+ include Marty::Migrations
3
+
4
+ disable_ddl_transaction!
5
+
6
+ def up
7
+ Marty::PostingType::VALUES += [
8
+ 'OTHER',
9
+ 'SNAPSHOT'
10
+ ]
11
+
12
+ update_enum(Marty::PostingType, 'do_not_override_prefix')
13
+ end
14
+ end
@@ -14,11 +14,9 @@ select
14
14
  main.created_dt,
15
15
  main.obsoleted_dt,
16
16
  main.name,
17
- marty_posting_types1.name as posting_type_name,
18
- marty_posting_types1.id as post_type_id
17
+ main.posting_type
19
18
  from marty_postings main
20
19
  join marty_users u on main.user_id = u.id
21
- left join marty_users ou on main.o_user_id = ou.id
22
- left join marty_posting_types marty_posting_types1 on main.posting_type_id = marty_posting_types1.id;
20
+ left join marty_users ou on main.o_user_id = ou.id;
23
21
 
24
22
  grant select on vw_marty_postings to public;
@@ -42,8 +42,6 @@ module Marty
42
42
 
43
43
  context 'when valid parameters are supplied' do
44
44
  before do
45
- PostingType.create(name: 'SNAPSHOT')
46
- PostingType.create(name: 'OTHER')
47
45
  Posting.do_create('BASE', 0.days.from_now, 'base posting')
48
46
  Posting.do_create('SNAPSHOT', 1.day.from_now, 'snapshot1 posting')
49
47
  Posting.do_create('SNAPSHOT', 2.days.from_now, 'snapshot2 posting')
@@ -362,6 +362,39 @@ describe Marty::Promise, slow: true, retry: 3 do
362
362
  expect(promise.result['error']).to eq 'Something went wrong'
363
363
  expect(promise.result['backtrace']).to_not be_empty
364
364
  end
365
+
366
+ describe 'without DJs' do
367
+ before do
368
+ stop_delayed_job
369
+ end
370
+
371
+ after do
372
+ start_delayed_job
373
+ end
374
+
375
+ it 'fails on exception' do
376
+ Marty::Promises::Ruby::Create.call(
377
+ module_name: 'Gemini::BudCategory',
378
+ method_name: 'create_from_promise_error',
379
+ method_args: [],
380
+ params: {
381
+ _user_id: user.id,
382
+ }
383
+ )
384
+
385
+ promise = Marty::Promise.where(promise_type: 'ruby').last
386
+ # Simulate exception outside of the job
387
+ expect(promise).to receive(:work_off_job).once.and_raise 'Test exception'
388
+
389
+ promise.wait_for_result(Marty::Promise::DEFAULT_PROMISE_TIMEOUT)
390
+ promise.reload
391
+
392
+ expect(promise.status).to be false
393
+ expect(promise.promise_type).to eq 'ruby'
394
+ expect(promise.result['error']).to eq 'Test exception'
395
+ expect(promise.result['backtrace']).to_not be_empty
396
+ end
397
+ end
365
398
  end
366
399
 
367
400
  describe 'priority' do
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ module Marty
4
+ describe BackgroundJob::UpdateSchedule do
5
+ before(:each) do
6
+ @schedule = Marty::BackgroundJob::Schedule.create!(
7
+ job_class: 'TestJob',
8
+ cron: '0 0 * * *',
9
+ state: 'on'
10
+ )
11
+ Marty::Jobs::Schedule.call
12
+ end
13
+ context '.call' do
14
+ let(:new_cron) { '1 0 * * *' }
15
+ it 'updates a Delayed::Job cron when cron is updated to on' do
16
+ @schedule.update(cron: new_cron)
17
+ described_class.call(
18
+ id: @schedule.id,
19
+ job_class: @schedule.job_class
20
+ )
21
+ expect(@schedule.reload.delayed_job.cron).to eq(new_cron)
22
+ end
23
+
24
+ it 'does not update Delayed::Job cron when cron is updated to off' do
25
+ @schedule.update(cron: new_cron, state: 'off')
26
+ described_class.call(
27
+ id: @schedule.id,
28
+ job_class: @schedule.job_class
29
+ )
30
+ expect(@schedule.reload.delayed_job).to be_nil
31
+ end
32
+ end
33
+ end
34
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: marty
3
3
  version: !ruby/object:Gem::Version
4
- version: 11.0.0
4
+ version: 13.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arman Bostani
@@ -14,7 +14,7 @@ authors:
14
14
  autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
- date: 2020-03-31 00:00:00.000000000 Z
17
+ date: 2020-04-14 00:00:00.000000000 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: actioncable
@@ -283,8 +283,10 @@ files:
283
283
  - ".rspec"
284
284
  - ".rubocop.yml"
285
285
  - ".rubocop_todo.yml"
286
+ - ".schemalintrc.js"
286
287
  - ".ssh-docker/.keep"
287
288
  - ".travis.yml"
289
+ - CHANGELOG.md
288
290
  - Dockerfile.dummy
289
291
  - Gemfile
290
292
  - INDEPENDENCE_ISSUES.md
@@ -521,6 +523,10 @@ files:
521
523
  - db/migrate/524_add_timeout_to_promise_view.rb
522
524
  - db/migrate/525_add_arguments_to_jobs_schedules.rb
523
525
  - db/migrate/526_add_schedule_id_to_delayed_jobs.rb
526
+ - db/migrate/527_use_pg_enum_for_posting_types.rb
527
+ - db/migrate/600_replace_varchars_with_text.rb
528
+ - db/migrate/601_add_posting_type_index_to_postings.rb
529
+ - db/migrate/602_replace_text_with_varchars_without_size_limit.rb
524
530
  - db/seeds.rb
525
531
  - db/sql/lookup_grid_distinct_v1.sql
526
532
  - db/sql/query_grid_dir_v1.sql
@@ -609,6 +615,7 @@ files:
609
615
  - spec/dummy/README.rdoc
610
616
  - spec/dummy/Rakefile
611
617
  - spec/dummy/app/assets/config/manifest.js
618
+ - spec/dummy/app/assets/javascripts/application.js
612
619
  - spec/dummy/app/components/gemini/cm_auth_app.rb
613
620
  - spec/dummy/app/components/gemini/loan_program_view.rb
614
621
  - spec/dummy/app/components/gemini/my_rule_view.rb
@@ -689,6 +696,7 @@ files:
689
696
  - spec/dummy/db/migrate/20190702115241_add_simple_guards_options_to_rules.rb
690
697
  - spec/dummy/db/migrate/20191101132729_add_activity_flag_to_simple.rb
691
698
  - spec/dummy/db/migrate/20191206132729_add_default_true_column_to_simple.rb
699
+ - spec/dummy/db/migrate/20200402150405_add_posting_types.rb
692
700
  - spec/dummy/db/seeds.rb
693
701
  - spec/dummy/delorean/base_code.dl
694
702
  - spec/dummy/delorean/blame_report.dl
@@ -1801,6 +1809,7 @@ files:
1801
1809
  - spec/performance/caching_spec.rb
1802
1810
  - spec/requests/routes_spec.rb
1803
1811
  - spec/services/background_job/fetch_missing_in_schedule_cron_jobs_spec.rb
1812
+ - spec/services/background_job/update_schedule_spec.rb
1804
1813
  - spec/services/jobs/schedule_spec.rb
1805
1814
  - spec/services/notifications/create_spec.rb
1806
1815
  - spec/spec_helper.rb