riddler_admin 0.1.0 → 0.2.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/riddler_admin/application.js +0 -1
  3. data/app/assets/javascripts/riddler_admin/preview_contexts.js +2 -0
  4. data/app/assets/javascripts/riddler_admin/publish_requests.js +2 -0
  5. data/app/assets/javascripts/riddler_admin/steps.js +36 -22
  6. data/app/assets/stylesheets/riddler_admin/application.scss +3 -0
  7. data/app/assets/stylesheets/riddler_admin/preview_contexts.css +4 -0
  8. data/app/assets/stylesheets/riddler_admin/publish_requests.css +4 -0
  9. data/app/controllers/riddler_admin/application_controller.rb +24 -2
  10. data/app/controllers/riddler_admin/dashboard_controller.rb +8 -0
  11. data/app/controllers/riddler_admin/elements_controller.rb +6 -2
  12. data/app/controllers/riddler_admin/preview_contexts_controller.rb +60 -0
  13. data/app/controllers/riddler_admin/publish_requests_controller.rb +90 -0
  14. data/app/controllers/riddler_admin/slugs_controller.rb +67 -0
  15. data/app/controllers/riddler_admin/steps_controller.rb +73 -20
  16. data/app/helpers/riddler_admin/application_helper.rb +19 -0
  17. data/app/helpers/riddler_admin/preview_contexts_helper.rb +4 -0
  18. data/app/helpers/riddler_admin/publish_requests_helper.rb +4 -0
  19. data/app/models/riddler_admin/application_record.rb +4 -4
  20. data/app/models/riddler_admin/content_definition.rb +87 -0
  21. data/app/models/riddler_admin/element.rb +52 -10
  22. data/app/models/riddler_admin/elements/heading.rb +6 -0
  23. data/app/models/riddler_admin/elements/image.rb +18 -0
  24. data/app/models/riddler_admin/elements/link.rb +18 -0
  25. data/app/models/riddler_admin/elements/{copy.rb → text.rb} +7 -1
  26. data/app/models/riddler_admin/elements/variant.rb +19 -0
  27. data/app/models/riddler_admin/preview_context.rb +83 -0
  28. data/app/models/riddler_admin/publish_request.rb +57 -0
  29. data/app/models/riddler_admin/slug.rb +90 -0
  30. data/app/models/riddler_admin/step.rb +66 -4
  31. data/app/models/riddler_admin/steps/variant.rb +19 -0
  32. data/app/validators/parseable_predicate_validator.rb +8 -0
  33. data/app/views/layouts/riddler_admin/application.html.erb +11 -4
  34. data/app/views/riddler_admin/_element.html.erb +14 -10
  35. data/app/views/riddler_admin/_element_container.html.erb +11 -0
  36. data/app/views/riddler_admin/_navbar.html.erb +29 -0
  37. data/app/views/riddler_admin/_preview_step.html.erb +39 -0
  38. data/app/views/riddler_admin/_step.html.erb +35 -0
  39. data/app/views/riddler_admin/_step_container.html.erb +11 -0
  40. data/app/views/riddler_admin/dashboard/index.html.erb +44 -0
  41. data/app/views/riddler_admin/elements/_form.html.erb +37 -0
  42. data/app/views/riddler_admin/elements/_new.html.erb +41 -8
  43. data/app/views/riddler_admin/elements/_show.html.erb +3 -1
  44. data/app/views/riddler_admin/elements/edit.js.erb +2 -0
  45. data/app/views/riddler_admin/elements/heading/_detail.html.erb +0 -0
  46. data/app/views/riddler_admin/elements/heading/_form.html.erb +8 -2
  47. data/app/views/riddler_admin/elements/image/_detail.html.erb +0 -0
  48. data/app/views/riddler_admin/elements/image/_form.html.erb +12 -0
  49. data/app/views/riddler_admin/elements/link/_detail.html.erb +0 -0
  50. data/app/views/riddler_admin/elements/link/_form.html.erb +12 -0
  51. data/app/views/riddler_admin/elements/show.js.erb +2 -2
  52. data/app/views/riddler_admin/elements/text/_detail.html.erb +0 -0
  53. data/app/views/riddler_admin/elements/text/_form.html.erb +4 -0
  54. data/app/views/riddler_admin/elements/variant/_detail.html.erb +6 -0
  55. data/app/views/riddler_admin/elements/variant/_form.html.erb +1 -0
  56. data/app/views/riddler_admin/preview_contexts/_form.html.erb +44 -0
  57. data/app/views/riddler_admin/preview_contexts/edit.html.erb +10 -0
  58. data/app/views/riddler_admin/preview_contexts/index.html.erb +43 -0
  59. data/app/views/riddler_admin/preview_contexts/new.html.erb +10 -0
  60. data/app/views/riddler_admin/preview_contexts/show.html.erb +45 -0
  61. data/app/views/riddler_admin/publish_requests/_actions.html.erb +19 -0
  62. data/app/views/riddler_admin/publish_requests/_form.html.erb +53 -0
  63. data/app/views/riddler_admin/publish_requests/approve.js.erb +2 -0
  64. data/app/views/riddler_admin/publish_requests/edit.html.erb +10 -0
  65. data/app/views/riddler_admin/publish_requests/index.html.erb +62 -0
  66. data/app/views/riddler_admin/publish_requests/new.html.erb +10 -0
  67. data/app/views/riddler_admin/publish_requests/publish.js.erb +2 -0
  68. data/app/views/riddler_admin/publish_requests/show.html.erb +25 -0
  69. data/app/views/riddler_admin/slugs/_form.html.erb +48 -0
  70. data/app/views/riddler_admin/slugs/edit.html.erb +10 -0
  71. data/app/views/riddler_admin/slugs/index.html.erb +44 -0
  72. data/app/views/riddler_admin/slugs/new.html.erb +10 -0
  73. data/app/views/riddler_admin/slugs/show.html.erb +18 -0
  74. data/app/views/riddler_admin/steps/_definition.html.erb +11 -0
  75. data/app/views/riddler_admin/steps/_form.html.erb +45 -17
  76. data/app/views/riddler_admin/steps/_new_sub_step.html.erb +56 -0
  77. data/app/views/riddler_admin/steps/_visual_preview.html.erb +24 -0
  78. data/app/views/riddler_admin/steps/content/_show.html.erb +53 -25
  79. data/app/views/riddler_admin/steps/edit.html.erb +15 -4
  80. data/app/views/riddler_admin/steps/index.html.erb +24 -10
  81. data/app/views/riddler_admin/steps/internal_preview.js.erb +5 -0
  82. data/app/views/riddler_admin/steps/new.html.erb +10 -5
  83. data/app/views/riddler_admin/steps/new.js.erb +3 -0
  84. data/app/views/riddler_admin/steps/preview.json.erb +1 -0
  85. data/app/views/riddler_admin/steps/show.html.erb +26 -11
  86. data/app/views/riddler_admin/steps/show.js.erb +4 -0
  87. data/app/views/riddler_admin/steps/variant/_show.html.erb +59 -0
  88. data/config/routes.rb +20 -1
  89. data/db/migrate/20181124201519_riddler_admin_genesis.rb +72 -0
  90. data/lib/riddler_admin.rb +29 -2
  91. data/lib/riddler_admin/configuration.rb +44 -0
  92. data/lib/riddler_admin/version.rb +1 -1
  93. metadata +75 -30
  94. data/app/views/riddler_admin/elements/copy/_class.html.erb +0 -3
  95. data/app/views/riddler_admin/elements/copy/_form.html.erb +0 -2
  96. data/app/views/riddler_admin/elements/heading/_class.html.erb +0 -3
  97. data/app/views/riddler_admin/steps/_preview.html.erb +0 -5
  98. data/db/migrate/20181124201519_create_riddler_admin_steps.rb +0 -20
@@ -0,0 +1,90 @@
1
+ require "riddler/protobuf/content_management_services_pb"
2
+
3
+ module RiddlerAdmin
4
+ class Slug < ::RiddlerAdmin::ApplicationRecord
5
+ MODEL_KEY = "sl".freeze
6
+ ID_LENGTH = 5 # 916_132_832 per second
7
+
8
+ belongs_to :content_definition
9
+
10
+ validates :name, presence: true, uniqueness: true,
11
+ format: {
12
+ with: /\A[a-z][a-zA-Z0-9_]*\z/,
13
+ message: "must be a valid variable name (no spaces, start with character)"
14
+ }
15
+
16
+ validates :status, presence: true, inclusion: {
17
+ in: ::Riddler::Protobuf::SlugStatus.constants.map(&:to_s)
18
+ }
19
+
20
+ validates :target_predicate, parseable_predicate: true
21
+
22
+ after_create :create_remote
23
+ after_update :update_remote
24
+
25
+ def daily_stats
26
+ day_stats = stats_response.slug_stats.detect { |ss| ss.interval == :DAY }
27
+ day_stats.event_counts
28
+ rescue GRPC::Unavailable, GRPC::DeadlineExceeded
29
+ []
30
+ end
31
+
32
+ def create_remote
33
+ content_management_grpc.create_slug create_request_proto
34
+ end
35
+
36
+ def update_remote
37
+ content_management_grpc.update_slug update_request_proto
38
+ end
39
+
40
+ def to_proto
41
+ proto = ::Riddler::Protobuf::Slug.new \
42
+ id: id,
43
+ created_at: timestamp_proto(created_at),
44
+ updated_at: timestamp_proto(updated_at),
45
+ name: name,
46
+ status: status.upcase.to_sym,
47
+ content_definition_id: content_definition.id
48
+
49
+ if interaction_identity.to_s.strip != ""
50
+ proto.interaction_identity = interaction_identity
51
+ end
52
+
53
+ if target_predicate.to_s.strip != ""
54
+ proto.target_predicate = target_predicate
55
+ end
56
+
57
+ proto
58
+ end
59
+
60
+ private
61
+
62
+ def stats_response
63
+ @stats_response ||= content_management_grpc.get_slug_stats get_stats_proto
64
+ end
65
+
66
+ def timestamp_proto timestamp
67
+ ::Google::Protobuf::Timestamp.new seconds: timestamp.to_i,
68
+ nanos: timestamp.nsec
69
+ end
70
+
71
+ def get_stats_proto
72
+ ::Riddler::Protobuf::GetSlugStatsRequest.new slug: to_proto
73
+ end
74
+
75
+ def create_request_proto
76
+ ::Riddler::Protobuf::CreateSlugRequest.new slug: to_proto
77
+ end
78
+
79
+ def update_request_proto
80
+ ::Riddler::Protobuf::UpdateSlugRequest.new slug: to_proto
81
+ end
82
+
83
+ def content_management_grpc
84
+ ::Riddler::Protobuf::ContentManagement::Stub.new \
85
+ ::RiddlerAdmin.configuration.riddler_grpc_address,
86
+ :this_channel_is_insecure,
87
+ timeout: 0.5
88
+ end
89
+ end
90
+ end
@@ -1,12 +1,52 @@
1
1
  module RiddlerAdmin
2
- class Step < ApplicationRecord
2
+ class Step < ::RiddlerAdmin::ApplicationRecord
3
3
  MODEL_KEY = "st".freeze
4
4
  ID_LENGTH = 5 # 916_132_832 per second
5
5
 
6
+ belongs_to :stepable, polymorphic: true, optional: true
7
+ acts_as_list scope: [:stepable_type, :stepable_id]
8
+
9
+ validates_presence_of :title
10
+
11
+ # Alphanumeric and underscore only - no whitespace.
12
+ # We might consider doing lowercase only for snake casing.
13
+ validates :name, presence: true,
14
+ format: {
15
+ with: /\A[a-z][a-zA-Z0-9_]*\z/,
16
+ message: "must be a valid variable name (no spaces, start with character)"
17
+ }
18
+
19
+ validates :include_predicate, parseable_predicate: true
20
+
21
+ has_many :publish_requests,
22
+ dependent: :nullify,
23
+ as: :content
24
+
25
+ has_many :content_definitions,
26
+ dependent: :nullify,
27
+ as: :content
28
+
29
+ before_validation :set_defaults
30
+
31
+ def self.available_classes
32
+ [
33
+ Steps::Content,
34
+ Steps::Variant
35
+ ]
36
+ end
37
+
6
38
  def self.default_class
7
39
  Steps::Content
8
40
  end
9
41
 
