Merge branch 'master' of github.com:snap-cloud/snapcon into admin-ticketing
* 'master' of github.com:snap-cloud/snapcon: (22 commits) Cleanup some Auth specs and weird haml syntax add sentry puma hooks fix user model spec Tweak Tickets Text fix missing ] Fix specs for omniauth providers? Update stripe mock gem remove airbrake Add sentry logging Update stripe gem Cleanup stripe stuff Fix the docker init script Cleanup specs Cleanup config file, ensure providers are set if present Fix format spec with new markdown options Try to update spec file with image in navbar Delint Delint helpers Lint: Sort gems Update user model spec ...
This commit is contained in:
commit
be991c53d3
18 changed files with 113 additions and 96 deletions
12
Gemfile
12
Gemfile
|
|
@ -20,7 +20,6 @@ gem 'puma', '~> 3.0'
|
|||
gem 'responders', '~> 2.0'
|
||||
|
||||
# as supported databases
|
||||
# gem 'mysql2'
|
||||
gem 'pg'
|
||||
|
||||
# for tracking data changes
|
||||
|
|
@ -39,11 +38,11 @@ gem 'devise'
|
|||
gem 'devise_ichain_authenticatable'
|
||||
|
||||
gem 'omniauth'
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-discourse'
|
||||
# gem 'omniauth-openid'
|
||||
# gem 'omniauth-facebook'
|
||||
# gem 'omniauth-github'
|
||||
gem 'omniauth-facebook'
|
||||
gem 'omniauth-github'
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-openid'
|
||||
|
||||
# Bot-filtering
|
||||
gem 'recaptcha', require: 'recaptcha/rails'
|
||||
|
|
@ -143,8 +142,7 @@ gem 'rqrcode'
|
|||
gem 'axlsx', git: 'https://github.com/randym/axlsx.git'
|
||||
gem 'axlsx_rails'
|
||||
|
||||
# as error catcher
|
||||
gem 'airbrake'
|
||||
gem 'sentry'
|
||||
|
||||
# to make links faster
|
||||
gem 'turbolinks'
|
||||
|
|
|
|||
36
Gemfile.lock
36
Gemfile.lock
|
|
@ -68,10 +68,6 @@ GEM
|
|||
addressable (2.6.0)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
afm (0.2.2)
|
||||
airbrake (9.2.2)
|
||||
airbrake-ruby (~> 4.4)
|
||||
airbrake-ruby (4.4.0)
|
||||
rbtree3 (~> 0.5)
|
||||
ajax-datatables-rails (0.4.3)
|
||||
railties (>= 4.0)
|
||||
archive-zip (0.12.0)
|
||||
|
|
@ -130,7 +126,6 @@ GEM
|
|||
url
|
||||
coderay (1.1.1)
|
||||
concurrent-ruby (1.1.5)
|
||||
connection_pool (2.2.2)
|
||||
countable-rails (0.0.1)
|
||||
railties (>= 3.1)
|
||||
countries (3.0.0)
|
||||
|
|
@ -301,12 +296,10 @@ GEM
|
|||
monetize (~> 1.9.0)
|
||||
money (~> 6.13.2)
|
||||
railties (>= 3.0)
|
||||
multi_json (1.13.1)
|
||||
multi_json (1.14.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
nenv (0.3.0)
|
||||
net-http-persistent (3.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.5)
|
||||
|
|
@ -325,6 +318,11 @@ GEM
|
|||
rack (>= 1.6.2, < 3)
|
||||
omniauth-discourse (1.0.0)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-facebook (6.0.0)
|
||||
omniauth-oauth2 (~> 1.2)
|
||||
omniauth-github (1.3.0)
|
||||
omniauth (~> 1.5)
|
||||
omniauth-oauth2 (>= 1.4.0, < 2.0)
|
||||
omniauth-google-oauth2 (0.6.1)
|
||||
jwt (>= 2.0)
|
||||
omniauth (>= 1.1.1)
|
||||
|
|
@ -332,6 +330,9 @@ GEM
|
|||
omniauth-oauth2 (1.6.0)
|
||||
oauth2 (~> 1.1)
|
||||
omniauth (~> 1.9)
|
||||
omniauth-openid (1.0.1)
|
||||
omniauth (~> 1.0)
|
||||
rack-openid (~> 1.3.1)
|
||||
open4 (1.3.4)
|
||||
orm_adapter (0.5.0)
|
||||
paper_trail (10.3.1)
|
||||
|
|
@ -373,6 +374,9 @@ GEM
|
|||
public_suffix (3.1.1)
|
||||
puma (3.12.2)
|
||||
rack (2.0.8)
|
||||
rack-openid (1.3.1)
|
||||
rack (>= 1.1.0)
|
||||
ruby-openid (>= 2.1.8)
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (5.2.3)
|
||||
|
|
@ -431,7 +435,6 @@ GEM
|
|||
rb-fsevent (0.10.3)
|
||||
rb-inotify (0.10.0)
|
||||
ffi (~> 1.0)
|
||||
rbtree3 (0.5.0)
|
||||
rdoc (6.0.4)
|
||||
rdoc-generator-fivefish (0.4.0)
|
||||
inversion (~> 1.1)
|
||||
|
|
@ -489,6 +492,7 @@ GEM
|
|||
rubocop-rspec (1.36.0)
|
||||
rubocop (>= 0.68.1)
|
||||
ruby-oembed (0.12.0)
|
||||
ruby-openid (2.9.2)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-rc4 (0.1.5)
|
||||
ruby_dep (1.5.0)
|
||||
|
|
@ -514,6 +518,7 @@ GEM
|
|||
selenium-webdriver (3.142.6)
|
||||
childprocess (>= 0.5, < 4.0)
|
||||
rubyzip (>= 1.2.2)
|
||||
sentry (0.5.3)
|
||||
sexp_processor (4.11.0)
|
||||
shellany (0.0.1)
|
||||
shoulda-matchers (4.1.2)
|
||||
|
|
@ -542,13 +547,11 @@ GEM
|
|||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sqlite3 (1.4.1)
|
||||
stripe (4.21.3)
|
||||
faraday (~> 0.13)
|
||||
net-http-persistent (~> 3.0)
|
||||
stripe-ruby-mock (2.5.8)
|
||||
stripe (5.14.0)
|
||||
stripe-ruby-mock (3.0.1)
|
||||
dante (>= 0.2.0)
|
||||
multi_json (~> 1.0)
|
||||
stripe (>= 2.0.3)
|
||||
stripe (> 5, < 6)
|
||||
sysexits (1.2.0)
|
||||
temple (0.8.0)
|
||||
thor (0.20.3)
|
||||
|
|
@ -605,7 +608,6 @@ DEPENDENCIES
|
|||
active_model_serializers
|
||||
acts_as_commentable_with_threading
|
||||
acts_as_list
|
||||
airbrake
|
||||
ajax-datatables-rails
|
||||
autoprefixer-rails
|
||||
awesome_nested_set
|
||||
|
|
@ -657,7 +659,10 @@ DEPENDENCIES
|
|||
nokogiri (>= 1.8.1)
|
||||
omniauth
|
||||
omniauth-discourse
|
||||
omniauth-facebook
|
||||
omniauth-github
|
||||
omniauth-google-oauth2
|
||||
omniauth-openid
|
||||
paper_trail
|
||||
pdf-inspector
|
||||
pg
|
||||
|
|
@ -693,6 +698,7 @@ DEPENDENCIES
|
|||
ruby-oembed
|
||||
sass-rails (>= 4.0.2)
|
||||
selectize-rails
|
||||
sentry
|
||||
shoulda-matchers
|
||||
skylight
|
||||
spring-commands-rspec
|
||||
|
|
|
|||
|
|
@ -178,20 +178,22 @@ module ApplicationHelper
|
|||
'hidden' if Date.today > conference.end_date
|
||||
end
|
||||
|
||||
# TODO:Snap!Con: Replace this with a search for a conference logo.
|
||||
def nav_root_link_for(conference)
|
||||
link_text = (
|
||||
conference.try(:organization).try(:name) ||
|
||||
ENV['OSEM_NAME'] ||
|
||||
'OSEM'
|
||||
)
|
||||
link_to(
|
||||
image_tag('snapcon_logo.png'),
|
||||
root_path,
|
||||
class: 'navbar-brand',
|
||||
title: 'Open Source Event Manager'
|
||||
title: nav_link_text(conference)
|
||||
)
|
||||
end
|
||||
|
||||
def nav_link_text(conference)
|
||||
conference.try(:organization).try(:name) ||
|
||||
ENV['OSEM_NAME'] ||
|
||||
'OSEM'
|
||||
end
|
||||
|
||||
# returns the url to be used for logo on basis of sponsorship level position
|
||||
def get_logo(object)
|
||||
if object.try(:sponsorship_level)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'redcarpet/render_strip'
|
||||
|
||||
module FormatHelper
|
||||
|
|
@ -43,12 +44,11 @@ module FormatHelper
|
|||
|
||||
def target_progress_color(progress)
|
||||
progress = progress.to_i
|
||||
result =
|
||||
case
|
||||
when progress >= 90 then 'green'
|
||||
when progress < 90 && progress >= 80 then 'orange'
|
||||
else 'red'
|
||||
end
|
||||
result = case
|
||||
when progress >= 90 then 'green'
|
||||
when progress < 90 && progress >= 80 then 'orange'
|
||||
else 'red'
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
|
@ -100,19 +100,11 @@ module FormatHelper
|
|||
end
|
||||
|
||||
def icon_for_todo(bool)
|
||||
if bool
|
||||
'fa fa-check'
|
||||
else
|
||||
'fa fa-times'
|
||||
end
|
||||
bool ? 'fa fa-check' : 'fa fa-times'
|
||||
end
|
||||
|
||||
def class_for_todo(bool)
|
||||
if bool
|
||||
'todolist-ok'
|
||||
else
|
||||
'todolist-missing'
|
||||
end
|
||||
bool ? 'todolist-ok' : 'todolist-missing'
|
||||
end
|
||||
|
||||
def word_pluralize(count, singular, plural = nil)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,10 @@ class User < ApplicationRecord
|
|||
else
|
||||
[:database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :trackable, :validatable, :confirmable,
|
||||
:omniauthable, omniauth_providers: [:google, :discourse]]
|
||||
:omniauthable,
|
||||
# omniauth_providers: [:suse, :google, :facebook, :github, :discourse]
|
||||
omniauth_providers: [:google, :discourse]
|
||||
]
|
||||
end
|
||||
|
||||
devise(*devise_modules)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
.container
|
||||
.row
|
||||
.col-md-12.text-center
|
||||
%h2 Sign Up for Campus Housing
|
||||
%h2 Sign Up for Snap<em>!</em>Con
|
||||
%br
|
||||
-# %h2
|
||||
-# Support
|
||||
|
|
@ -15,8 +15,7 @@
|
|||
.row.row-centered
|
||||
- tickets.each do |ticket|
|
||||
.col-lg-4.col-md-3.col-sm-3.col-centered.col-top
|
||||
= link_to(conference_tickets_path(conference.short_title),
|
||||
class: 'thumbnail') do
|
||||
= link_to(conference_tickets_path(conference.short_title), class: 'thumbnail') do
|
||||
.caption
|
||||
%h3.text-center.word_break
|
||||
= ticket.title
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
title: "Your #{provider} login" do
|
||||
%i{class: "fa fa-#{provider}"}
|
||||
- else
|
||||
= link_to("Snap!",
|
||||
"user_#{provider}_omniauth_authorize".to_sym,
|
||||
= link_to("user_#{provider}_omniauth_authorize".to_sym,
|
||||
class: "btn btn-success btn-lg",
|
||||
id: "omniauth-#{provider}",
|
||||
title: "Your #{provider} login")
|
||||
title: "Your #{provider} login") do
|
||||
%span
|
||||
Snap
|
||||
%em> !
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.div
|
||||
.col-md-12.table-responsive
|
||||
%table.table.table-hover
|
||||
%table.table.table-hover.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th Ticket
|
||||
|
|
@ -20,8 +20,13 @@
|
|||
= humanized_money_with_symbol ticket.quantity * ticket.price
|
||||
|
||||
= form_tag conference_payments_path do
|
||||
%script.stripe-button{ src: "https://checkout.stripe.com/checkout.js",
|
||||
data: { amount: @total_amount_to_pay.cents, label: "Pay #{humanized_money_with_symbol @total_amount_to_pay}",
|
||||
email: current_user.email, currency: @total_amount_to_pay.currency, name: ENV['OSEM_NAME'] || 'OSEM',
|
||||
description: "book your tickets", key: Rails.application.secrets.stripe_publishable_key, locale: "auto"}}
|
||||
= link_to 'Edit Purchase', conference_tickets_path(@conference.short_title), class: 'btn btn-default'
|
||||
%script.stripe-button{'src': "https://checkout.stripe.com/checkout.js",
|
||||
'data': {amount: @total_amount_to_pay.cents,
|
||||
label: "Pay #{humanized_money_with_symbol @total_amount_to_pay}",
|
||||
email: current_user.email,
|
||||
currency: @total_amount_to_pay.currency,
|
||||
name: ENV['OSEM_NAME'] || 'OSEM',
|
||||
description: "#{ENV['OSEM_NAME']} tickets",
|
||||
key: ENV['STRIPE_PUBLISHABLE_KEY'] || Rails.application.secrets.stripe_publishable_key, locale: "auto"}}
|
||||
= link_to('Edit Purchase', conference_tickets_path(@conference.short_title),
|
||||
class: 'btn btn-default')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/bin/bash
|
||||
bundle install
|
||||
# Setup the app if it isn't already setup
|
||||
bundle exec rake db:bootstrap
|
||||
# Start the app
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
Airbrake.configure do |config|
|
||||
# Change this to some sensible data for your errbit instance
|
||||
config.project_id = ENV['OSEM_ERRBIT_ID'] || Rails.application.secrets.errbit_id || ''
|
||||
config.project_key = ENV['OSEM_ERRBIT_KEY'] || Rails.application.secrets.errbit_key || ''
|
||||
config.host = ENV['OSEM_ERRBIT_HOST']
|
||||
config.environment = Rails.env
|
||||
if config.project_key.blank? || config.host.blank?
|
||||
config.ignore_environments = [:production, :development, :test]
|
||||
else
|
||||
config.ignore_environments = [:development, :test]
|
||||
end
|
||||
end
|
||||
|
||||
Airbrake.add_filter do |notice|
|
||||
notice.ignore! if notice[:errors].any? { |error| error[:type] == 'ActiveRecord::RecordNotFound' }
|
||||
notice.ignore! if notice[:errors].any? { |error| error[:type] == 'ActionController::InvalidAuthenticityToken' }
|
||||
notice.ignore! if notice[:errors].any? { |error| error[:type] == 'ActionController::UnknownAction' }
|
||||
notice.ignore! if notice[:errors].any? { |error| error[:type] == 'AbstractController::ActionNotFound' }
|
||||
notice.ignore! if notice[:errors].any? { |error| error[:type] == 'ActionView::MissingTemplate' }
|
||||
notice.ignore! if notice[:errors].any? { |error| error[:type] == 'ActionController::UnknownFormat' }
|
||||
notice.ignore! if notice[:errors].any? { |error| error[:type] == 'ActionController::RoutingError' && error[:message] =~ %r{\[GET\]} }
|
||||
end
|
||||
|
|
@ -8,16 +8,23 @@ Devise.setup do |config|
|
|||
|
||||
# config.omniauth :open_id, name: 'suse', identifier: 'http://www.opensuse.org/openid/user'
|
||||
|
||||
config.omniauth :google_oauth2, (ENV['OSEM_GOOGLE_KEY'] || Rails.application.secrets.google_key), (ENV['OSEM_GOOGLE_SECRET'] || Rails.application.secrets.google_secret),
|
||||
config.omniauth :google_oauth2,
|
||||
(ENV['OSEM_GOOGLE_KEY'] || Rails.application.secrets.google_key),
|
||||
(ENV['OSEM_GOOGLE_SECRET'] || Rails.application.secrets.google_secret),
|
||||
name: 'google',
|
||||
scope: 'email'
|
||||
|
||||
config.omniauth :discourse,
|
||||
sso_url: "https://forum.snap.berkeley.edu/session/sso_provider",
|
||||
sso_url: 'https://forum.snap.berkeley.edu/session/sso_provider',
|
||||
sso_secret: ENV['OSEM_DISCOURSE_SECRET']
|
||||
|
||||
# config.omniauth :facebook, (ENV['OSEM_FACEBOOK_KEY'] || Rails.application.secrets.facebook_key), (ENV['OSEM_FACEBOOK_SECRET'] || Rails.application.secrets.facebook_secret)
|
||||
# config.omniauth :github, (ENV['OSEM_GITHUB_KEY'] || Rails.application.secrets.github_key), (ENV['OSEM_GITHUB_SECRET'] || Rails.application.secrets.github_secret)
|
||||
config.omniauth :facebook,
|
||||
(ENV['OSEM_FACEBOOK_KEY'] || Rails.application.secrets.facebook_key),
|
||||
(ENV['OSEM_FACEBOOK_SECRET'] || Rails.application.secrets.facebook_secret)
|
||||
|
||||
config.omniauth :github,
|
||||
(ENV['OSEM_GITHUB_KEY'] || Rails.application.secrets.github_key),
|
||||
(ENV['OSEM_GITHUB_SECRET'] || Rails.application.secrets.github_secret)
|
||||
|
||||
# ==> Mailer Configuration
|
||||
# Configure the e-mail address which will be shown in Devise::Mailer,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Stripe.api_key = Rails.application.secrets.stripe_secret_key
|
||||
Stripe.api_key = ENV['STRIPE_SECRET_KEY'] || Rails.application.secrets.stripe_secret_key
|
||||
|
|
|
|||
|
|
@ -43,5 +43,16 @@ on_worker_boot do
|
|||
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
|
||||
end
|
||||
|
||||
lowlevel_error_handler do |ex, env|
|
||||
Raven.capture_exception(
|
||||
ex,
|
||||
:message => ex.message,
|
||||
:extra => { :puma => env },
|
||||
:transaction => "Puma"
|
||||
)
|
||||
# note the below is just a Rack response
|
||||
[500, {}, ["An error has occurred, and engineers have been informed. Please reload the page. If you continue to have problems, contact conference@snap.berkeley.edu\n"]]
|
||||
end
|
||||
|
||||
# Allow puma to be restarted by `rails restart` command.
|
||||
plugin :tmp_restart
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@ feature Conference do
|
|||
sign_in user
|
||||
visit admin_conference_path(conference.short_title)
|
||||
|
||||
expect(find('.navbar-brand').text).to eq(conference.organization.name)
|
||||
# expect(find('.navbar-brand')).to eq(conference.organization.name)
|
||||
expect(find('.navbar-brand img')['alt']).to have_content conference.organization.name
|
||||
end
|
||||
|
||||
it_behaves_like 'add and update conference'
|
||||
|
|
|
|||
|
|
@ -65,16 +65,19 @@ describe ApplicationHelper, type: :helper do
|
|||
describe 'navigation title link' do
|
||||
it 'should default to OSEM' do
|
||||
ENV.delete('OSEM_NAME')
|
||||
expect(nav_root_link_for(nil)).to match 'OSEM'
|
||||
# TODO:Snap!Con: expect(nav_root_link_for(nil)).to match 'OSEM'
|
||||
expect(nav_root_link_for(nil)).to match image_tag('snapcon_logo.png')
|
||||
end
|
||||
|
||||
it 'should use the environment variable' do
|
||||
ENV['OSEM_NAME'] = Faker::Company.name + "'"
|
||||
expect(nav_root_link_for(nil)).to match h(ENV['OSEM_NAME'])
|
||||
# expect(nav_root_link_for(nil)).to match h(ENV['OSEM_NAME'])
|
||||
expect(nav_root_link_for(nil)).to match image_tag('snapcon_logo.png')
|
||||
end
|
||||
|
||||
it 'should use the conference organization name' do
|
||||
expect(nav_root_link_for(conference)).to match h(conference.organization.name)
|
||||
# expect(nav_root_link_for(conference)).to match h(conference.organization.name)
|
||||
expect(nav_root_link_for(nil)).to match image_tag('snapcon_logo.png')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,7 +11,15 @@ describe FormatHelper, type: :helper do
|
|||
|
||||
it 'should return HTML for header markdown' do
|
||||
expect(Redcarpet::Markdown).to receive(:new)
|
||||
.with(Redcarpet::Render::HTML, autolink: true, space_after_headers: true, no_intra_emphasis: true)
|
||||
.with(
|
||||
Redcarpet::Render::HTML,
|
||||
autolink: true,
|
||||
space_after_headers: true,
|
||||
tables: true,
|
||||
strikethrough: true,
|
||||
footnotes: true,
|
||||
superscript: true
|
||||
)
|
||||
.and_call_original
|
||||
|
||||
expect(markdown('# this is my header')).to eq "<h1>this is my header</h1>\n"
|
||||
|
|
|
|||
|
|
@ -468,7 +468,8 @@ describe User do
|
|||
|
||||
describe '.omniauth_providers' do
|
||||
it 'contains providers' do
|
||||
expect(User.omniauth_providers).to eq [:suse, :google, :facebook, :github]
|
||||
# expect(User.omniauth_providers).to eq [:suse, :google, :facebook, :github]
|
||||
expect(User.omniauth_providers).to eq [:google, :discourse]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ module OmniauthMacros
|
|||
|
||||
ENV['OSEM_GOOGLE_KEY'] = 'test key google'
|
||||
ENV['OSEM_GOOGLE_SECRET'] = 'test secret google'
|
||||
ENV['OSEM_FACEBOOK_KEY'] = 'test key facebook'
|
||||
ENV['OSEM_FACEBOOK_SECRET'] = 'test secret facebook'
|
||||
ENV['OSEM_SUSE_KEY'] = 'test key suse'
|
||||
ENV['OSEM_SUSE_SECRET'] = 'test secret suse'
|
||||
ENV['OSEM_GITHUB_KEY'] = 'test key github'
|
||||
ENV['OSEM_GITHUB_SECRET'] = 'test secret github'
|
||||
# ENV['OSEM_FACEBOOK_KEY'] = 'test key facebook'
|
||||
# ENV['OSEM_FACEBOOK_SECRET'] = 'test secret facebook'
|
||||
# ENV['OSEM_SUSE_KEY'] = 'test key suse'
|
||||
# ENV['OSEM_SUSE_SECRET'] = 'test secret suse'
|
||||
# ENV['OSEM_GITHUB_KEY'] = 'test key github'
|
||||
# ENV['OSEM_GITHUB_SECRET'] = 'test secret github'
|
||||
|
||||
def mock_auth_new_user
|
||||
OmniAuth.config.mock_auth[:google] =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue