Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sharing feature #410

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,47 @@ cache:

Of course, you can force a refresh at any time.

### Sharing (optional)

You can generate sharing urls for queries. You can download up-to-date results for each query in CSV directly.

This is useful for scripts or for automatic importing into spreadsheets.

There are 2 steps necessary for setting up sharing:

1. Configuring an API key
2. Make the sharing endpoint accessible in your routes

First configure an API key in `blazer.yml`:

```yml
sharing:
api_key: 'secret'
```

Alternatively you can set the `BLAZER_DOWNLOAD_API_KEY` ENV var which blazer uses by default.

Now routes: we assume you have secured blazer so you will need to expose a new route outside of the mount.

The default path for shares is `/blazer_share`. You can change this in `blazer.yml`:

```yml
sharing:
path: /another_path
```

This config is only so that blazer can generate the correct url.

Now add this route to your `routes.rb`:

```ruby
get Blazer.sharing.route_path, to: Blazer.sharing.to_controller if Blazer.sharing.enabled?
```

Now restart your server and each query page will have a `share` button which will open up a modal that allows you to copy sharing urls.

Each url has a unique token based on a hash of the query's id and the API key, so the token can't be reused for other queries.

## Charts

Blazer will automatically generate charts based on the types of the columns returned in your query.
Expand Down
12 changes: 11 additions & 1 deletion app/controllers/blazer/queries_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ def show
def edit
end

def share
return render_forbidden unless params[:token] && params[:query_id]

@query = Query.find_by(id: params[:query_id]) if params[:query_id]
return render_forbidden unless @query.correct_token?(params[:token])

run
end

def run
@query = Query.find_by(id: params[:query_id]) if params[:query_id]

Expand All @@ -92,7 +101,8 @@ def run
data_source ||= params[:data_source]
@data_source = Blazer.data_sources[data_source]

@statement = Blazer::Statement.new(params[:statement], @data_source)
sql_statement = params[:statement] || @query.statement
@statement = Blazer::Statement.new(sql_statement, @data_source)
# before process_vars
@cohort_analysis = @statement.cohort_analysis?

Expand Down
6 changes: 6 additions & 0 deletions app/models/blazer/query.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Blazer
class Query < Record
has_secure_token :secret_token, length: 36

belongs_to :creator, optional: true, class_name: Blazer.user_class.to_s if Blazer.user_class
has_many :checks, dependent: :destroy
has_many :dashboard_queries, dependent: :destroy
Expand All @@ -15,6 +17,10 @@ def to_param
[id, name].compact.join("-").gsub("'", "").parameterize
end

def correct_token?(token)
ActiveSupport::SecurityUtils.secure_compare(secret_token, token)
end

def friendly_name
name.to_s.sub(/\A[#\*]/, "").gsub(/\[.+\]/, "").strip
end
Expand Down
25 changes: 25 additions & 0 deletions app/views/blazer/queries/_sharing_modal.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div class="modal fade" id="sharingModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">Sharing</h4>
</div>
<div class="modal-body">
Below are some shareable links to this query. Copy and share them.

Anyone with these urls can access the results of this query!

<div class="form-group">
<label for="csvShare">CSV</label>
<input type="text" class="form-control" id="csvShare" readonly value="<%= Blazer.sharing.url_for(@query.id, request.url, format: 'csv') %>">
</div>

<div class="form-group">
<label for="csvShare">Google sheets</label>
<input type="text" class="form-control" id="googleShare" readonly value='=IMPORTDATA("<%= Blazer.sharing.url_for(@query.id, request.url, format: 'csv') %>")'>
</div>
</div>
</div>
</div>
</div>
12 changes: 10 additions & 2 deletions app/views/blazer/queries/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
<div class="topbar">
<div class="container">
<div class="row" style="padding-top: 13px;">
<div class="col-sm-9">
<div class="col-sm-8">
<%= render partial: "blazer/nav" %>
<h3 style="line-height: 34px; display: inline; margin-left: 5px;">
<%= @query.name %>
</h3>
</div>
<div class="col-sm-3 text-right">
<div class="col-sm-4 text-right">
<%= link_to "Edit", edit_query_path(@query, params: variable_params(@query)), class: "btn btn-default", disabled: !@query.editable?(blazer_user) %>
<%= link_to "Fork", new_query_path(params: {variables: variable_params(@query), fork_query_id: @query.id, data_source: @query.data_source, name: @query.name}), class: "btn btn-info" %>

<% if !@error && @success %>
<% if Blazer.sharing.enabled? %>
<span class='btn btn-success' data-toggle="modal" data-target="#sharingModal">Share</span>
<% end %>

<%= button_to "Download", run_queries_path(format: "csv"), params: @run_data, class: "btn btn-primary" %>
<% end %>
</div>
Expand Down Expand Up @@ -72,3 +76,7 @@
}
</script>
<% end %>

<% if Blazer.sharing.enabled? %>
<%= render(partial: 'sharing_modal') %>
<% end %>
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
get :tables, on: :collection
get :schema, on: :collection
get :docs, on: :collection
get Blazer.sharing.route_path, to: 'queries#share', as: :share
end

resources :checks, except: [:show] do
Expand Down
8 changes: 8 additions & 0 deletions lib/blazer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
require_relative "blazer/result"
require_relative "blazer/result_cache"
require_relative "blazer/run_statement"
require_relative "blazer/sharing"
require_relative "blazer/statement"

# adapters
Expand Down Expand Up @@ -135,6 +136,13 @@ def self.data_sources
end
end

def self.sharing
@sharing ||= begin
sharing_settings = settings["sharing"] || {}
Blazer::Sharing.new(**sharing_settings.symbolize_keys)
end
end

def self.run_checks(schedule: nil)
checks = Blazer::Check.includes(:query)
checks = checks.where(schedule: schedule) if schedule
Expand Down
33 changes: 33 additions & 0 deletions lib/blazer/sharing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Blazer
class Sharing
attr_accessor :path, :enabled

def initialize(enabled: false, path: '/blazer_share')
@path = path.sub(/\/$/, '') # Strip trailing /
@enabled = enabled
end

def route_path
@route_path ||= "#{path}/:token/:query_id"
end

def to_controller
'blazer/queries#share'
end

def enabled?
enabled
end

def share_path(query_id, format: nil)
query = Query.find(query_id)
"#{path}/#{query.secret_token}/#{query_id}#{".#{format}" if format}"
end

def url_for(query_id, current_url, format: 'csv')
url = URI.parse(current_url)
url.path = share_path(query_id, format: format)
url.to_s
end
end
end
1 change: 1 addition & 0 deletions lib/generators/blazer/templates/install.rb.tt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
t.string :name
t.text :description
t.text :statement
t.text :secret_token
t.string :data_source
t.string :status
t.timestamps null: false
Expand Down
4 changes: 4 additions & 0 deletions test/internal/config/blazer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,7 @@ uploads:
url: postgres://localhost/blazer_test
schema: uploads
data_source: main

sharing:
path: /blazer_share
enabled: true
2 changes: 2 additions & 0 deletions test/internal/config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Rails.application.routes.draw do
mount Blazer::Engine, at: "/"

get Blazer.sharing.route_path, to: Blazer.sharing.to_controller, as: :share_query if Blazer.sharing.enabled?
end
1 change: 1 addition & 0 deletions test/internal/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
t.string :name
t.text :description
t.text :statement
t.text :secret_token
t.string :data_source
t.string :status
t.timestamps null: false
Expand Down
25 changes: 25 additions & 0 deletions test/queries_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ def test_variables_time_range
assert_match "daterangepicker", response.body
end

def test_correct_token
query = create_query(statement: "SELECT 1")
get share_query_path(query.id, token: query.secret_token, format: 'csv')

assert_response :success
assert_equal "text/csv; charset=utf-8", response.content_type
end

def test_incorrect_token
query = create_query(statement: "SELECT 1")
get share_query_path(query.id, token: "x")

assert_response :forbidden
assert_match "Access denied", response.body
end

def test_variable_defaults
query = create_query(statement: "SELECT {default_var}")
get blazer.query_path(query)
Expand Down Expand Up @@ -138,6 +154,15 @@ def test_csv_query_variables
assert_equal "text/csv; charset=utf-8", response.headers["Content-Type"]
end

def test_share
query = create_query
assert query.secret_token

get blazer.query_share_path(query_id: query.id, token: query.secret_token, format: 'csv')

assert_response :success
end

def test_url
run_query "SELECT 'http://localhost:3000/'"
assert_match %{<a target="_blank" href="http://localhost:3000/">http://localhost:3000/</a>}, response.body
Expand Down