naf 1.1.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. data/Gemfile +4 -2
  2. data/app/assets/images/{papertrail_job.png → job.png} +0 -0
  3. data/app/assets/images/{papertrail_machine.png → machine.png} +0 -0
  4. data/app/assets/images/{papertrail_machine_runner.png → machine_runner.png} +0 -0
  5. data/app/assets/javascripts/col_reorder_with_resize.js +1228 -0
  6. data/app/assets/javascripts/dataTablesTemplates/applications.js +2 -1
  7. data/app/assets/javascripts/dataTablesTemplates/jobs.js +2 -1
  8. data/app/assets/javascripts/dataTablesTemplates/machine_runner_invocations.js +2 -1
  9. data/app/assets/javascripts/dataTablesTemplates/machine_runners.js +2 -1
  10. data/app/assets/javascripts/dataTablesTemplates/machines.js +2 -1
  11. data/app/assets/javascripts/jquery.dataTables.js +10339 -5103
  12. data/app/assets/javascripts/naf.js +1 -0
  13. data/app/assets/stylesheets/jquery_ui/jquery-ui-1.8.5.custom.css.erb +6 -6
  14. data/app/assets/stylesheets/min_naf/layout.css.scss +94 -43
  15. data/app/assets/stylesheets/naf/layout.css.scss +94 -43
  16. data/app/controllers/naf/affinities_controller.rb +1 -1
  17. data/app/controllers/naf/applications_controller.rb +3 -0
  18. data/app/controllers/naf/historical_job_affinity_tabs_controller.rb +1 -1
  19. data/app/controllers/naf/historical_jobs_controller.rb +2 -5
  20. data/app/controllers/naf/log_parsers_controller.rb +16 -0
  21. data/app/controllers/naf/log_viewer_controller.rb +19 -0
  22. data/app/controllers/naf/machine_affinity_slots_controller.rb +1 -1
  23. data/app/controllers/naf/machine_runners_controller.rb +12 -0
  24. data/app/controllers/naf/machines_controller.rb +8 -10
  25. data/app/controllers/naf/status_controller.rb +12 -0
  26. data/app/helpers/naf/application_helper.rb +19 -38
  27. data/app/helpers/naf/time_helper.rb +37 -0
  28. data/app/models/logical/naf/application.rb +13 -19
  29. data/app/models/logical/naf/construction_zone/boss.rb +1 -1
  30. data/app/models/logical/naf/construction_zone/foreman.rb +1 -1
  31. data/app/models/logical/naf/job.rb +39 -34
  32. data/app/models/logical/naf/job_creator.rb +19 -23
  33. data/app/models/logical/naf/job_fetcher.rb +36 -6
  34. data/app/models/logical/naf/log_file.rb +70 -0
  35. data/app/models/logical/naf/log_parser/base.rb +272 -0
  36. data/app/models/logical/naf/log_parser/job.rb +65 -0
  37. data/app/models/logical/naf/log_parser/machine.rb +64 -0
  38. data/app/models/logical/naf/log_parser/runner.rb +72 -0
  39. data/app/models/logical/naf/log_reader.rb +85 -0
  40. data/app/models/logical/naf/machine.rb +39 -1
  41. data/app/models/naf/affinity.rb +18 -0
  42. data/app/models/naf/application_schedule_affinity_tab.rb +1 -0
  43. data/app/models/naf/application_type.rb +2 -1
  44. data/app/models/naf/historical_job.rb +9 -29
  45. data/app/models/naf/machine.rb +8 -0
  46. data/app/models/naf/machine_runner.rb +11 -2
  47. data/app/models/naf/machine_runner_invocation.rb +9 -1
  48. data/app/models/naf/running_job.rb +40 -1
  49. data/app/models/process/naf/application.rb +3 -3
  50. data/app/models/process/naf/log_archiver.rb +78 -0
  51. data/app/models/process/naf/machine_manager.rb +3 -1
  52. data/app/models/process/naf/runner.rb +286 -162
  53. data/app/models/process/naf/runner_log.rb +26 -0
  54. data/app/views/naf/application_schedule_affinity_tabs/_form.html.erb +1 -5
  55. data/app/views/naf/applications/show.html.erb +1 -1
  56. data/app/views/naf/historical_job_affinity_tabs/_form.html.erb +1 -5
  57. data/app/views/naf/historical_jobs/_form.html.erb +1 -1
  58. data/app/views/naf/historical_jobs/_runners.html.erb +21 -12
  59. data/app/views/naf/historical_jobs/_search_container.html.erb +1 -2
  60. data/app/views/naf/historical_jobs/index.html.erb +0 -1
  61. data/app/views/naf/historical_jobs/index.json.erb +4 -4
  62. data/app/views/naf/historical_jobs/show.html.erb +57 -51
  63. data/app/views/naf/log_viewer/_job_logs.html.erb +65 -0
  64. data/app/views/naf/log_viewer/_log_display.html.erb +259 -0
  65. data/app/views/naf/log_viewer/_log_layout.html.erb +59 -0
  66. data/app/views/naf/log_viewer/_machine_logs.html.erb +62 -0
  67. data/app/views/naf/log_viewer/_runner_logs.html.erb +62 -0
  68. data/app/views/naf/log_viewer/_search_options.html.erb +36 -0
  69. data/app/views/naf/log_viewer/_update_page_title.html.erb +9 -0
  70. data/app/views/naf/log_viewer/index.html.erb +1 -0
  71. data/app/views/naf/logger_names/_form.html.erb +1 -2
  72. data/app/views/naf/machine_affinity_slots/_form.html.erb +1 -5
  73. data/app/views/naf/machine_runner_invocations/show.html.erb +4 -0
  74. data/app/views/naf/machine_runners/show.html.erb +44 -34
  75. data/app/views/naf/machines/index.json.erb +14 -6
  76. data/app/views/naf/machines/show.html.erb +44 -40
  77. data/app/views/naf/shared/_auto_resize_width.html.erb +7 -0
  78. data/app/views/naf/shared/_date_select.html.erb +65 -0
  79. data/app/views/naf/shared/_select_per_page.html.erb +48 -13
  80. data/app/views/naf/status/index.html.erb +27 -0
  81. data/bin/naf +26 -0
  82. data/config/initializers/naf.rb +13 -1
  83. data/config/routes.rb +16 -2
  84. data/db/migrate/20131106162436_add_uuid_column_to_machine_runner_invocations.rb +15 -0
  85. data/db/migrate/20131121185222_move_tabs_column_from_historical_jobs_to_running_jobs.rb +15 -0
  86. data/lib/generators/templates/config/logging/naf.yml +0 -8
  87. data/lib/generators/templates/config/logging/nafjob.yml +0 -8
  88. data/lib/generators/templates/config/logging/nafrunner.yml +0 -8
  89. data/lib/generators/templates/naf.rb +0 -8
  90. data/lib/naf.rb +0 -8
  91. data/lib/naf/configuration.rb +0 -4
  92. data/lib/naf/version.rb +1 -1
  93. data/lib/tasks/naf_tasks.rake +18 -0
  94. data/naf.gemspec +3 -1
  95. data/spec/controllers/naf/affinities_controller_spec.rb +0 -1
  96. data/spec/controllers/naf/applications_controller_spec.rb +3 -2
  97. data/spec/controllers/naf/machine_affinity_slots_controller_spec.rb +0 -1
  98. data/spec/controllers/naf/machines_controller_spec.rb +1 -1
  99. data/spec/dummy/config/logging/naf.yml +0 -8
  100. data/spec/dummy/config/logging/nafjob.yml +0 -9
  101. data/spec/dummy/config/logging/nafrunner.yml +0 -10
  102. data/spec/factories/naf.rb +4 -0
  103. data/spec/models/logical/naf/application_spec.rb +3 -4
  104. data/spec/models/logical/naf/job_creator_spec.rb +91 -21
  105. data/spec/models/logical/naf/job_spec.rb +19 -6
  106. data/spec/models/logical/naf/log_file_spec.rb +105 -0
  107. data/spec/models/logical/naf/machine_runner_invocation_spec.rb +41 -0
  108. data/spec/models/logical/naf/machine_runner_spec.rb +42 -0
  109. data/spec/models/logical/naf/machine_spec.rb +98 -28
  110. data/spec/models/naf/affinity_classification_spec.rb +20 -0
  111. data/spec/models/naf/affinity_spec.rb +21 -0
  112. data/spec/models/naf/historical_job_spec.rb +2 -44
  113. data/spec/models/naf/machine_runner_invocation_spec.rb +17 -1
  114. data/spec/models/naf/running_job_spec.rb +64 -1
  115. metadata +40 -9
  116. data/app/models/log4r/papertrail_outputter.rb +0 -19
  117. data/app/views/naf/historical_jobs/edit.html.erb +0 -11
  118. data/app/views/naf/machines/_show.html.erb +0 -169
