marty 11.0.0 → 13.0.2

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