42
+ def self.short_name
43
+ name.demodulize
44
+ end
45
+
46
+ def short_name
47
+ self.class.short_name
48
+ end
49
+
10
50
  def to_partial_path detail=nil
11
51
  [self.class.name.underscore, detail].compact.join "/"
12
52
  end
@@ -16,20 +56,42 @@ module RiddlerAdmin
16
56
  type.demodulize.underscore
17
57
  end
18
58
 
59
+ def content_type
60
+ "step"
61
+ end
62
+
63
+ def content_id
64
+ id
65
+ end
66
+
67
+ def published?
68
+ content_definitions.any?
69
+ end
70
+
19
71
  def definition_hash options=nil
20
72
  options ||= {}
21
- serializable_hash options.merge(serializable_hash_options)
73
+ hash = serializable_hash options.merge(serializable_hash_options)
74
+ hash["type"] = object
75
+ hash.delete "include_predicate" if hash["include_predicate"].blank?
76
+ hash
22
77
  end
23
78
 
24
79
  def serializable_hash_options
25
80
  {
26
- methods: :object,
81
+ methods: [:content_type],
27
82
  except: excluded_attrs
28
83
  }
29
84
  end
30
85
 
31
86
  def excluded_attrs
32
- [:created_at, :updated_at, :name]
87
+ [:created_at, :updated_at, :title, :preview_enabled, :stepable_type, :stepable_id, :position]
88
+ end
89
+
90
+ private
91
+
92
+ def set_defaults
93
+ return if name.present?
94
+ self.name = self.title.parameterize.underscore
33
95
  end
34
96
  end
35
97
  end
@@ -0,0 +1,19 @@
1
+ module RiddlerAdmin
2
+ module Steps
3
+ class Variant < Step
4
+ has_many :steps, -> { order position: :asc },
5
+ dependent: :destroy,
6
+ as: :stepable
7
+
8
+ def self.model_name
9
+ Step.model_name
10
+ end
11
+
12
+ def definition_hash options=nil
13
+ hash = super
14
+ hash["steps"] = steps.map { |s| s.definition_hash }
15
+ hash
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ class ParseablePredicateValidator < ActiveModel::EachValidator
2
+ def validate_each record, attribute, value
3
+ return if value.blank?
4
+ Predicator.parse value
5
+ rescue Racc::ParseError, Predicator::Lexer::ScanError
6
+ record.errors[attribute] << (options[:message] || "is not a valid predicate")
7
+ end
8
+ end
@@ -1,17 +1,24 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <title>Riddler Admin</title>
4
+ <title><%= page_title %></title>
5
5
  <%= csrf_meta_tags %>
6
6
  <%= csp_meta_tag %>
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
8
8
 
9
9
  <%= stylesheet_link_tag "riddler_admin/application", media: "all" %>
10
10
  <%= javascript_include_tag "riddler_admin/application" %>
11
- </head>
12
- <body>
11
+ <script>
12
+ window.riddler_admin_path = "<%= riddler_admin.root_path %>"
13
+ </script>
13
14
 
14
- <%= yield %>
15
+ <%= additional_javascript_includes %>
16
+ </head>
17
+ <body class="pt-5">
18
+ <%= render partial: "riddler_admin/navbar" %>
15
19
 
20
+ <div class="container mt-4">
21
+ <%= yield %>
22
+ </div>
16
23
  </body>
17
24
  </html>
@@ -1,15 +1,19 @@
1
1
  <li class="list-group-item" id="<%= element.id %>" data-id="<%= element.id %>">
2
- <b><%= element.short_name %> Element</b>
2
+ <div>
3
+ <%= link_to "Toggle", "#", onclick: "$('##{element.id}-detail').toggle(); return false;" %>
4
+ <span class="handle">Move</span>
5
+ </div>
3
6
 
4
- <%= link_to "Delete", element, method: :delete, remote: true, data: { confirm: "Are you sure?" } %>
7
+ <h5>
8
+ <%= element.name %>
9
+ <span class="small text-muted"><%= element.short_name %> Element</span>
10
+ </h5>
5
11
 
6
- <%= form_with model: element do |f| %>
7
- <% if element.errors.any? %>
8
- ERRORS
9
- <% end %>
12
+ <div id="<%= element.id %>-detail" class="<%= element.container.id %>-detail" style="display: none;">
13
+ <div id="<%= element.id %>-form">
14
+ <%= render partial: "riddler_admin/elements/form", locals: { element: element } %>
15
+ </div>
10
16
 
11
- <%= render partial: element.to_partial_path("form"), locals: { f: f } %>
12
-
13
- <%= f.submit %>
14
- <% end %>
17
+ <%= render partial: element.to_partial_path("detail"), locals: { element: element } %>
18
+ </div>
15
19
  </li>
@@ -0,0 +1,11 @@
1
+ <div class="card">
2
+ <div class="card-header">
3
+ <h4>Elements</h4>
4
+ <a href="#" onclick="$('.<%= container.id %>-detail').toggle(); return false;">Toggle all</a>
5
+ </div>
6
+ <div class="card-body">
7
+ <ul id="<%= container.id %>-elements" class="list-group element-container">
8
+ <%= render partial: "riddler_admin/element", collection: container.elements %>
9
+ </ul>
10
+ </div>
11
+ </div>
@@ -0,0 +1,29 @@
1
+ <nav class="navbar navbar-expand-lg navbar-dark bg-primary fixed-top border-bottom border-white">
2
+ <%= brand_link %>
3
+ <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
4
+ <span class="navbar-toggler-icon"></span>
5
+ </button>
6
+ <div class="collapse navbar-collapse" id="navbarNav">
7
+ <ul class="navbar-nav">
8
+ <% if ::RiddlerAdmin.config.main_app_name.present? %>
9
+ <li class="nav-item">
10
+ <%= link_to ::RiddlerAdmin.config.app_name, riddler_admin.root_path, class: "nav-link" %>
11
+ </li>
12
+ <% end %>
13
+ <li class="nav-item">
14
+ <%= link_to "Steps", steps_path, class: "nav-link" %>
15
+ </li>
16
+ <li class="nav-item">
17
+ <%= link_to "Preview contexts", preview_contexts_path, class: "nav-link" %>
18
+ </li>
19
+ <% if ::RiddlerAdmin.configuration.remote_riddler? %>
20
+ <li class="nav-item">
21
+ <%= link_to "Publish Requests", publish_requests_path, class: "nav-link" %>
22
+ </li>
23
+ <% end %>
24
+ <li class="nav-item">
25
+ <%= link_to "Slugs", slugs_path, class: "nav-link" %>
26
+ </li>
27
+ </ul>
28
+ </div>
29
+ </nav>
@@ -0,0 +1,39 @@
1
+ <script>
2
+ $(document).ready(function(){
3
+ $(".step-context-preview").first().click()
4
+ })
5
+ </script>
6
+
7
+ <div class="row">
8
+ <div class="col-sm-3">
9
+ <h4>Preview Contexts</h4>
10
+ <div class="list-group" id="preview-sections-tab" role="tablist">
11
+ <% preview_contexts.each do |ctx| %>
12
+ <a class="list-group-item list-group-item-action step-context-preview" id="list-preview-<%= ctx.id %>" data-step-id="<%= step.id %>" data-pctx-id="<%= ctx.id %>" data-toggle="list" href="#preview-context-<%= ctx.id %>" role="tab" aria-controls="preview-context-<%= ctx.id %>"><%= ctx.title %></a>
13
+ <% end %>
14
+ </div>
15
+ </div>
16
+ <div class="col-sm-9">
17
+ <div class="tab-content" id="nav-tabContent">
18
+ <% preview_contexts.each do |ctx| %>
19
+ <div class="tab-pane fade" id="preview-context-<%= ctx.id %>" role="tabpanel" aria-labelledby="list-preview-context-<%= ctx.id %>">
20
+ <ul class="nav nav-pills" id="<%= ctx.id %>-type" role="tablist">
21
+ <li class="nav-item">
22
+ <a class="nav-link active" id="<%= ctx.id %>-visual-tab" data-toggle="tab" href="#<%= ctx.id %>-visual" role="tab" aria-controls="<%= ctx.id %>-visual" aria-selected="true">Visual</a>
23
+ </li>
24
+ <li class="nav-item">
25
+ <a class="nav-link" id="<%= ctx.id %>-json-tab" data-toggle="tab" href="#<%= ctx.id %>-json" role="tab" aria-controls="<%= ctx.id %>-json" aria-selected="false">JSON</a>
26
+ </li>
27
+ </ul>
28
+ <div class="tab-content" id="<%= ctx.id %>-typeContent">
29
+ <div class="tab-pane fade show active" id="<%= ctx.id %>-visual" role="tabpanel" aria-labelledby="<%= ctx.id %>-visual-tab">
30
+ </div>
31
+ <div class="tab-pane fade" id="<%= ctx.id %>-json" role="tabpanel" aria-labelledby="<%= ctx.id %>-json-tab">
32
+ <pre></pre>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ <% end %>
37
+ </div>
38
+ </div>
39
+ </div>
@@ -0,0 +1,35 @@
1
+ <li class="list-group-item" id="<%= step.id %>" data-id="<%= step.id %>">
2
+ <div>
3
+ <%= link_to "Toggle", "#", onclick: "$('##{step.id}-detail').toggle(); return false;" %>
4
+ <span class="handle">Move</span>
5
+ </div>
6
+
7
+ <h5>
8
+ <%= step.name %>
9
+ <span class="small text-muted"><%= step.short_name %> Step</span>
10
+ </h5>
11
+
12
+ <div id="<%= step.id %>-detail" class="<%= step.stepable.id %>-detail" style="display: none;">
13
+ <%= form_with model: step do |f| %>
14
+ <% if step.errors.any? %>
15
+ ERRORS
16
+ <% end %>
17
+
18
+ <div class="form-row">
19
+ <div class="form-group col">
20
+ <%= f.label :include_predicate %>
21
+ <%= f.text_field :include_predicate, class: "form-control", placeholder: "e.g. params.user_id is present" %>
22
+ <small class="form-text text-muted">Use data from the context that evaluates to <code>true</code> or <code>false</code></small>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="form-row">
27
+ <div class="form-group col">
28
+ <%= f.submit 'Update step', class: 'btn btn-outline-primary' %>
29
+ <%= link_to "Delete", step, method: :delete, remote: true, data: { confirm: "Are you sure?" }, class: 'btn btn-link' %>
30
+ </div>
31
+ </div>
32
+
33
+ <% end %>
34
+ </div>
35
+ </li>
@@ -0,0 +1,11 @@
1
+ <div class="card">
2
+ <div class="card-header">
3
+ <h4>Steps</h4>
4
+ <a href="#" onclick="$('.<%= stepable.id %>-detail').toggle(); return false;">Toggle all</a>
5
+ </div>
6
+ <div class="card-body">
7
+ <ul id="<%= stepable.id %>-steps" class="list-group step-container">
8
+ <%= render partial: "riddler_admin/step", collection: stepable.steps %>
9
+ </ul>
10
+ </div>
11
+ </div>
@@ -0,0 +1,44 @@
1
+ <div class="row">
2
+ <% if notice %>
3
+ <div class="col-sm">
4
+ <span id="notice"><%= notice %></span>
5
+ </div>
6
+ <% end %>
7
+ </div>
8
+
9
+ <div class="row">
10
+ <div class="col">
11
+ <h1><%= ::RiddlerAdmin.config.app_name %> Dashboard</h1>
12
+ </div>
13
+ </div>
14
+
15
+ <% if riddler_user_can_approve? %>
16
+ <div class="row">
17
+ <div class="col">
18
+ <h2>Publish Requests</h2>
19
+
20
+ <% unapproved_prs = ::RiddlerAdmin::PublishRequest.unapproved %>
21
+ <% if unapproved_prs.count.zero? %>
22
+ No unapproved requests at this time.
23
+ <% else %>
24
+ <table class="table">
25
+ <thead>
26
+ <tr>
27
+ <th>Title</th>
28
+ <th>Content</th>
29
+ </tr>
30
+ </thead>
31
+
32
+ <tbody>
33
+ <% unapproved_prs.each do |publish_request| %>
34
+ <tr>
35
+ <td><%= link_to publish_request.title, publish_request %></td>
36
+ <td><%= publish_request.content.title %></td>
37
+ </tr>
38
+ <% end %>
39
+ </tbody>
40
+ </table>
41
+ <% end %>
42
+ </div>
43
+ </div>
44
+ <% end %>