extface 0.0.1

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 (143) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +34 -0
  5. data/app/assets/javascripts/extface/application.js +13 -0
  6. data/app/assets/javascripts/extface/devices.js +2 -0
  7. data/app/assets/javascripts/extface/jobs.js +17 -0
  8. data/app/assets/stylesheets/extface/application.css +13 -0
  9. data/app/assets/stylesheets/extface/devices.css +4 -0
  10. data/app/assets/stylesheets/extface/jobs.css +4 -0
  11. data/app/assets/stylesheets/extface/sse.css +4 -0
  12. data/app/assets/stylesheets/scaffold.css +56 -0
  13. data/app/controllers/extface/application_controller.rb +21 -0
  14. data/app/controllers/extface/devices_controller.rb +69 -0
  15. data/app/controllers/extface/handler_controller.rb +66 -0
  16. data/app/controllers/extface/jobs_controller.rb +30 -0
  17. data/app/helpers/extface/application_helper.rb +40 -0
  18. data/app/helpers/extface/devices_helper.rb +21 -0
  19. data/app/helpers/extface/jobs_helper.rb +4 -0
  20. data/app/helpers/extface/sse_helper.rb +4 -0
  21. data/app/models/extface/device.rb +47 -0
  22. data/app/models/extface/driver/datecs_fp550.rb +6 -0
  23. data/app/models/extface/driver/fixed_width_serial_cdr.rb +6 -0
  24. data/app/models/extface/driver/generic_pos.rb +7 -0
  25. data/app/models/extface/driver/raw_serial.rb +27 -0
  26. data/app/models/extface/driver/star_scp700.rb +16 -0
  27. data/app/models/extface/driver/star_tsp200.rb +6 -0
  28. data/app/models/extface/driver_base.rb +67 -0
  29. data/app/models/extface/fiscal_print_driver.rb +8 -0
  30. data/app/models/extface/job.rb +54 -0
  31. data/app/models/extface/pbx_cdr_driver.rb +12 -0
  32. data/app/models/extface/pos_print_driver.rb +45 -0
  33. data/app/models/extface/raw_driver.rb +16 -0
  34. data/app/models/extface/serial_config.rb +10 -0
  35. data/app/views/extface/application/_object_errors.html.erb +10 -0
  36. data/app/views/extface/devices/form.html.erb +46 -0
  37. data/app/views/extface/devices/index.html.erb +36 -0
  38. data/app/views/extface/devices/show.html.erb +56 -0
  39. data/app/views/extface/driver/fixed_width_serial_cdr/_settings.html.erb +0 -0
  40. data/app/views/extface/driver/star_scp700/_settings.html.erb +1 -0
  41. data/app/views/extface/jobs/_form.html.erb +21 -0
  42. data/app/views/extface/jobs/edit.html.erb +6 -0
  43. data/app/views/extface/jobs/index.html.erb +27 -0
  44. data/app/views/extface/jobs/new.html.erb +5 -0
  45. data/app/views/extface/jobs/show.html.erb +9 -0
  46. data/app/views/extface/serial_configs/_settings.html.erb +37 -0
  47. data/app/views/layouts/extface/application.html.erb +14 -0
  48. data/config/routes.rb +12 -0
  49. data/db/migrate/20140215192111_create_extface_devices.rb +12 -0
  50. data/db/migrate/20140216110018_create_extface_pos_print_drivers.rb +9 -0
  51. data/db/migrate/20140216110608_create_extface_serial_configs.rb +14 -0
  52. data/db/migrate/20140216131245_create_extface_raw_drivers.rb +9 -0
  53. data/db/migrate/20140216131457_create_extface_fiscal_print_drivers.rb +9 -0
  54. data/db/migrate/20140216134748_create_extface_pbx_cdr_drivers.rb +9 -0
  55. data/db/migrate/20140219091811_create_extface_jobs.rb +9 -0
  56. data/db/migrate/20140219175006_add_job_info_to_extface_jobs.rb +9 -0
  57. data/lib/extface.rb +34 -0
  58. data/lib/extface/engine.rb +17 -0
  59. data/lib/extface/extfaceable.rb +17 -0
  60. data/lib/extface/mapping.rb +31 -0
  61. data/lib/extface/routes.rb +8 -0
  62. data/lib/extface/version.rb +3 -0
  63. data/lib/tasks/extface_tasks.rake +4 -0
  64. data/test/controllers/extface/devices_controller_test.rb +51 -0
  65. data/test/controllers/extface/jobs_controller_test.rb +14 -0
  66. data/test/dummy/README.rdoc +28 -0
  67. data/test/dummy/Rakefile +6 -0
  68. data/test/dummy/app/assets/javascripts/application.js +13 -0
  69. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  70. data/test/dummy/app/controllers/application_controller.rb +5 -0
  71. data/test/dummy/app/helpers/application_helper.rb +2 -0
  72. data/test/dummy/app/models/shop.rb +3 -0
  73. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  74. data/test/dummy/bin/bundle +3 -0
  75. data/test/dummy/bin/rails +4 -0
  76. data/test/dummy/bin/rake +4 -0
  77. data/test/dummy/config.ru +4 -0
  78. data/test/dummy/config/application.rb +24 -0
  79. data/test/dummy/config/boot.rb +5 -0
  80. data/test/dummy/config/database.yml +25 -0
  81. data/test/dummy/config/environment.rb +5 -0
  82. data/test/dummy/config/environments/development.rb +29 -0
  83. data/test/dummy/config/environments/production.rb +80 -0
  84. data/test/dummy/config/environments/test.rb +36 -0
  85. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  86. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  87. data/test/dummy/config/initializers/inflections.rb +16 -0
  88. data/test/dummy/config/initializers/mime_types.rb +5 -0
  89. data/test/dummy/config/initializers/secret_token.rb +12 -0
  90. data/test/dummy/config/initializers/session_store.rb +3 -0
  91. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  92. data/test/dummy/config/locales/en.yml +23 -0
  93. data/test/dummy/config/routes.rb +7 -0
  94. data/test/dummy/db/development.sqlite3 +0 -0
  95. data/test/dummy/db/migrate/20140221203517_create_shops.rb +8 -0
  96. data/test/dummy/db/schema.rb +83 -0
  97. data/test/dummy/db/test.sqlite3 +0 -0
  98. data/test/dummy/log/development.log +57 -0
  99. data/test/dummy/log/test.log +5908 -0
  100. data/test/dummy/public/404.html +58 -0
  101. data/test/dummy/public/422.html +58 -0
  102. data/test/dummy/public/500.html +57 -0
  103. data/test/dummy/public/favicon.ico +0 -0
  104. data/test/dummy/tmp/cache/assets/test/sprockets/0a8a23b1a73ebf217f4e2b765013ce7b +0 -0
  105. data/test/dummy/tmp/cache/assets/test/sprockets/21fb58a50e25d1170f2c7d5bd5d29c54 +0 -0
  106. data/test/dummy/tmp/cache/assets/test/sprockets/263375f2d55c31687ed052f99ddead55 +0 -0
  107. data/test/dummy/tmp/cache/assets/test/sprockets/3f4ff460a79b2e29781690193a3960f6 +0 -0
  108. data/test/dummy/tmp/cache/assets/test/sprockets/4ef10b222fde3c042209a46daf3d3e36 +0 -0
  109. data/test/dummy/tmp/cache/assets/test/sprockets/544650645bc510ef8fbee0043c41fdcf +0 -0
  110. data/test/dummy/tmp/cache/assets/test/sprockets/6dbf2b0443792e51055710be15c83197 +0 -0
  111. data/test/dummy/tmp/cache/assets/test/sprockets/717acb3e7c0df18696cf1bc56de6d3d5 +0 -0
  112. data/test/dummy/tmp/cache/assets/test/sprockets/9891cbb0808f38b2c42f42fd89e662a4 +0 -0
  113. data/test/dummy/tmp/cache/assets/test/sprockets/9b178820b9dc8c81c9784cf7ee3b7d35 +0 -0
  114. data/test/dummy/tmp/cache/assets/test/sprockets/a06a4a7f51754a6a2c0694eb39ff824b +0 -0
  115. data/test/dummy/tmp/cache/assets/test/sprockets/a8cc2e73253025d2518d2449e16e5266 +0 -0
  116. data/test/dummy/tmp/cache/assets/test/sprockets/b56dfad0b14283f2cc10c62f37d71cc7 +0 -0
  117. data/test/dummy/tmp/cache/assets/test/sprockets/e1197e0129ade843b08de0b45c47fc40 +0 -0
  118. data/test/dummy/tmp/cache/assets/test/sprockets/ece3270b150db7c668579bb91ac7e2e5 +0 -0
  119. data/test/dummy/tmp/cache/assets/test/sprockets/f018ab87f3490dbbcbc2e3c9f36aed3d +0 -0
  120. data/test/extface_test.rb +7 -0
  121. data/test/fixtures/extface/devices.yml +13 -0
  122. data/test/fixtures/extface/fiscal_print_drivers.yml +7 -0
  123. data/test/fixtures/extface/jobs.yml +7 -0
  124. data/test/fixtures/extface/pbx_cdr_drivers.yml +7 -0
  125. data/test/fixtures/extface/pos_print_drivers.yml +7 -0
  126. data/test/fixtures/extface/raw_drivers.yml +7 -0
  127. data/test/fixtures/extface/serial_configs.yml +17 -0
  128. data/test/fixtures/shops.yml +11 -0
  129. data/test/helpers/extface/devices_helper_test.rb +6 -0
  130. data/test/helpers/extface/jobs_helper_test.rb +6 -0
  131. data/test/helpers/extface/sse_helper_test.rb +6 -0
  132. data/test/integration/navigation_test.rb +10 -0
  133. data/test/models/extface/device_test.rb +9 -0
  134. data/test/models/extface/driver/star_tsp200_test.rb +9 -0
  135. data/test/models/extface/driver_test.rb +9 -0
  136. data/test/models/extface/fiscal_print_driver_test.rb +9 -0
  137. data/test/models/extface/job_test.rb +9 -0
  138. data/test/models/extface/pbx_cdr_driver_test.rb +9 -0
  139. data/test/models/extface/pos_print_driver_test.rb +9 -0
  140. data/test/models/extface/raw_driver_test.rb +9 -0
  141. data/test/models/extface/serial_config_test.rb +9 -0
  142. data/test/test_helper.rb +29 -0
  143. metadata +322 -0
@@ -0,0 +1,4 @@
1
+ module Extface
2
+ module JobsHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Extface
2
+ module SseHelper
3
+ end
4
+ end
@@ -0,0 +1,47 @@
1
+ module Extface
2
+ class Device < ActiveRecord::Base
3
+ attr_writer :driver
4
+ belongs_to :extfaceable, polymorphic: true
5
+ belongs_to :driveable, polymorphic: true
6
+ has_many :jobs, inverse_of: :device
7
+
8
+ accepts_nested_attributes_for :driveable
9
+
10
+ delegate :print?, :fiscal?, :raw?, :cdr?, to: :driveable, allow_nil: true
11
+
12
+ validates_uniqueness_of :name, :uuid, scope: [:extfaceable_id, :extfaceable_type]
13
+
14
+ before_create do
15
+ self.uuid = SecureRandom.hex
16
+ self.name = uuid unless name.present?
17
+ self.driveable = @driver.constantize.create if @driver.present?
18
+ end
19
+
20
+ def driver
21
+ driveable.try(:class)
22
+ end
23
+
24
+ def driver_name
25
+ driver::NAME if driver
26
+ end
27
+
28
+ def session(description = nil)
29
+ job = jobs.create!(description: description)
30
+ Thread.new do
31
+ begin
32
+ raise 'No driver configured' unless driveable.present?
33
+ driveable.set_job(job)
34
+ yield driveable
35
+ job.complete!
36
+ rescue => e
37
+ STDERR.puts e.message
38
+ e.backtrace.each do |line|
39
+ p line
40
+ end
41
+ job.failed! e.message
42
+ end
43
+ end
44
+ job
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,6 @@
1
+ module Extface
2
+ class Driver::DatecsFp550 < Extface::FiscalPrintDriver
3
+ NAME = 'Datecs FP550'.freeze
4
+ has_serial_config
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Extface
2
+ class Driver::FixedWidthSerialCdr < Extface::PbxCdrDriver
3
+ NAME = 'PBX CDR Fixed Width Parser'.freeze
4
+ has_serial_config
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module Extface
2
+ class Driver::GenericPos < Extface::PosPrintDriver
3
+ NAME = 'Generic Pos Printer (Serial)'.freeze
4
+ has_serial_config
5
+ alias_method :print, :push
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ module Extface
2
+ class Driver::RawSerial < Extface::RawDriver
3
+ NAME = 'RAW Serial'.freeze
4
+ has_serial_config
5
+
6
+ def push(buffer)
7
+ Extface.redis.rpush(:key, buffer)
8
+ Extface.redis.subscribe(:extface) do |on|
9
+ on.message do |event, data|
10
+ @return = "event: #{event} data: #{data}\n\n"
11
+ Extface.redis.unsubscribe
12
+ end
13
+ end
14
+ end
15
+
16
+ def receive(timeout = nil)
17
+ list, element = Extface.redis.blpop(:key, :timeout => timeout)
18
+ # Extface.redis.subscribe(:extface) do |on|
19
+ # on.message do |event, data|
20
+ # @return = "event: #{event} data: #{data}\n\n"
21
+ # Extface.redis.unsubscribe
22
+ # end
23
+ # end
24
+ return element
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ module Extface
2
+ class Driver::StarScp700 < Extface::Driver::StarTsp200
3
+ NAME = 'Star SCP700 (Receipt only)'.freeze
4
+
5
+ alias_method :print, :push
6
+
7
+ def autocut(partial = true)
8
+ print "\r\n\r\n\r\n"
9
+ print partial ? "\x1B\x64\x31" : "\x1B\x64\x30"
10
+ end
11
+
12
+ def autocutter?
13
+ true
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ module Extface
2
+ class Driver::StarTsp200 < Extface::PosPrintDriver
3
+ NAME = 'Star TSP200 (Base Receipt Protocol)'.freeze
4
+ has_serial_config
5
+ end
6
+ end
@@ -0,0 +1,67 @@
1
+ require'timeout'
2
+ module Extface
3
+ class DriverBase < ActiveRecord::Base
4
+ self.abstract_class = true
5
+
6
+ has_one :device, :as => :driveable
7
+
8
+ [:raw?, :print?, :fiscal?, :cdr?].each do |driver_type|
9
+ define_method driver_type do
10
+ false
11
+ end
12
+ end
13
+
14
+ class << self
15
+ def has_serial_config
16
+ has_one :serial_config, as: :s_configureable
17
+ accepts_nested_attributes_for :serial_config
18
+ define_method :serial? do
19
+ true
20
+ end
21
+ end
22
+ end
23
+
24
+ def handle(buffer) # handle push messages from device outside active session
25
+ return false
26
+ end
27
+
28
+ def push(buffer)
29
+
30
+ if @job
31
+ Timeout.timeout(10) do
32
+ Extface.redis_block do |r|
33
+ r.subscribe(@job.id) do |on| #blocking until delivered
34
+ on.subscribe do |channel, subscriptions|
35
+ @job.rpush buffer
36
+ end
37
+ on.message do |event, data|
38
+ r.unsubscribe
39
+ @job.connected!
40
+ end
41
+ end
42
+ end
43
+ end
44
+ else
45
+ raise "No job given"
46
+ end
47
+
48
+ end
49
+
50
+ def pull(timeout = nil)
51
+ Extface.redis_block do |r|
52
+ list, element = r.blpop(device.uuid, :timeout => timeout)
53
+ end
54
+ element
55
+ end
56
+
57
+ def notify(message)
58
+ raise "No job given" unless @job
59
+ @job.notify(message)
60
+ end
61
+
62
+ def set_job(job)
63
+ @job = job
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,8 @@
1
+ module Extface
2
+ class FiscalPrintDriver < Extface::DriverBase
3
+ GROUP = 'Fiscal Printers'.freeze
4
+ def fiscal?
5
+ true
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,54 @@
1
+ module Extface
2
+ class Job < ActiveRecord::Base
3
+ belongs_to :device, inverse_of: :jobs
4
+
5
+ scope :active, ->{ where(arel_table[:completed_at].eq(nil).and(arel_table[:failed_at].eq(nil))) }
6
+
7
+ def complete!
8
+ self.completed_at = Time.now
9
+ save!
10
+ notify("Job #{self.id} completed!")
11
+ end
12
+
13
+ def completed?
14
+ !!completed_at
15
+ end
16
+
17
+ def connected!
18
+ unless connected?
19
+ self.connected_at = Time.now
20
+ save!
21
+ notify("Job #{self.id} device connected!")
22
+ end
23
+ end
24
+
25
+ def connected?
26
+ !!connected_at?
27
+ end
28
+
29
+ def failed!(message)
30
+ self.error = message
31
+ self.failed_at = Time.now
32
+ save!
33
+ notify(message)
34
+ notify("Job #{self.id} failed!")
35
+ end
36
+
37
+ def failed?
38
+ !!failed_at
39
+ end
40
+
41
+ def notify(message)
42
+ Extface.redis_block do |r|
43
+ r.publish(self.id, message)
44
+ end
45
+ end
46
+
47
+ def rpush(buffer)
48
+ Extface.redis_block do |r|
49
+ r.rpush self.id, buffer
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ module Extface
2
+ class PbxCdrDriver < Extface::DriverBase
3
+ GROUP = 'PBX CDR Loggers (Call Detail Record)'.freeze
4
+ def calls
5
+ raise "Not Impelmened"
6
+ end
7
+
8
+ def crd?
9
+ true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ module Extface
2
+ class PosPrintDriver < Extface::DriverBase
3
+ GROUP = 'POS Printers'.freeze
4
+ def print(buffer)
5
+ raise "Not Impelmented"
6
+ end
7
+
8
+ def print_test_page
9
+ device.session("Print Test Page") do |s|
10
+ sleep 1
11
+ s.notify "Printing Test Page"
12
+ s.print "******************************\r\n"
13
+ s.print "* Extface Print Test Page *\r\n"
14
+ s.print "******************************\r\n"
15
+
16
+ s.notify "Printing driver information"
17
+ s.print "\r\nDriver:\r\n"
18
+ s.print "------------------------------\r\n"
19
+ s.print "#{self.class::NAME}".truncate(30)
20
+ s.print "\r\n"
21
+
22
+ if try(:serial?)
23
+ s.notify "Printing serial settings"
24
+ s.print "\r\nSerial Port Settings:\r\n"
25
+ s.print "------------------------------\r\n"
26
+ end
27
+
28
+ s.print "\r\n"
29
+ s.print "------------------------------\r\n"
30
+ s.print Time.now.strftime("Printed on %m/%d/%Y %T\r\n").rjust(32)
31
+ s.print "\r\n\r\n"
32
+ s.notify "Printing finished"
33
+
34
+ if s.try(:autocutter?)
35
+ s.autocut
36
+ end
37
+ end
38
+ end
39
+
40
+ def print?
41
+ true
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ module Extface
2
+ class RawDriver < Extface::DriverBase
3
+ GROUP = 'RAW Communication'.freeze
4
+ def push(data)
5
+ raise "Not Impelmened"
6
+ end
7
+
8
+ def receive
9
+ raise "Not Impelmened"
10
+ end
11
+
12
+ def raw?
13
+ true
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module Extface
2
+ class SerialConfig < ActiveRecord::Base
3
+ BOUD_RATES = [2400, 4800, 9600, 19200].freeze
4
+ DATA_LENGTHS = [7, 8].freeze
5
+ PARITY_CHECKS = [0, 1, 2].freeze
6
+ STOP_BITS = [1, 2].freeze
7
+ HANDSHAKE = [0, 1, 2].freeze
8
+ belongs_to :s_configureable, polymorphic: true
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ <% if object.errors.any? %>
2
+ <div class='alert alert-danger'>
3
+ <button type="button" class="close" data-dismiss="alert">×</button>
4
+
5
+ <% object.errors.full_messages.each do |msg| %>
6
+ <p><%= msg %></p>
7
+ <% end %>
8
+
9
+ </div>
10
+ <% end %>
@@ -0,0 +1,46 @@
1
+ <%= form_for @device, html: { class: 'form-horizontal' } do |f| %>
2
+ <div class="page-header nav">
3
+ <%= f.button type: :submit, name: :submit, value: :submit, class: 'btn btn-primary navbar-btn pull-right' do %>
4
+ &#10003; <%= t( @device.new_record? ? '.create' : '.update' ) %>
5
+ <% end %>
6
+ <h3 class='navbar-text'>
7
+ <%= link_to extface.root_path do %>
8
+ Extface<sup>&hearts;</sup>
9
+ <% end %>
10
+ &raquo;
11
+ <%= link_to t('.devices'), devices_path %>
12
+ &raquo;
13
+ <%= t( @device.new_record? ? '.new' : '.edit') %>
14
+ </h3>
15
+ </div>
16
+
17
+ <%= render 'object_errors', object: f.object %>
18
+
19
+ <%= form_group f, :name, :text_field %>
20
+
21
+ <div class="form-group">
22
+ <%= f.label :driver, class: 'col-sm-2 control-label' %>
23
+ <div class='col-sm-10 col-md-8'>
24
+ <% if @device.driveable.present? %>
25
+ <p class="form-control-static">
26
+ <%= link_to '#', class: 'btn btn-danger btn-sm pull-right' do %>
27
+ <%= t('.change_driver') %>
28
+ <% end %>
29
+ <%= @device.driveable.class::NAME %>
30
+ </p>
31
+ <% else %>
32
+ <%= f.select :driver, options_for_drivers, { include_blank: true }, class: 'form-control' %>
33
+ <% end %>
34
+ </div>
35
+ </div>
36
+ <%= fields_for :driveable do |fd| %>
37
+ <%= driver_settings fd, @device.driveable %>
38
+ <%= fields_for :serial_config do |fds| %>
39
+ <div class="form-group">
40
+ <div class='col-sm-offset-2 col-sm-10 col-md-8'>
41
+ <%= render 'extface/serial_configs/settings', f: fds %>
42
+ </div>
43
+ </div>
44
+ <% end if @device.driveable.serial? %>
45
+ <% end if @device.driveable.present? %>
46
+ <% end %>
@@ -0,0 +1,36 @@
1
+ <div class="page-header nav">
2
+ <%= link_to new_device_path, class: 'btn btn-default navbar-btn pull-right' do %>
3
+ + <%= t('.new') %>
4
+ <% end %>
5
+ <h3 class='navbar-text'>
6
+ <%= link_to extface.root_path do %>
7
+ Extface<sup>&hearts;</sup>
8
+ <% end %>
9
+ &raquo;
10
+ <%= t('.devices') %>
11
+ </h3>
12
+ </div>
13
+
14
+ <table class="table table-condensed table-stripped">
15
+ <thead>
16
+ <tr class='active'>
17
+ <th><%= t('.name') %></th>
18
+ <th><%= t('.parent') %></th>
19
+ <th><%= t('.driver') %></th>
20
+ <th>&nbsp;</th>
21
+ <th>&nbsp;</th>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <% @devices.each do |device| %>
26
+ <tr>
27
+ <td><%= link_to device.name, device %></td>
28
+ <td><%= device.extfaceable.composite_id %></td>
29
+ <td><%= device.driveable ? device.driveable.class::NAME : 'N/A' %></td>
30
+ <td class='text-right'><%= link_to 'Edit', edit_device_path(device), class: 'btn btn-primary btn-sm' %></td>
31
+ <td class='text-right'><%= button_to 'Delete', device, method: :delete, class: 'btn btn-danger btn-sm', data: { confirm: 'Are you sure?' } %></td>
32
+ </tr>
33
+ <% end %>
34
+ </tbody>
35
+ </table>
36
+