@@ -0,0 +1,65 @@
1
+ require 'yajl'
2
+
3
+ module Logical::Naf
4
+ module LogParser
5
+ class Job < Base
6
+
7
+ def initialize(params)
8
+ super(params)
9
+ end
10
+
11
+ def logs
12
+ retrieve_logs
13
+ end
14
+
15
+ private
16
+
17
+ def insert_log_line(elem)
18
+ "&nbsp;&nbsp;<span>#{elem['line_number']} #{elem['output_time']}: #{elem['message']}</br></span>"
19
+ end
20
+
21
+ def sort_jsons
22
+ # Sort log lines based on timestamp
23
+ @jsons = jsons.sort { |x, y| x['line_number'] <=> y['line_number'] }
24
+ end
25
+
26
+ def parse_newest_log
27
+ if newest_log.scan(/\d+ \d{4}-\d{2}-\d{2}/).present?
28
+ newest_log.slice!(newest_log.split(/ /).first + ' ')
29
+ end
30
+
31
+ if newest_log.scan(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}:/).present?
32
+ date = newest_log.slice!(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}:/)[0..-2]
33
+ @newest_log = date + newest_log
34
+ end
35
+ newest_log
36
+ end
37
+
38
+ def retrieve_log_files_from_s3
39
+ s3_log_reader.retrieve_job_files(record_id)
40
+ end
41
+
42
+ def get_files
43
+ if log_type == 'old' && read_from_s3 == 'true'
44
+ get_s3_files do
45
+ @s3_log_reader = ::Logical::Naf::LogReader.new
46
+ return retrieve_log_files_from_s3
47
+ end
48
+ else
49
+ files = Dir["#{::Naf::PREFIX_PATH}/#{::Naf.schema_name}/jobs/#{record_id}/*"]
50
+ if files.present?
51
+ # Sort log files based on time
52
+ return files.sort { |x, y| Time.parse(y.scan(DATE_REGEX)[0][0]) <=> Time.parse(x.scan(DATE_REGEX)[0][0]) }
53
+ else
54
+ get_s3_files do
55
+ @read_from_s3 = 'true'
56
+ @s3_log_reader = ::Logical::Naf::LogReader.new
57
+ return retrieve_log_files_from_s3
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,64 @@
1
+ require 'yajl'
2
+
3
+ module Logical::Naf
4
+ module LogParser
5
+ class Machine < Base
6
+
7
+ def initialize(params)
8
+ super(params)
9
+ end
10
+
11
+ def logs
12
+ retrieve_logs
13
+ end
14
+
15
+ private
16
+
17
+ def insert_log_line(elem)
18
+ "&nbsp;&nbsp;<span>#{elem['output_time']} <font color='333399'><b>jid(#{elem['job_id']}):</b></font> #{elem['message']}</br></span>"
19
+ end
20
+
21
+ def sort_jsons
22
+ # Sort log lines based on timestamp
23
+ @jsons = jsons.sort { |x, y| Time.parse(x['output_time']) <=> Time.parse(y['output_time']) }
24
+ end
25
+
26
+ def parse_newest_log
27
+ if newest_log.scan(/jid\(\d*\)\: /).present?
28
+ newest_log.slice!(/jid\(\d*\)\: /)
29
+ end
30
+ newest_log
31
+ end
32
+
33
+ def retrieve_log_files_from_s3
34
+ s3_log_reader.log_files
35
+ end
36
+
37
+ def get_files
38
+ if log_type == 'old' && read_from_s3 == 'true'
39
+ get_s3_files do
40
+ @s3_log_reader = ::Logical::Naf::LogReader.new
41
+ return retrieve_log_files_from_s3
42
+ end
43
+ else
44
+ files = Dir["#{::Naf::PREFIX_PATH}/#{::Naf.schema_name}/jobs/*/*"]
45
+ if files.present?
46
+ # Sort log files based on time
47
+ return files.sort { |x, y| Time.parse(y.scan(DATE_REGEX)[0][0]) <=> Time.parse(x.scan(DATE_REGEX)[0][0]) }
48
+ else
49
+ get_s3_files do
50
+ @read_from_s3 = 'true'
51
+ @s3_log_reader = ::Logical::Naf::LogReader.new
52
+ return retrieve_log_files_from_s3
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def get_job_id(file)
59
+ file.scan(/\d+_\d{8}/).first.split('_').first
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,72 @@
1
+ require 'yajl'
2
+
3
+ module Logical::Naf
4
+ module LogParser
5
+ class Runner < Base
6
+
7
+ attr_accessor :invocations_ids
8
+
9
+ def initialize(params)
10
+ super(params)
11
+ @invocations_ids = {}
12
+ end
13
+
14
+ def logs
15
+ retrieve_logs
16
+ end
17
+
18
+ private
19
+
20
+ def insert_log_line(elem)
21
+ "&nbsp;&nbsp;<span>#{elem['output_time']} #{invocation_link(elem['id'])}: #{elem['message']}</br></span>"
22
+ end
23
+
24
+ def invocation_link(id)
25
+ "<a href=\"\/job_system\/machine_runner_invocations\/#{id}\" style=\"font-weight:bold; color: #333399\">invocation(#{id})</a>"
26
+ end
27
+
28
+ def sort_jsons
29
+ # Sort log lines based on timestamp
30
+ @jsons = jsons.sort { |x, y| Time.parse(x['output_time']) <=> Time.parse(y['output_time']) }
31
+ end
32
+
33
+ def parse_newest_log
34
+ "#{newest_log.scan(/\d{4}.*\.\d{3}/).first} #{newest_log.scan(/Process.*/).first.try(:split, '<br>').try(:first)}"
35
+ end
36
+
37
+ def retrieve_log_files_from_s3
38
+ s3_log_reader.runner_log_files(record_id)
39
+ end
40
+
41
+ def get_invocation_id(uuid)
42
+ if invocations_ids[uuid].blank?
43
+ @invocations_ids[uuid] = ::Naf::MachineRunnerInvocation.find_by_uuid(uuid).id
44
+ end
45
+
46
+ invocations_ids[uuid]
47
+ end
48
+
49
+ def get_files
50
+ if log_type == 'old' && read_from_s3 == 'true'
51
+ get_s3_files do
52
+ @s3_log_reader = ::Logical::Naf::LogReader.new
53
+ return s3_log_reader.runner_log_files(record_id)
54
+ end
55
+ else
56
+ files = Dir["#{::Naf::PREFIX_PATH}/#{::Naf.schema_name}/runners/*/*"]
57
+ if files.present?
58
+ # Sort log files based on time
59
+ return files.sort { |x, y| Time.parse(y.scan(DATE_REGEX)[0][0]) <=> Time.parse(x.scan(DATE_REGEX)[0][0]) }
60
+ else
61
+ get_s3_files do
62
+ @read_from_s3 = 'true'
63
+ @s3_log_reader = ::Logical::Naf::LogReader.new
64
+ return retrieve_log_files_from_s3
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,85 @@
1
+ require 'aws'
2
+
3
+ module Logical
4
+ module Naf
5
+ class LogReader
6
+
7
+ DATE_REGEX = /((\d){4}-(\d){2}-(\d){2} (\d){2}:(\d){2}:(\d){2} UTC)/
8
+
9
+ def log_files
10
+ tree = bucket.objects.with_prefix(prefix).as_tree
11
+ directories = tree.children.select(&:branch?).collect(&:prefix).uniq
12
+
13
+ files = []
14
+ directories.each do |directory|
15
+ tree = bucket.objects.with_prefix(directory).as_tree
16
+ tree.children.select(&:leaf?).collect(&:key).each do |file|
17
+ files << file
18
+ end
19
+ end
20
+ return sort_files(files)
21
+ end
22
+
23
+ def runner_log_files(runner_id)
24
+ @runner_id = runner_id
25
+ tree = bucket.objects.with_prefix(prefix).as_tree
26
+ directories = tree.children.select(&:branch?).collect(&:prefix).uniq
27
+
28
+ files = []
29
+ directories.each do |directory|
30
+ tree = bucket.objects.with_prefix(directory).as_tree
31
+ tree.children.select(&:leaf?).collect(&:key).each do |file|
32
+ files << file
33
+ end
34
+ end
35
+ return sort_files(files)
36
+ end
37
+
38
+
39
+ def retrieve_file(file)
40
+ bucket.objects[file].read
41
+ end
42
+
43
+ def retrieve_job_files(job_id)
44
+ tree = bucket.objects.with_prefix(prefix + "#{job_id}").as_tree
45
+ sort_files(tree.children.select(&:leaf?).collect(&:key))
46
+ end
47
+
48
+ private
49
+
50
+ def s3
51
+ # Use AWS credentials to access S3
52
+ @s3 ||= AWS::S3.new(access_key_id: AWS_ID,
53
+ secret_access_key: AWS_KEY,
54
+ ssl_verify_peer: false)
55
+ end
56
+
57
+ def bucket
58
+ @bucket ||= s3.buckets[NAF_BUCKET]
59
+ end
60
+
61
+ def prefix
62
+ if @runner_id.present?
63
+ "naf/#{project_name}/#{Rails.env}/#{creation_time}/#{::Naf::NAF_DATABASE_HOSTNAME}/#{::Naf::NAF_DATABASE}/#{::Naf.schema_name}/runners/#{@runner_id}/invocations/"
64
+ else
65
+ "naf/#{project_name}/#{Rails.env}/#{creation_time}/#{::Naf::NAF_DATABASE_HOSTNAME}/#{::Naf::NAF_DATABASE}/#{::Naf.schema_name}/jobs/"
66
+ end
67
+ end
68
+
69
+ def sort_files(files)
70
+ files.sort do |x, y|
71
+ -Time.parse(x.scan(/\d{8}_\d{4}/).last).to_i <=> -Time.parse(y.scan(/\d{8}_\d{4}/).last).to_i
72
+ end
73
+ end
74
+
75
+ def project_name
76
+ (`git remote -v`).slice(/\/\S+/).sub('.git','')[1..-1]
77
+ end
78
+
79
+ def creation_time
80
+ ::Naf::ApplicationType.first.created_at.strftime("%Y%m%d_%H%M%S")
81
+ end
82
+
83
+ end
84
+ end
85
+ end
@@ -61,7 +61,11 @@ module Logical
61
61
  def affinities
