mirror of
https://github.com/openSUSE/osem.git
synced 2026-08-13 19:54:02 +00:00
commit
5d5ad9348c
8 changed files with 300 additions and 195 deletions
|
|
@ -95,7 +95,7 @@ Metrics/BlockNesting:
|
|||
Max: 4
|
||||
|
||||
Metrics/ClassLength:
|
||||
Max: 550
|
||||
Max: 560
|
||||
|
||||
# avoid redundunt curly braces when it is obvious that hash is used
|
||||
Style/BracesAroundHashParameters:
|
||||
|
|
|
|||
|
|
@ -57,3 +57,7 @@
|
|||
display: inline-block;
|
||||
padding: 0px 10px 0px 0px;
|
||||
}
|
||||
|
||||
.margin-event-table{
|
||||
margin-top: 40px !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ module Admin
|
|||
@difficulty_levels = @program.difficulty_levels
|
||||
@machine_states = @events.state_machine.states.map
|
||||
@event_types = @program.event_types
|
||||
@tracks_distribution_confirmed = @conference.tracks_distribution(:confirmed)
|
||||
@event_distribution = @conference.event_distribution
|
||||
@scheduled_event_distribution = @conference.scheduled_event_distribution
|
||||
|
||||
@mystates = []
|
||||
@mytypes = []
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ module Admin
|
|||
@pdf_filename = "#{@conference.title}.pdf"
|
||||
@registrations = @conference.registrations.includes(:user).order('registrations.created_at ASC')
|
||||
@attended = @conference.registrations.where('attended = ?', true).count
|
||||
|
||||
@registration_distribution = @conference.registration_distribution
|
||||
@affiliation_distribution = @conference.affiliation_distribution
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
|
|
|||
|
|
@ -316,6 +316,67 @@ class Conference < ActiveRecord::Base
|
|||
Conference.calculate_event_distribution_hash(program.events.select(:state).group(:state).count)
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a hash with scheduled vs unscheduled events
|
||||
# { "Scheduled" => { value: number of confirmed and scheduled events, color: color },
|
||||
# "Unscheduled" => { value: number of confirmed and unscheduled events, color: color }
|
||||
#
|
||||
# ====Returns
|
||||
# * +hash+ -> hash
|
||||
def scheduled_event_distribution
|
||||
confirmed_events = program.events.where(state: 'confirmed')
|
||||
scheduled_value = { 'value' => confirmed_events.where.not(start_time: nil).count, 'color' => 'green' }
|
||||
unscheduled_value = { 'value' => confirmed_events.where(start_time: nil).count, 'color' => 'red' }
|
||||
{ 'Scheduled' => scheduled_value, 'Unscheduled' => unscheduled_value }
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a hash with Registration attended vs. Registration not attended
|
||||
# { "Attended" => { value: number of registration attended, color: color },
|
||||
# "Not attended" => { value: number of registration not attended, color: color }
|
||||
#
|
||||
# ====Returns
|
||||
# * +hash+ -> hash
|
||||
def registration_distribution
|
||||
reg = registrations.includes(:user)
|
||||
attended_value = { 'value' => reg.where(attended: true).count, 'color' => 'magenta' }
|
||||
not_attended_value = { 'value' => reg.where.not(attended: true).count, 'color' => 'blue' }
|
||||
{ 'Attended' => attended_value, 'Not attended' => not_attended_value }
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a hash with affiliation =>
|
||||
# {value: count of registration whose user has that affilation, color: color}
|
||||
# In case that the affiliation is blank, it groups them in None and
|
||||
# if the number of persons that have an affiliation are less than the 2% of
|
||||
# the total number of registered people, they are grouped in others.
|
||||
#
|
||||
# ====Returns
|
||||
# * +hash+ -> hash
|
||||
def affiliation_distribution
|
||||
counted_affiliations = registrations.joins(:user).group(:affiliation).count
|
||||
result = {}
|
||||
i=1
|
||||
others = 0
|
||||
none = 0
|
||||
counted_affiliations.each do |key, value|
|
||||
if value < 0.02 * registrations.length
|
||||
others += value
|
||||
elsif key.blank?
|
||||
none += value
|
||||
else
|
||||
result[key.capitalize] = { 'value' => value, 'color' => next_color(i) }
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
if others > 0
|
||||
result['Others'] = { 'value' => others, 'color' => next_color(i) }
|
||||
i += 1
|
||||
end
|
||||
result['None'] = { 'value' => none, 'color' => next_color(i) } if none > 0
|
||||
result
|
||||
end
|
||||
|
||||
##
|
||||
# Returns a hash with user distribution => {value: count of user state, color: color}
|
||||
# active: signed in during the last 3 months
|
||||
|
|
@ -536,6 +597,20 @@ class Conference < ActiveRecord::Base
|
|||
|
||||
private
|
||||
|
||||
# Returns a different html colour for every i and consecutive colors are
|
||||
# clearly different.
|
||||
def next_color(i)
|
||||
'#' + next_color_component(:r, i) + next_color_component(:g, i) + next_color_component(:b, i)
|
||||
end
|
||||
|
||||
# Auxiliar function which is used in next_color and returns each component of
|
||||
# the color. We make use of big prime numbers to avoid repetition and to make
|
||||
# consecutive colors clearly different.
|
||||
def next_color_component(component, i)
|
||||
big_prime_numbers = {r: 113, g: 67, b: 151}
|
||||
((i*big_prime_numbers[component])%239 + 16).to_s(16)
|
||||
end
|
||||
|
||||
after_create do
|
||||
self.create_contact
|
||||
self.create_program
|
||||
|
|
|
|||
|
|
@ -7,161 +7,169 @@
|
|||
= "(#{@events.length})"
|
||||
%p.text-muted
|
||||
All the submissions of your speakers
|
||||
.row
|
||||
.col-md-4
|
||||
= render partial: 'admin/conference/doughnut_chart', locals: {title: 'Events state', data: @event_distribution}
|
||||
.col-md-4
|
||||
= render partial: 'admin/conference/doughnut_chart', locals: {title: 'Confirmed events scheduled', data: @scheduled_event_distribution}
|
||||
.col-md-4
|
||||
= render partial: 'admin/conference/doughnut_chart', locals: {title: 'Tracks of confirmed events', data: @tracks_distribution_confirmed}
|
||||
.row
|
||||
.col-md-12
|
||||
%table.table.table-striped.table-bordered.table-hover.datatable
|
||||
%thead
|
||||
%th
|
||||
%b ID
|
||||
%th
|
||||
%b Title
|
||||
- if @program.rating_enabled?
|
||||
%div.margin-event-table
|
||||
%table.table.table-striped.table-bordered.table-hover.datatable
|
||||
%thead
|
||||
%th
|
||||
%b Rating
|
||||
%th
|
||||
%b Submitter
|
||||
%th
|
||||
%b Speaker
|
||||
-if @program.languages.present?
|
||||
%b ID
|
||||
%th
|
||||
%b Language
|
||||
%th
|
||||
%b Requires Registration
|
||||
%th
|
||||
%b Highlight
|
||||
%th
|
||||
%b Type
|
||||
%th
|
||||
%b Track
|
||||
%th
|
||||
%b Difficulty
|
||||
%th
|
||||
%b State
|
||||
- @events.each do |event|
|
||||
%tr
|
||||
%td
|
||||
= event.id
|
||||
%td
|
||||
=link_to event.title, admin_conference_program_event_path(@conference.short_title, event)
|
||||
|
||||
%b Title
|
||||
- if @program.rating_enabled?
|
||||
%td.col-md-1{'data-order' => "#{event.average_rating}"}
|
||||
- if event.average_rating.to_f > 0
|
||||
#{event.average_rating}/#{@program.rating}
|
||||
%br
|
||||
#{pluralize(event.voters.length, 'voter')}
|
||||
%br
|
||||
- @program.rating.times do |counter|
|
||||
- if event.average_rating.to_f.round == counter+1
|
||||
= label_tag "label_rating", "", :class => "avgrating", :avgrate => true
|
||||
= javascript_tag "$('label[avgrate=true]').prevAll().andSelf().addClass('bright');"
|
||||
- else
|
||||
= label_tag "label_rating", "", :class => "avgrating"
|
||||
%br
|
||||
- voted = event.voted?(event, current_user)
|
||||
- if voted
|
||||
%span.label.label-success
|
||||
Your rating: #{voted.rating}
|
||||
- else
|
||||
%span.label.label-danger
|
||||
Not rated
|
||||
- else
|
||||
0/#{@program.rating}
|
||||
%br
|
||||
|
||||
- if event.submitter && event.submitter.registrations && event.submitter.registrations.count < 1
|
||||
- bgcolor="#F7819F"
|
||||
- else
|
||||
- bgcolor=""
|
||||
%td{:style=>"background-color: #{bgcolor}"}
|
||||
- if !event.submitter.nil?
|
||||
=link_to event.submitter.name, admin_user_path(event.submitter)
|
||||
- if event.submitter.registrations.count < 1
|
||||
(Unregistered!)
|
||||
- else
|
||||
Unknown submitter
|
||||
%td
|
||||
- if speaker = event.speakers.first
|
||||
= link_to speaker.name, admin_user_path(speaker)
|
||||
- else
|
||||
Unknown speaker
|
||||
|
||||
%th
|
||||
%b Rating
|
||||
%th
|
||||
%b Submitter
|
||||
%th
|
||||
%b Speaker
|
||||
-if @program.languages.present?
|
||||
%th
|
||||
%b Language
|
||||
%th
|
||||
%b Requires Registration
|
||||
%th
|
||||
%b Highlight
|
||||
%th
|
||||
%b Type
|
||||
%th
|
||||
%b Track
|
||||
%th
|
||||
%b Difficulty
|
||||
%th
|
||||
%b State
|
||||
- @events.each do |event|
|
||||
%tr
|
||||
%td
|
||||
= event.language
|
||||
= event.id
|
||||
%td
|
||||
=link_to event.title, admin_conference_program_event_path(@conference.short_title, event)
|
||||
|
||||
%td.text-center{'data-order' => "#{event.require_registration}"}
|
||||
= check_box_tag @conference.short_title, event.id, event.require_registration,
|
||||
method: :patch, url: "/admin/conference/#{@conference.short_title}/program/events/#{event.id}?event[require_registration]=",
|
||||
class: 'switch-checkbox', data: { size: 'small',
|
||||
off_color: 'warning',
|
||||
on_text: 'Yes',
|
||||
off_text: 'No' }
|
||||
- if event.require_registration
|
||||
%br
|
||||
= link_to registered_text(event), registrations_admin_conference_program_event_path(@conference.short_title, event), class: 'btn btn-xs btn-default'
|
||||
|
||||
%td.text-center{'data-order' => "#{event.is_highlight}"}
|
||||
= check_box_tag @conference.short_title, event.id, event.is_highlight,
|
||||
method: :patch, url: "/admin/conference/#{@conference.short_title}/program/events/#{event.id}?event[is_highlight]=",
|
||||
class: 'switch-checkbox', data: { size: 'small',
|
||||
off_color: 'warning',
|
||||
on_text: 'Yes',
|
||||
off_text: 'No' }
|
||||
|
||||
%td
|
||||
.btn-group
|
||||
%button{:type=>"button", :class=>"btn btn-link dropdown-toggle", "data-toggle"=>"dropdown"}
|
||||
- if event.event_type.nil?
|
||||
Event Type
|
||||
- if @program.rating_enabled?
|
||||
%td.col-md-1{'data-order' => "#{event.average_rating}"}
|
||||
- if event.average_rating.to_f > 0
|
||||
#{event.average_rating}/#{@program.rating}
|
||||
%br
|
||||
#{pluralize(event.voters.length, 'voter')}
|
||||
%br
|
||||
- @program.rating.times do |counter|
|
||||
- if event.average_rating.to_f.round == counter+1
|
||||
= label_tag "label_rating", "", :class => "avgrating", :avgrate => true
|
||||
= javascript_tag "$('label[avgrate=true]').prevAll().andSelf().addClass('bright');"
|
||||
- else
|
||||
= label_tag "label_rating", "", :class => "avgrating"
|
||||
%br
|
||||
- voted = event.voted?(event, current_user)
|
||||
- if voted
|
||||
%span.label.label-success
|
||||
Your rating: #{voted.rating}
|
||||
- else
|
||||
%span.label.label-danger
|
||||
Not rated
|
||||
- else
|
||||
= event.event_type.title
|
||||
%span.caret
|
||||
%ul.dropdown-menu
|
||||
- @event_types.each do |type|
|
||||
%li= link_to type.title,
|
||||
admin_conference_program_event_path(@conference.short_title,
|
||||
event,
|
||||
event: { event_type_id: type.id }),
|
||||
method: :patch
|
||||
%td
|
||||
.btn-group
|
||||
%button{:type=>"button", :class=>"btn btn-link dropdown-toggle", "data-toggle"=>"dropdown"}
|
||||
- if event.track.nil?
|
||||
Track
|
||||
- else
|
||||
= event.track.name
|
||||
%span.caret
|
||||
%ul.dropdown-menu
|
||||
- @tracks.each do |track|
|
||||
%li= link_to track.name,
|
||||
admin_conference_program_event_path(@conference.short_title,
|
||||
event,
|
||||
event: { track_id: track.id }),
|
||||
method: :patch
|
||||
%td
|
||||
.btn-group
|
||||
%button{:type=>"button", :class=>"btn btn-link dropdown-toggle", "data-toggle"=>"dropdown"}
|
||||
- if event.difficulty_level.nil?
|
||||
Difficulty
|
||||
- else
|
||||
= event.difficulty_level.title
|
||||
%span.caret
|
||||
%ul.dropdown-menu
|
||||
- @difficulty_levels.each do |difficulty_level|
|
||||
%li= link_to difficulty_level.title,
|
||||
admin_conference_program_event_path(@conference.short_title,
|
||||
event,
|
||||
event: { difficulty_level_id: difficulty_level.id }),
|
||||
method: :patch
|
||||
0/#{@program.rating}
|
||||
%br
|
||||
|
||||
%td
|
||||
- if event.state == "withdrawn"
|
||||
Withdrawn
|
||||
- if event.submitter && event.submitter.registrations && event.submitter.registrations.count < 1
|
||||
- bgcolor="#F7819F"
|
||||
- else
|
||||
- bgcolor=""
|
||||
%td{:style=>"background-color: #{bgcolor}"}
|
||||
- if !event.submitter.nil?
|
||||
=link_to event.submitter.name, admin_user_path(event.submitter)
|
||||
- if event.submitter.registrations.count < 1
|
||||
(Unregistered!)
|
||||
- else
|
||||
Unknown submitter
|
||||
%td
|
||||
- if speaker = event.speakers.first
|
||||
= link_to speaker.name, admin_user_path(speaker)
|
||||
- else
|
||||
Unknown speaker
|
||||
|
||||
-if @program.languages.present?
|
||||
%td
|
||||
= event.language
|
||||
|
||||
%td.text-center{'data-order' => "#{event.require_registration}"}
|
||||
= check_box_tag @conference.short_title, event.id, event.require_registration,
|
||||
method: :patch, url: "/admin/conference/#{@conference.short_title}/program/events/#{event.id}?event[require_registration]=",
|
||||
class: 'switch-checkbox', data: { size: 'small',
|
||||
off_color: 'warning',
|
||||
on_text: 'Yes',
|
||||
off_text: 'No' }
|
||||
- if event.require_registration
|
||||
%br
|
||||
= link_to registered_text(event), registrations_admin_conference_program_event_path(@conference.short_title, event), class: 'btn btn-xs btn-default'
|
||||
|
||||
%td.text-center{'data-order' => "#{event.is_highlight}"}
|
||||
= check_box_tag @conference.short_title, event.id, event.is_highlight,
|
||||
method: :patch, url: "/admin/conference/#{@conference.short_title}/program/events/#{event.id}?event[is_highlight]=",
|
||||
class: 'switch-checkbox', data: { size: 'small',
|
||||
off_color: 'warning',
|
||||
on_text: 'Yes',
|
||||
off_text: 'No' }
|
||||
|
||||
%td
|
||||
.btn-group
|
||||
%button{:type=>"button", :class=>"btn btn-link dropdown-toggle", "data-toggle"=>"dropdown"}
|
||||
= event.state.humanize
|
||||
- if event.event_type.nil?
|
||||
Event Type
|
||||
- else
|
||||
= event.event_type.title
|
||||
%span.caret
|
||||
%ul.dropdown-menu{:role=>"menu"}
|
||||
= render 'change_state_dropdown', event: event
|
||||
%ul.dropdown-menu
|
||||
- @event_types.each do |type|
|
||||
%li= link_to type.title,
|
||||
admin_conference_program_event_path(@conference.short_title,
|
||||
event,
|
||||
event: { event_type_id: type.id }),
|
||||
method: :patch
|
||||
%td
|
||||
.btn-group
|
||||
%button{:type=>"button", :class=>"btn btn-link dropdown-toggle", "data-toggle"=>"dropdown"}
|
||||
- if event.track.nil?
|
||||
Track
|
||||
- else
|
||||
= event.track.name
|
||||
%span.caret
|
||||
%ul.dropdown-menu
|
||||
- @tracks.each do |track|
|
||||
%li= link_to track.name,
|
||||
admin_conference_program_event_path(@conference.short_title,
|
||||
event,
|
||||
event: { track_id: track.id }),
|
||||
method: :patch
|
||||
%td
|
||||
.btn-group
|
||||
%button{:type=>"button", :class=>"btn btn-link dropdown-toggle", "data-toggle"=>"dropdown"}
|
||||
- if event.difficulty_level.nil?
|
||||
Difficulty
|
||||
- else
|
||||
= event.difficulty_level.title
|
||||
%span.caret
|
||||
%ul.dropdown-menu
|
||||
- @difficulty_levels.each do |difficulty_level|
|
||||
%li= link_to difficulty_level.title,
|
||||
admin_conference_program_event_path(@conference.short_title,
|
||||
event,
|
||||
event: { difficulty_level_id: difficulty_level.id }),
|
||||
method: :patch
|
||||
|
||||
%td
|
||||
- if event.state == "withdrawn"
|
||||
Withdrawn
|
||||
- else
|
||||
.btn-group
|
||||
%button{:type=>"button", :class=>"btn btn-link dropdown-toggle", "data-toggle"=>"dropdown"}
|
||||
= event.state.humanize
|
||||
%span.caret
|
||||
%ul.dropdown-menu{:role=>"menu"}
|
||||
= render 'change_state_dropdown', event: event
|
||||
|
|
|
|||
|
|
@ -11,57 +11,65 @@
|
|||
= link_to 'Export XLS', {format: :xlsx}, class: 'btn btn-success'
|
||||
%p.text-muted
|
||||
All the people who registered to your event
|
||||
%table.table.table-hover.datatable#registrations
|
||||
%thead
|
||||
%tr
|
||||
%th ID#
|
||||
%th Name
|
||||
%th E-Mail
|
||||
%th Arrival
|
||||
%th Departure
|
||||
- if @conference.questions.any?
|
||||
%th Questions
|
||||
%th Actions
|
||||
%tbody
|
||||
- @registrations.each_with_index do |registration, index|
|
||||
.col-md-4
|
||||
= render partial: 'admin/conference/doughnut_chart', locals: {title: 'Affiliation', data: @affiliation_distribution}
|
||||
- unless @conference.pending?
|
||||
.col-md-4
|
||||
= render partial: 'admin/conference/doughnut_chart', locals: {title: 'Attended registrations', data: @registration_distribution}
|
||||
.row
|
||||
.col-md-12
|
||||
%div.margin-event-table
|
||||
%table.table.table-hover.datatable#registrations
|
||||
%thead
|
||||
%tr
|
||||
%td
|
||||
= registration.id
|
||||
%td
|
||||
= registration.name.present? ? registration.name : registration.username
|
||||
%br
|
||||
- registration.user.roles.where(resource: @conference).each do |role|
|
||||
%span.label.label-info
|
||||
= role.name.titleize
|
||||
%td
|
||||
= registration.email
|
||||
%td
|
||||
- if registration.arrival
|
||||
= registration.arrival.strftime('%d %b %H:%M')
|
||||
- else
|
||||
n/a
|
||||
%td
|
||||
- if registration.departure
|
||||
= registration.departure.strftime('%d %b %H:%M')
|
||||
- else
|
||||
n/a
|
||||
-if @conference.questions.any?
|
||||
%th ID#
|
||||
%th Name
|
||||
%th E-Mail
|
||||
%th Arrival
|
||||
%th Departure
|
||||
- if @conference.questions.any?
|
||||
%th Questions
|
||||
%th Actions
|
||||
%tbody
|
||||
- @registrations.each_with_index do |registration, index|
|
||||
%tr
|
||||
%td
|
||||
= link_to 'Questions','#', class: 'btn btn-success question-btn', 'data-id' => index, 'data-name' => registration.name
|
||||
%td
|
||||
= check_box_tag "#{@conference.short_title}_#{registration.id}", registration.id, registration.attended,
|
||||
class: 'switch-checkbox', method: :patch,
|
||||
url: toggle_attendance_admin_conference_registration_path(@conference.short_title, id: registration.id)+"?attended=",
|
||||
data: { size: 'small',
|
||||
on_color: 'success',
|
||||
off_color: 'warning',
|
||||
on_text: 'Present',
|
||||
off_text: 'Absent' }
|
||||
.btn-group
|
||||
= link_to 'Edit', edit_admin_conference_registration_path(@conference.short_title, id: registration),
|
||||
method: :get, class: 'btn btn-primary'
|
||||
= link_to 'Delete', admin_conference_registration_path(@conference.short_title, registration),
|
||||
method: :delete, class: 'btn btn-danger', data: { confirm: "Do you really want to delete the Registration for #{registration.name}?" }
|
||||
= registration.id
|
||||
%td
|
||||
= registration.name.present? ? registration.name : registration.username
|
||||
%br
|
||||
- registration.user.roles.where(resource: @conference).each do |role|
|
||||
%span.label.label-info
|
||||
= role.name.titleize
|
||||
%td
|
||||
= registration.email
|
||||
%td
|
||||
- if registration.arrival
|
||||
= registration.arrival.strftime('%d %b %H:%M')
|
||||
- else
|
||||
n/a
|
||||
%td
|
||||
- if registration.departure
|
||||
= registration.departure.strftime('%d %b %H:%M')
|
||||
- else
|
||||
n/a
|
||||
-if @conference.questions.any?
|
||||
%td
|
||||
= link_to 'Questions','#', class: 'btn btn-success question-btn', 'data-id' => index, 'data-name' => registration.name
|
||||
%td
|
||||
= check_box_tag "#{@conference.short_title}_#{registration.id}", registration.id, registration.attended,
|
||||
class: 'switch-checkbox', method: :patch,
|
||||
url: toggle_attendance_admin_conference_registration_path(@conference.short_title, id: registration.id)+"?attended=",
|
||||
data: { size: 'small',
|
||||
on_color: 'success',
|
||||
off_color: 'warning',
|
||||
on_text: 'Present',
|
||||
off_text: 'Absent' }
|
||||
.btn-group
|
||||
= link_to 'Edit', edit_admin_conference_registration_path(@conference.short_title, id: registration),
|
||||
method: :get, class: 'btn btn-primary'
|
||||
= link_to 'Delete', admin_conference_registration_path(@conference.short_title, registration),
|
||||
method: :delete, class: 'btn btn-danger', data: { confirm: "Do you really want to delete the Registration for #{registration.name}?" }
|
||||
- @registrations.each_with_index do |registration, index|
|
||||
.questions{class: "question#{index}", style: 'display:none;'}
|
||||
= render partial: 'questions', locals: { registration: registration }
|
||||
|
|
|
|||
|
|
@ -343,9 +343,13 @@ ActiveRecord::Schema.define(version: 20160624151257) do
|
|||
t.boolean "include_registrations"
|
||||
t.boolean "include_sponsors"
|
||||
t.boolean "include_lodgings"
|
||||
t.string "banner_photo_file_name"
|
||||
t.string "banner_photo_content_type"
|
||||
t.integer "banner_photo_file_size"
|
||||
t.datetime "banner_photo_updated_at"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.boolean "include_cfp", default: false
|
||||
t.boolean "include_cfp", default: false
|
||||
end
|
||||
|
||||
create_table "sponsors", force: :cascade do |t|
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue