time_streamer 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6335822a9bf6fcbe463bc8e08d72a3ce9e70bf65d566174b275442e7eeec1fb9
4
+ data.tar.gz: df9b6a36be5b17b5c635ba761fd8112faa62c5f61b8862968200c239cbd3af1d
5
+ SHA512:
6
+ metadata.gz: 589c4f9cea810ecb2cbd394053e263a14a1dbd72413541e1c4e7a8371f46603bb4920814b48372197cba6f8256394b7a341e899c65568318e0498a3460bcc4a3
7
+ data.tar.gz: 8d460f8bb6881c3fc35590c4e1d01fb0bafe286e4257e4f5ca07b1928e49e2e647c790c4ce73c130149dc772c48d562aeb1e9ee0f7f73677e694130097d759c0
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in simple_value.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Matous Vokal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,63 @@
1
+ # Time Streamer
2
+
3
+ Explore the versions of your ActiveRecord models in the browser.
4
+
5
+ The web interface allows you to view models at different points in time based on versions from an auditing system. It also allows for traversing associations. Works with [Audited](https://github.com/collectiveidea/audited) and [PaperTrail](https://github.com/paper-trail-gem/paper_trail) by default.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'time_streamer'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install time_streamer
22
+
23
+ ## Usage
24
+
25
+ Mount Time Streamer inside your application
26
+
27
+ ```ruby
28
+ # routes.rb
29
+ mount TimeStreamer::App, at: '/time_streamer'
30
+ ```
31
+
32
+ Configure the gem to suit your needs. The following example shows the default configuration.
33
+
34
+ ```ruby
35
+ # config/initializers/time_streamer.rb
36
+ TimeStreamer.configure do |config|
37
+ config.adapter = TimeStreamer::Adapters::Audited.new
38
+ config.global_ignored_associations = []
39
+ config.ignored_associations = {}
40
+ config.mount_path = '/time_streamer'
41
+ end
42
+ ```
43
+
44
+ The two available adapters are `TimeStreamer::Adapters::Audited` and `TimeStreamer::Adapters::PaperTrail`.
45
+
46
+ Ignore associations that should not be displayed. For example, the association containing the versions (`audits` for Audited and `versions` for PaperTrail) should probably be ignored.
47
+
48
+ To ignore associations for specific models, set the `ignored_associations` hash.
49
+
50
+ *IMPORTANT*: The associations have to be given as strings, not symbols!
51
+ ```ruby
52
+ TimeStreamer.configure do |config|
53
+ config.global_ignored_associations = ['audits']
54
+ config.ignored_associations = {
55
+ 'Customer' => ['product_ratings'],
56
+ 'Order' => ['products', 'discounts']
57
+ }
58
+ end
59
+ ```
60
+
61
+ ## License
62
+
63
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "time_streamer"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time_streamer/version'
4
+ require 'time_streamer/app'
5
+ require 'time_streamer/configuration'
6
+ require 'time_streamer/version_view_model'
7
+ require 'time_streamer/adapters/audited'
8
+ require 'time_streamer/adapters/paper_trail'
9
+
10
+ module TimeStreamer
11
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeStreamer
4
+ module Adapters
5
+ class Audited
6
+ def search_placeholder
7
+ 'AuditableType#AuditableId'
8
+ end
9
+
10
+ def identifier_for(record)
11
+ "#{record.class}##{record&.id}"
12
+ end
13
+
14
+ def find_version(id)
15
+ ::Audited::Audit.find id
16
+ end
17
+
18
+ def find_versions_by_search_term(search_term)
19
+ auditable_type, auditable_id = search_term.split '#', 2
20
+ ::Audited::Audit.where(auditable_type: auditable_type, auditable_id: auditable_id)
21
+ .order version: :desc
22
+ end
23
+
24
+ def record_at_version(version)
25
+ version.revision
26
+ end
27
+
28
+ def current_record_from_version(version)
29
+ version.auditable
30
+ end
31
+
32
+ def versions_of_record(record)
33
+ record.audits.reorder version: :desc
34
+ end
35
+
36
+ def version_data(version)
37
+ {
38
+ id: version.id.to_s,
39
+ title: "#{version.created_at.strftime '%-d. %-m. %Y %H:%M:%S'} - #{version.action}",
40
+ metadata: "Request UUID: #{version.request_uuid}",
41
+ changes: version.audited_changes
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeStreamer
4
+ module Adapters
5
+ class PaperTrail
6
+ def search_placeholder
7
+ 'ItemType#ItemId'
8
+ end
9
+
10
+ def identifier_for(record)
11
+ "#{record.class}##{record&.id}"
12
+ end
13
+
14
+ def find_version(id)
15
+ ::PaperTrail::Version.find id
16
+ end
17
+
18
+ def find_versions_by_search_term(search_term)
19
+ item_type, item_id = search_term.split '#', 2
20
+ ::PaperTrail::Version.includes(:item)
21
+ .where(item_type: item_type, item_id: item_id)
22
+ .order created_at: :desc
23
+ end
24
+
25
+ def record_at_version(version)
26
+ version.item.paper_trail.version_at version.created_at
27
+ end
28
+
29
+ def current_record_from_version(version)
30
+ version.item
31
+ end
32
+
33
+ def versions_of_record(record)
34
+ record.versions.reorder created_at: :desc
35
+ end
36
+
37
+ def version_data(version)
38
+ {
39
+ id: version.id.to_s,
40
+ title: "#{version.created_at.strftime '%-d. %-m. %Y %H:%M:%S'} - #{version.event}",
41
+ metadata: "Request UUID: #{version.whodunnit}",
42
+ changes: version.changeset
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+
5
+ module TimeStreamer
6
+ class App < Sinatra::Base
7
+ get '/' do
8
+ puts params
9
+ @search_placeholder = adapter.search_placeholder
10
+ @search_term = params[:search_term].to_s
11
+ versions = adapter.find_versions_by_search_term @search_term
12
+ @record = adapter.current_record_from_version(versions.first) if versions.any?
13
+ @associations = associations_for @record
14
+ @header = "#{adapter.identifier_for @record} - latest version"
15
+ @versions = versions_to_view_models versions
16
+ erb :index
17
+ end
18
+
19
+ get '/version/:id' do
20
+ @search_placeholder = adapter.search_placeholder
21
+ version = adapter.find_version params[:id]
22
+ @record = adapter.record_at_version version
23
+ @versions = versions_to_view_models adapter.versions_of_record(@record)
24
+ @search_term = adapter.identifier_for @record
25
+ @associations = associations_for @record
26
+ @header = "#{adapter.identifier_for @record} - at #{format_time version.created_at}"
27
+ erb :index
28
+ end
29
+
30
+ ###
31
+
32
+ def associations_for(parent)
33
+ return if parent.nil?
34
+
35
+ associations = load_associations parent
36
+ associations.transform_values! do |value|
37
+ Array(value).map { |record| adapter.identifier_for(record) }
38
+ end
39
+ associations.reject { |_, value| value.empty? }
40
+ end
41
+
42
+ def load_associations(record)
43
+ associations = record.class.reflections.keys - ignored_associations(record.class)
44
+ associations.map { |key| [key, record.send(key)] }.to_h
45
+ end
46
+
47
+ def format_time(time)
48
+ time.strftime '%Y-%m-%d %H:%M:%S'
49
+ end
50
+
51
+ def versions_to_view_models(versions)
52
+ versions.map { |version| VersionViewModel.new adapter.version_data(version) }
53
+ end
54
+
55
+ def adapter
56
+ TimeStreamer.configuration.adapter
57
+ end
58
+
59
+ def ignored_associations(model_class)
60
+ global = TimeStreamer.configuration.global_ignored_associations
61
+ specific = TimeStreamer.configuration.ignored_associations.fetch(model_class.to_s) { [] }
62
+ global | specific
63
+ end
64
+
65
+ def mount_path
66
+ TimeStreamer.configuration.mount_path
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeStreamer
4
+ CONFIGURABLE_VALUES = %i[
5
+ adapter
6
+ global_ignored_associations
7
+ ignored_associations
8
+ mount_path
9
+ ].freeze
10
+
11
+ Configuration = Struct.new(*CONFIGURABLE_VALUES, keyword_init: true)
12
+
13
+ def self.configure
14
+ yield configuration
15
+ end
16
+
17
+ def self.configuration
18
+ @configuration ||= Configuration.new(
19
+ adapter: Adapters::Audited.new,
20
+ global_ignored_associations: [],
21
+ ignored_associations: {},
22
+ mount_path: '/time_streamer'
23
+ )
24
+ end
25
+ end
@@ -0,0 +1,59 @@
1
+ html, table {
2
+ font-size: 14px;
3
+ }
4
+
5
+ .red-text {
6
+ color: #e53935;
7
+ }
8
+
9
+ .grey-text {
10
+ color: #757575;
11
+ }
12
+
13
+ .versions {
14
+ margin-top: 2rem;
15
+ padding: 1rem;
16
+ background-color: #f5f5f5;
17
+ }
18
+
19
+ .version-card {
20
+ margin-bottom: 2rem;
21
+ }
22
+
23
+ .version-title, .version-meta {
24
+ display: inline-block;
25
+ margin: 1rem 1rem 1rem 0;
26
+ font-size: 16px;
27
+ font-weight: 500;
28
+ }
29
+
30
+ .version-meta {
31
+ color: #757575;
32
+ }
33
+
34
+ .link-btn {
35
+ display: inline-block;
36
+ background-color: #1e88e5;
37
+ color: white;
38
+ margin: 0 0.02rem 0.2rem 0;
39
+ padding: 0.4rem 1rem;
40
+ border-radius: 4px;
41
+ text-decoration: none;
42
+ }
43
+
44
+ .link-btn:hover {
45
+ background-color: #01579b;
46
+ }
47
+
48
+ table {
49
+ margin-bottom: 1rem;
50
+ background-color: #fff;
51
+ }
52
+
53
+ table, th, td {
54
+ border: 1px solid #bdbdbd;
55
+ }
56
+
57
+ table > tbody > tr > td {
58
+ padding: 0.5rem;
59
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeStreamer
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeStreamer
4
+ class VersionViewModel
5
+ attr_reader :id, :title, :metadata, :changes
6
+
7
+ def initialize(id:, title:, metadata:, changes:)
8
+ @id = id
9
+ @title = title
10
+ @metadata = metadata
11
+ @changes = changes
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,29 @@
1
+ <table>
2
+ <tbody>
3
+ <% attributes.each do |key, value| %>
4
+ <tr>
5
+ <td>
6
+ <%= key %>
7
+ </td>
8
+ <% if value.is_a?(Array) && value.size == 2 %>
9
+ <td>
10
+ <%= value.first.inspect %>
11
+ </td>
12
+ <td>
13
+ <span class='grey-text'>
14
+ =>
15
+ </span>
16
+ </td>
17
+ <td>
18
+ <%= value.last.inspect %>
19
+ </td>
20
+ <% else %>
21
+ <td>
22
+ <%= value.inspect %>
23
+ </td>
24
+ <% end %>
25
+ </td>
26
+ </tr>
27
+ <% end %>
28
+ </tbody>
29
+ </table>
@@ -0,0 +1,44 @@
1
+ <html>
2
+ <head>
3
+ <link rel='stylesheet' href='<%= mount_path %>/time_streamer.css'>
4
+ </head>
5
+ <body>
6
+ <form action='<%= mount_path %>' method='get'>
7
+ <input type='text' name='search_term' placeholder='<%= @search_placeholder %>'>
8
+ <input type='submit' value='Find history'>
9
+ </form>
10
+
11
+ <% unless @record.nil? %>
12
+ <h1>
13
+ <%= @header %>
14
+ </h1>
15
+ <%= erb :attributes, locals: { attributes: @record.serializable_hash } %>
16
+ <div class='red-text'>
17
+ Only associations with ids stored in this record reflect the selected version
18
+ </div>
19
+ <% @associations.each do |name, associated_records| %>
20
+ <p>
21
+ <%= name %>:
22
+ </p>
23
+ <% associated_records.each do |associated_record| %>
24
+ <a href='<%= mount_path %>?search_term=<%= CGI.escape(associated_record) %>' class="link-btn">
25
+ <%= associated_record %>
26
+ </a>
27
+ <% end %>
28
+ <% end %>
29
+ <% end %>
30
+
31
+ <div class='versions'>
32
+ <% @versions.each do |version| %>
33
+ <div class='version-card'>
34
+ <span class="version-title"><%= version.title %></span>
35
+ <span class="version-meta">[ <%= version.metadata %> ]</span>
36
+ <%= erb :attributes, locals: { attributes: version.changes } %>
37
+ <a href='<%= mount_path %>/version/<%= CGI.escape(version.id) %>' class="link-btn">
38
+ Show record at version
39
+ </a>
40
+ </div>
41
+ <% end %>
42
+ </div>
43
+ </body>
44
+ </html>
@@ -0,0 +1,39 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "time_streamer/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "time_streamer"
7
+ spec.version = TimeStreamer::VERSION
8
+ spec.authors = ["Matous Vokal"]
9
+ spec.email = ["vokalmat@gmail.com"]
10
+
11
+ spec.summary = "Web interface for audits in Rails applications"
12
+ spec.description = "Explore the versions of your ActiveRecord models in the browser."
13
+ spec.homepage = "https://github.com/Sorc96/time_streamer"
14
+ spec.license = "MIT"
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
+
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = "https://github.com/Sorc96/time_streamer"
23
+ else
24
+ raise "RubyGems 2.0 or newer is required to protect against " \
25
+ "public gem pushes."
26
+ end
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
31
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_development_dependency "bundler", "~> 2.0"
36
+ spec.add_development_dependency "rake", "~> 10.0"
37
+
38
+ spec.add_dependency "sinatra"
39
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: time_streamer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Matous Vokal
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sinatra
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Explore the versions of your ActiveRecord models in the browser.
56
+ email:
57
+ - vokalmat@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - Gemfile
64
+ - LICENSE.txt
65
+ - README.md
66
+ - bin/console
67
+ - bin/setup
68
+ - lib/time_streamer.rb
69
+ - lib/time_streamer/adapters/audited.rb
70
+ - lib/time_streamer/adapters/paper_trail.rb
71
+ - lib/time_streamer/app.rb
72
+ - lib/time_streamer/configuration.rb
73
+ - lib/time_streamer/public/time_streamer.css
74
+ - lib/time_streamer/version.rb
75
+ - lib/time_streamer/version_view_model.rb
76
+ - lib/time_streamer/views/attributes.erb
77
+ - lib/time_streamer/views/index.erb
78
+ - time_streamer.gemspec
79
+ homepage: https://github.com/Sorc96/time_streamer
80
+ licenses:
81
+ - MIT
82
+ metadata:
83
+ allowed_push_host: https://rubygems.org
84
+ homepage_uri: https://github.com/Sorc96/time_streamer
85
+ source_code_uri: https://github.com/Sorc96/time_streamer
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubygems_version: 3.0.8
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Web interface for audits in Rails applications
105
+ test_files: []