62
62
  @machine.machine_affinity_slots.map do |slot|
63
63
  if slot.affinity_short_name
64
- slot.affinity_short_name
64
+ if slot.affinity_parameter.present? && slot.affinity_parameter > 0
65
+ slot.affinity_short_name + "(#{slot.affinity_parameter})"
66
+ else
67
+ slot.affinity_short_name
68
+ end
65
69
  else
66
70
  name = slot.affinity_classification_name + '_' + slot.affinity_name
67
71
  name = name + '_required' if slot.required
@@ -80,6 +84,40 @@ module Logical
80
84
  end
81
85
  end
82
86
 
87
+ def status
88
+ runner_down = true
89
+ @machine.machine_runners.each do |runner|
90
+ if runner.machine_runner_invocations.where(wind_down_at: nil, dead_at: nil).count > 0
91
+ runner_down = false
92
+ break;
93
+ end
94
+ end
95
+
96
+ status = 'Good'
97
+ if runner_down
98
+ notes = 'Runner down'
99
+ status = 'Bad'
100
+ else
101
+ notes = ''
102
+ end
103
+
104
+ { server_name: name,
105
+ status: status,
106
+ notes: notes }
107
+ end
108
+
109
+ def runner
110
+ if @machine.server_name.present?
111
+ @machine.server_name.to_s
112
+ else
113
+ if Rails.env == 'development'
114
+ "localhost:#{Rails::Server.new.options[:Port]}"
115
+ else
116
+ @machine.server_address
117
+ end
118
+ end
119
+ end
120
+
83
121
  end
84
122
  end
85
123
  end
@@ -51,6 +51,24 @@ module Naf
51
51
  where(selectable: true)
52
52
  end
53
53
 
54
+ def self.names_list
55
+ selectable.map do |a|
56
+ classification = a.affinity_classification
57
+ if classification.affinity_classification_name == 'machine'
58
+ if a.affinity_short_name.present?
59
+ [a.affinity_short_name, a.id]
60
+ elsif ::Naf::Machine.find_by_id(Integer(a.affinity_name)).present?
61
+ machine = ::Naf::Machine.find_by_id(Integer(a.affinity_name))
62
+ [machine.hostname, a.id]
63
+ else
64
+ ['Bad affinity: ' + classification.affinity_classification_name + ', ' + a.affinity_name, a.id]
65
+ end
66
+ else
67
+ [classification.affinity_classification_name + ', ' + a.affinity_name, a.id]
68
+ end
69
+ end
70
+ end
71
+
54
72
  def self.pre_unpickle(unpickler)
55
73
  unpickler.references.merge!(Hash[::Naf::Affinity.all.map do |m|
56
74
  [{ association_model_name: ::Naf::Affinity.name,
@@ -28,6 +28,7 @@ module Naf
28
28
  #++++++++++++++++++++
29
29
 
30
30
  delegate :affinity_name,
31
+ :affinity_short_name,
31
32
  :affinity_classification_name, to: :affinity
32
33
 
33
34
 
@@ -33,7 +33,8 @@ module Naf
33
33
  end
34
34
 
35
35
  def invoke(job, command)
36
- Process.spawn({ "NAF_JOB_ID" => job.id.to_s }, command)
36
+ ENV['NAF_JOB_ID'] = job.id.to_s
37
+ Open4::popen4(command)
37
38
  end
38
39
 
39
40
  def rails_invocator(job)
@@ -30,7 +30,6 @@ module Naf
30
30
  :request_to_terminate,
31
31
  :marked_dead_by_machine_id,
32
32
  :log_level,
33
- :tags,
34
33
  :machine_runner_invocation_id
35
34
 
36
35
  JOB_STALE_TIME = 1.week
@@ -59,6 +58,12 @@ module Naf
59
58
  class_name: "::Naf::ApplicationRunGroupRestriction"
60
59
  belongs_to :machine_runner_invocation,
61
60
  class_name: "::Naf::MachineRunnerInvocation"
61
+ has_one :running_job,
62
+ class_name: "::Naf::RunningJob",
63
+ foreign_key: :id
64
+ has_one :queued_job,
65
+ class_name: "::Naf::QueuedJob",
66
+ foreign_key: :id
62
67
  # Must access instance methods job_prerequisites through helper methods so we can use partitioning sql
63
68
  has_many :historical_job_prerequisites,
64
69
  class_name: "::Naf::HistoricalJobPrerequisite",
@@ -99,6 +104,9 @@ module Naf
99
104
 
100
105
  delegate :application_run_group_restriction_name, to: :application_run_group_restriction
101
106
  delegate :script_type_name, to: :application_type
107
+ delegate :affinity_name,
108
+ :affinity_classification_name,
109
+ :affinity_short_name, to: :affinity
102
110
 
103
111
  #------------------
104
112
  # *** Partition ***
@@ -302,33 +310,5 @@ module Naf
302
310
  application_type.spawn(self)
303
311
  end
304
312
 
305
- def add_tags(tags_to_add)
306
- tags_array = nil
307
- if self.tags.present?
308
- tags_array = self.tags.gsub(/[{}]/,'').split(',')
309
- new_tags = '{' + (tags_array | tags_to_add).join(',') + '}'
310
- else
311
- new_tags = '{' + tags_to_add.join(',') + '}'
312
- end
313
-
314
- self.tags = new_tags
315
- self.save!
316
- end
317
-
318
- def remove_tags(tags_to_remove)
319
- if self.tags.present?
320
- tags_array = self.tags.gsub(/[{}]/,'').split(',')
321
- new_tags = '{' + (tags_array - tags_to_remove).join(',') + '}'
322
-
323
- self.tags = new_tags
324
- self.save!
325
- end
326
- end
327
-
328
- def remove_all_tags
329
- self.tags = '{}'
330
- self.save!
331
- end
332
-
333
313
  end
334
314
  end