glib-web 4.31.1 → 4.33.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d404f99ae7ff7d1e309c9bedca7b93145bc5331281c64308daa033b9ae6b4524
4
- data.tar.gz: 2f89659ddffb2206989074678c6388176aad57dd872f9b14c43149c51b9e91b5
3
+ metadata.gz: 3500da71e998b625c15ef1a41519581ed5d52d9dd9562a3e031e08d16aa2908c
4
+ data.tar.gz: 6f781cafa2ffe0cacfe1c7e31656415b2699bd19af4f9149614a5c229f60d94e
5
5
  SHA512:
6
- metadata.gz: de7bad6075a55452237fa31c44ad4a0405b6048fa84959385fe12003b1b37b53fc64576c59e658391b4c1efcb3608c62f772a1cffe050b6c2afacd87c86500ea
7
- data.tar.gz: 418db2e1fdf0af1838af518595b4eea273bfe5fd140676870f1a84c3fe8788e585f1701beb23861b84a7b2ade8206d0aed535256eecc5e6669ff69bfbc603749
6
+ metadata.gz: 31478adf1a4abdd83df77febe25c6454ebdf66908ecef04b34cb4a48746878a8e0e058184c3cb093c7c4526831b948c539e79d88330c52a6a722fb5be885b4cc
7
+ data.tar.gz: 97f3164d075437ccb1bac37a6d0bea555ad1ec1b9a7b9870fb1d37a3619cc7a5a361a6684e0a412488d406da3c86dfa90416f7713445ead43bd9c355e18e9deb
@@ -3,6 +3,7 @@ class Glib::JsonUi::ActionBuilder
3
3
  class AbstractHttp < Action
4
4
  string :url, cache: true
5
5
  bool :silent
6
+ int :retryLimit
6
7
 
7
8
  def formData(hash)
8
9
  form_data = {}
@@ -42,6 +42,8 @@ class Glib::JsonUi::ViewBuilder
42
42
  string :prefix
43
43
  string :suffix
44
44
  bool :formatYAxis
45
+ string :indexAxis
46
+ bool :horizontalStacked
45
47
  int :min
46
48
  int :max
47
49
  # https://www.chartjs.org/docs/latest/configuration/legend.html
@@ -152,6 +152,14 @@ class Glib::JsonUi::ViewBuilder
152
152
  end
153
153
 
154
154
  class Hidden < Text
155
+
156
+ def created
157
+ # no need to auto translate this
158
+ @label = ''
159
+ @placeholder = ''
160
+
161
+ super
162
+ end
155
163
  end
156
164
 
157
165
  class Timer < Text
@@ -520,6 +520,8 @@ class Glib::JsonUi::ViewBuilder
520
520
 
521
521
  views :childViews
522
522
 
523
+ string :align
524
+
523
525
  # required :innerPadding
524
526
  end
525
527
 
@@ -10,7 +10,9 @@ section.rows builder: ->(template) do
10
10
  end
11
11
  template.thumbnail title: 'Detect Country', onClick: ->(action) do
12
12
  action.browsers_detectCountry onDetect: ->(detect) do
13
- detect.http_post url: json_ui_garage_url(path: 'forms/generic_post_all')
13
+ detect.http_post url: json_ui_garage_url(path: 'forms/generic_post_all'), formData: {
14
+ message: 'hello'
15
+ }
14
16
  end
15
17
  end
16
18
  end
@@ -3,7 +3,29 @@ markdown = '## Emphasis' + "\n" +
3
3
  "\n" +
4
4
  '*This is italic text*' + "\n" +
5
5
  "\n" +
6
- '~~Strikethrough~~' + "\n"
6
+ '~~Strikethrough~~' + "\n" + "
7
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry.
8
+ Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
9
+ It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
10
+ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
11
+ and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
12
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry.
13
+ Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
14
+ It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
15
+ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
16
+ and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
17
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry.
18
+ Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
19
+ It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
20
+ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
21
+ and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
22
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry.
23
+ Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
24
+ It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.
25
+ It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
26
+ and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
27
+ "
28
+
7
29
 
8
30
  options = {}
9
31
  if (update_existing = local_assigns[:update_existing])
@@ -10,15 +10,33 @@ section.rows builder: ->(template) do
10
10
  end
11
11
  end
12
12
 
13
+ template.thumbnail title: 'http/post (error page)', onClick: ->(action) do
14
+ action.auth_saveCsrfToken token: form_authenticity_token, onSave: ->(subaction) do
15
+ subaction.http_post retryLimit: 5, url: json_ui_garage_url(path: SecureRandom.hex(3)), formData: { user: { name: { first: 'New', last: 'Joe' } } }
16
+ end
17
+ end
18
+
13
19
  template.thumbnail title: 'http/patch', onClick: ->(action) do
14
20
  action.auth_saveCsrfToken token: form_authenticity_token, onSave: ->(subaction) do
15
21
  subaction.http_patch url: json_ui_garage_url(path: 'forms/basic_post'), formData: { 'user[name]' => 'Edit Joe' }
16
22
  end
17
23
  end
18
24
 
25
+ template.thumbnail title: 'http/patch (error page)', onClick: ->(action) do
26
+ action.auth_saveCsrfToken token: form_authenticity_token, onSave: ->(subaction) do
27
+ subaction.http_patch url: json_ui_garage_url(path: SecureRandom.hex(3)), formData: { 'user[name]' => 'Edit Joe' }
28
+ end
29
+ end
30
+
19
31
  template.thumbnail title: 'http/delete', onClick: ->(action) do
20
32
  action.auth_saveCsrfToken token: form_authenticity_token, onSave: ->(subaction) do
21
33
  subaction.http_delete url: json_ui_garage_url(path: 'forms/basic_post'), formData: { 'user[name]' => 'Delete Joe' }
22
34
  end
23
35
  end
36
+
37
+ template.thumbnail title: 'http/delete (error page)', onClick: ->(action) do
38
+ action.auth_saveCsrfToken token: form_authenticity_token, onSave: ->(subaction) do
39
+ subaction.http_delete url: json_ui_garage_url(path: SecureRandom.hex(3)), formData: { 'user[name]' => 'Delete Joe' }
40
+ end
41
+ end
24
42
  end
@@ -60,21 +60,21 @@ page.scroll padding: glib_json_padding_body, childViews: ->(scroll) do
60
60
  panel.button text: '5'
61
61
  end
62
62
 
63
- # scroll.label text: "\n"
64
- # scroll.h1 text: 'Alignments'
65
- # scroll.panels_horizontal align: 'top', childViews: ->(panel) do
66
- # panel.button height: 50, text: 'Button'
67
- # panel.spacer width: 10
68
- # panel.label text: 'top'
69
- # end
70
- # scroll.panels_horizontal align: 'middle', childViews: ->(panel) do
71
- # panel.button height: 50, text: 'Button'
72
- # panel.spacer width: 10
73
- # panel.label text: 'middle'
74
- # end
75
- # scroll.panels_horizontal align: 'bottom', childViews: ->(panel) do
76
- # panel.button height: 50, text: 'Button'
77
- # panel.spacer width: 10
78
- # panel.label text: 'bottom'
79
- # end
63
+ scroll.label text: "\n"
64
+ scroll.h1 text: 'Alignments'
65
+ scroll.panels_horizontal align: 'top', childViews: ->(panel) do
66
+ panel.button height: 50, text: 'Button'
67
+ panel.spacer width: 10
68
+ panel.label text: 'top'
69
+ end
70
+ scroll.panels_horizontal align: 'middle', childViews: ->(panel) do
71
+ panel.button height: 50, text: 'Button'
72
+ panel.spacer width: 10
73
+ panel.label text: 'middle'
74
+ end
75
+ scroll.panels_horizontal align: 'bottom', childViews: ->(panel) do
76
+ panel.button height: 50, text: 'Button'
77
+ panel.spacer width: 10
78
+ panel.label text: 'bottom'
79
+ end
80
80
  end
@@ -138,6 +138,40 @@ else
138
138
  plugins: default_plugins,
139
139
  colors: ['#7A71F4', '#53B3E4', 'skyblue'], legend: { override: true }
140
140
 
141
+
142
+ scroll.h2 text: 'Column chart (Right-to-left)'
143
+ scroll.charts_column dataGroups: [
144
+ -> do
145
+ json.title 'Data 1'
146
+
147
+ points = {
148
+ 'Hotel One' => 10,
149
+ 'Hotel Two' => 16,
150
+ 'Hotel Three' => 18,
151
+ }
152
+ json.points points
153
+ end,
154
+ -> do
155
+ json.title 'Data 2'
156
+
157
+ points = {
158
+ 'Hotel One' => 24,
159
+ 'Hotel Two' => 22,
160
+ }
161
+ json.points points
162
+ end,
163
+ -> do
164
+ json.title 'Data 3'
165
+
166
+ points = {
167
+ 'Hotel One' => 20,
168
+ 'Hotel Two' => 23,
169
+ }
170
+ json.points points
171
+ end
172
+ ],
173
+ indexAxis: 'y', horizontalStacked: true,
174
+ colors: ['#7A71F4', '#53B3E4', 'red'], legend: { override: true }
141
175
  scroll.h2 text: 'Column chart (Stacked)'
142
176
  scroll.charts_column stacked: true, dataGroups: [
143
177
  -> do
@@ -21,14 +21,13 @@ page.scroll padding: glib_json_padding_body, childViews: ->(scroll) do
21
21
 
22
22
  f.spacer width: 10
23
23
 
24
- # TODO: Fix. Crawler test is failing.
25
- # f.shareButton \
26
- # network: 'email',
27
- # url: '',
28
- # text: 'Email',
29
- # onClick: ->(action) do
30
- # action.windows_openWeb url: 'mailto://test@email.com'
31
- # end
24
+ f.shareButton \
25
+ network: 'email',
26
+ url: '',
27
+ text: 'Email',
28
+ onClick: ->(action) do
29
+ action.windows_openWeb url: 'mailto:test@email.com'
30
+ end
32
31
 
33
32
  f.spacer width: 10
34
33
 
@@ -4,6 +4,7 @@ module Glib
4
4
  def initialize(method, http, args, controller)
5
5
  @http = http
6
6
  json = @http.send(method, args['url'], controller, args.fetch('formData', {}))
7
+ @http.router.http_actions.add([args['action'], args['url'], args.fetch('formData', {})])
7
8
  perform(json['onResponse'])
8
9
  end
9
10
  end
@@ -76,11 +76,13 @@ module Glib
76
76
  case method
77
77
  when :patch, :put
78
78
  json = @http.patch url, action, params
79
+ @http.router.http_actions.add([action, url, params])
79
80
  perform(json['onResponse'])
80
81
  when :post
81
82
  if (groups = form_post_param_groups)
82
83
  groups.each do |group_params|
83
84
  json = @http.post url, action, group_params
85
+ @http.router.http_actions.add([action, url, group_params])
84
86
  perform(json['onResponse'])
85
87
  end
86
88
  else
@@ -5,7 +5,8 @@ module Glib
5
5
  @http = http
6
6
 
7
7
  if (url = args['url'])
8
- json = @http.get(url, action, args.except('url'))
8
+ params = args.except('url').merge('format' => 'json')
9
+ json = @http.get(url, action, params)
9
10
 
10
11
  unless json.nil?
11
12
  @http.router.begin_page(json, url)
@@ -24,19 +24,19 @@ module Glib
24
24
  fetch(:get, url, action, params, inspect_result)
25
25
  end
26
26
 
27
- def post(url, action, params)
27
+ def post(url, action, params, inspect_result = true)
28
28
  fetch(:post, url, action, params)
29
29
  end
30
30
 
31
- def patch(url, action, params)
31
+ def patch(url, action, params, inspect_result = true)
32
32
  fetch(:patch, url, action, params)
33
33
  end
34
34
 
35
- def put(url, action, params)
35
+ def put(url, action, params, inspect_result = true)
36
36
  fetch(:put, url, action, params)
37
37
  end
38
38
 
39
- def delete(url, action, params = {})
39
+ def delete(url, action, params = {}, inspect_result = true)
40
40
  fetch(:delete, url, action, {})
41
41
  end
42
42
 
@@ -1,7 +1,9 @@
1
1
  module Glib
2
2
  module JsonCrawler
3
3
  class Router
4
- attr_reader :read_only_actions, :logger, :last_log, :deferred_actions
4
+ attr_reader :read_only_actions # deprecated
5
+ attr_reader :logger, :last_log, :deferred_actions
6
+ attr_reader :http_actions
5
7
  attr_accessor :host
6
8
 
7
9
  def log(action, url, response = nil)
@@ -46,6 +48,7 @@ module Glib
46
48
  @logger = ''
47
49
  @visitor = Glib::Json::Traversal::Visitor.new(crawler_test: true)
48
50
  @read_only_actions = Set.new
51
+ @http_actions = Set.new
49
52
  # default rails's development host
50
53
  @host ||= 'localhost:3000'
51
54
  @page_specs = []
@@ -65,12 +68,14 @@ module Glib
65
68
  end
66
69
 
67
70
  if args.is_a?(Hash) && args['rel'] != 'nofollow'
68
- if (on_click = args.fetch('onClick', nil))
71
+ on_click = args.fetch('onClick', nil)
72
+
73
+ if on_click && !args['disabled']
69
74
  process_action(http, on_click)
70
75
  end
71
76
  end
72
77
 
73
- @read_only_actions.replace(@read_only_actions.sort_by { |e| e[1].to_s })
78
+ # @read_only_actions.replace(@read_only_actions.sort_by { |e| e[1].to_s })
74
79
  end
75
80
 
76
81
  def process_action(http, spec)
@@ -83,14 +88,16 @@ module Glib
83
88
  @depth += 1
84
89
  case action
85
90
  when 'initiate_navigation'
86
- @read_only_actions.add([action, params['url']])
91
+ # @read_only_actions.add([action, params['url']])
92
+ http_actions.add([action, params['url']])
87
93
  JsonCrawler::NavInitiate.new(http, params, action)
88
94
  when 'runMultiple-v1', 'runMultiple'
89
95
  JsonCrawler::RunMultiple.new(http, params, action)
90
96
  when 'windows/open-v1', 'dialogs/open-v1', 'windows/reload-v1', 'windows/open',
91
97
  'dialogs/open', 'windows/reload', 'windows/openWeb', 'windows/openWeb-v1'
92
98
  if allowed?(params['url'])
93
- @read_only_actions.add([action, params['url']])
99
+ # @read_only_actions.add([action, params['url']])
100
+ http_actions.add([action, params['url']])
94
101
  JsonCrawler::WindowsOpen.new(http, params, action)
95
102
  else
96
103
  self.log action, params['url']
@@ -116,7 +123,8 @@ module Glib
116
123
  'http/delete',
117
124
  'dialogs/oauth'
118
125
  ].include?(action)
119
- @read_only_actions.add([action, params['url']])
126
+ # @read_only_actions.add([action, params['url']])
127
+ http_actions.add([action, params['url']])
120
128
  end
121
129
  self.log action, params['url']
122
130
  end
@@ -128,6 +136,7 @@ module Glib
128
136
  @visitor.forms.last
129
137
  end
130
138
 
139
+ # deprecated
131
140
  def follow(http, target_routers)
132
141
  if !target_routers.is_a?(Array)
133
142
  target_routers = [target_routers]
@@ -145,6 +154,29 @@ module Glib
145
154
  end
146
155
  end
147
156
 
157
+ def follow_v2(http, crawler_actions)
158
+ @depth += 1
159
+ crawler_actions.each do |crawler_action|
160
+ action, url, params = crawler_action
161
+
162
+ params = JSON.parse(params) if params.is_a?(String)
163
+ params ||= {}
164
+
165
+ case action.to_s.downcase
166
+ when 'http/post-v1', 'forms/post'
167
+ http.post(url, action, params, false)
168
+ when 'http/patch-v1', 'forms/patch'
169
+ http.patch(url, action, params, false)
170
+ when 'http/put-v1', 'forms/put'
171
+ http.put(url, action, params, false)
172
+ when 'http/delete-v1'
173
+ http.delete(url, action, params, false)
174
+ else
175
+ http.get(url, action, params, false)
176
+ end
177
+ end
178
+ end
179
+
148
180
  def crawl_multiple(views, block)
149
181
  @visitor.traverse_multiple views, block
150
182
  end
@@ -0,0 +1,219 @@
1
+ require 'active_snapshot'
2
+ require 'hashdiff'
3
+
4
+ module Glib
5
+ module Snapshot
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include ::ActiveSnapshot
10
+
11
+ attr_accessor :snapshot_changed
12
+ attr_accessor :attributes_before_save
13
+
14
+ before_save do
15
+ self.attributes_before_save = attributes_in_database
16
+ self.snapshot_changed = changed?
17
+ end
18
+ end
19
+
20
+ def where_snapshots(user: nil, field: nil, action: nil, association: nil, association_action: nil, like: nil)
21
+ query = snapshots
22
+
23
+ if user.present? && !user.is_a?(String)
24
+ query = query.where(user_id: user.id, user_type: user.class.to_s)
25
+ elsif user.present? && user.is_a?(String)
26
+ query = query.where("metadata ->> 'user' = ?", user)
27
+ end
28
+
29
+ if field.present?
30
+ query = query.where("metadata -> 'diff' ->> 'item' LIKE ?", "%#{field}%")
31
+ end
32
+
33
+ if action.present?
34
+ query = query.where("metadata ->> 'action' = ?", action)
35
+ end
36
+
37
+ if association.present?
38
+ query = query.where("metadata -> 'diff' -> 'associations' -> '#{association}' -> 0 IS NOT NULL")
39
+ end
40
+
41
+ if association_action.present?
42
+ raise ArgumentError.new('association_action expect association to be present') if association.blank?
43
+
44
+ known_actions = {
45
+ 'create' => '+',
46
+ 'destroy' => '-',
47
+ 'update' => '~'
48
+ }
49
+ query = query.where("metadata -> 'diff' -> 'associations' ->> '#{association}' LIKE ?", "%\"#{known_actions[association_action.to_s]}\"%")
50
+ end
51
+
52
+ if like.present?
53
+ query = query.where("metadata ->> 'diff' LIKE ?", "%#{like}%")
54
+ end
55
+
56
+ query.to_a
57
+ end
58
+
59
+ # information need to be store:
60
+ # - action: create, update, destroy
61
+ # - track changes
62
+ def glib_create_snapshot!(user, action)
63
+ known_actions = ['create', 'update', 'destroy']
64
+
65
+ raise 'unknown action' if !known_actions.include?(action.to_s)
66
+
67
+ result = nil
68
+ with_lock do
69
+ version = last_version + 1
70
+ metadata = {
71
+ action: action,
72
+ diff: diff,
73
+ version: version
74
+ }
75
+ snapshot_obj = {
76
+ identifier: "#{self.class.to_s.underscore}_#{id}_version_#{version}",
77
+ user: user,
78
+ metadata: metadata
79
+ }
80
+
81
+ if user.is_a?(String)
82
+ metadata[:user] = user
83
+ snapshot_obj.delete(:user)
84
+ end
85
+
86
+ # dont create version if same as before
87
+ if !same_as_before?
88
+ result = create_snapshot!(**snapshot_obj)
89
+ remove_old_snapshot if result.present?
90
+ end
91
+ end
92
+ result
93
+ end
94
+
95
+ def glib_revert_snapshot!(version)
96
+ snapshot = snapshots.find_by(identifier: "#{self.class.to_s.underscore}_#{id}_version_#{version}")
97
+ snapshot.restore!
98
+ end
99
+
100
+ def interval_from_prev_version
101
+ updated_at - snapshot_prev.created_at
102
+ end
103
+
104
+ def diff(snapshot = snapshot_prev)
105
+ default_ignored_keys = ['updated_at', 'created_at']
106
+ ignore_keys = default_ignored_keys
107
+
108
+ if !watched_keys_for_snapshot.nil?
109
+ ignored_keys_for_snapshot = attributes.except(*watched_keys_for_snapshot).keys
110
+ ignore_keys = (default_ignored_keys + ignored_keys_for_snapshot.map(&:to_s)).uniq
111
+ end
112
+
113
+ # always watch column with name *id
114
+ ignore_keys.filter! { |key| !key.ends_with?('id') || key != 'id' }
115
+
116
+ if snapshot.present?
117
+ item, associations = snapshot.fetch_reified_items
118
+ else
119
+ item = OpenStruct.new(attributes: attributes_before_save || {})
120
+ associations = {}
121
+ end
122
+
123
+ obj = {
124
+ 'item' => ::Hashdiff.diff(item.attributes.except(*ignore_keys), attributes.except(*ignore_keys))
125
+ }
126
+
127
+ obj['associations'] = associations_for_snapshot.reduce({}) do |prev, curr|
128
+ first_record_off_same_collection = send(curr).first
129
+
130
+ if !first_record_off_same_collection.respond_to?(:watched_keys_for_snapshot) && !first_record_off_same_collection.nil?
131
+ raise NotImplementedError, "please add method 'watched_keys_for_snapshot' to #{first_record_off_same_collection.class}"
132
+ end
133
+
134
+ association_ignored_keys =
135
+ if !first_record_off_same_collection.try(:watched_keys_for_snapshot).nil?
136
+ assoc_ignore_keys = first_record_off_same_collection.attributes.except(*first_record_off_same_collection.watched_keys_for_snapshot).keys.map(&:to_s)
137
+ (default_ignored_keys + assoc_ignore_keys).uniq
138
+ else
139
+ default_ignored_keys
140
+ end
141
+
142
+ # always watch column with name *id
143
+ association_ignored_keys.filter! { |key| !key.ends_with?('id') || key != 'id' }
144
+
145
+ # attrs_before = (associations[curr] || []).map { |record| record.attributes.except(*association_ignored_keys) }
146
+ # attrs_now = send(curr).order(id: :asc).map { |record| record.attributes.except(*association_ignored_keys) }
147
+
148
+ before = (associations[curr] || [])
149
+ now = send(curr).order(id: :asc)
150
+
151
+ if before.blank?
152
+ prev.merge(curr.to_s => ::Hashdiff.diff([], now.map { |record| record.attributes.except(*association_ignored_keys) }))
153
+ else
154
+ diff = before.map.with_index do |record_before, index|
155
+ record_now = now.find_by(id: record_before.id)
156
+ if record_now.blank?
157
+ ['-', "[#{index}]", record_before.attributes.except(*association_ignored_keys)]
158
+ elsif record_now.present?
159
+ next if Hashdiff.diff(record_before.attributes.except(*association_ignored_keys), record_now.attributes.except(*association_ignored_keys)).blank?
160
+ ['~', "[#{index}]", record_before.attributes.except(*association_ignored_keys), record_now.attributes.except(*association_ignored_keys)]
161
+ end
162
+ end.compact_blank
163
+
164
+ diff2 = now.map.with_index do |record_now, index|
165
+ record_before = before.detect { |record| record.id == record_now.id }
166
+ if record_before.blank?
167
+ ['+', "[#{index}]", record_now.attributes.except(*association_ignored_keys)]
168
+ end
169
+ end.compact_blank
170
+
171
+ prev.merge(curr.to_s => (diff + diff2))
172
+ end
173
+ end
174
+
175
+ obj
176
+ end
177
+
178
+ def snapshot_prev
179
+ snapshots.order(id: :asc).last
180
+ end
181
+
182
+ def same_as_before?
183
+ return !snapshot_changed if snapshot_prev.blank?
184
+
185
+ result = diff['item'].blank?
186
+
187
+ result &&= diff['associations'].reduce(true) { |prev, (_k, v)| v.blank? && prev } if diff['associations'].present?
188
+
189
+ result
190
+ end
191
+
192
+ def last_version
193
+ return 0 if snapshot_prev.blank?
194
+
195
+ snapshot_prev.metadata['version']
196
+ end
197
+
198
+ def remove_old_snapshot
199
+ return if max_snapshots.nil?
200
+
201
+ newest_ids = snapshots.order(id: :desc).limit(max_snapshots).ids
202
+ return unless newest_ids.size >= max_snapshots
203
+
204
+ snapshots.where.not(id: newest_ids).destroy_all
205
+ end
206
+
207
+ def watched_keys_for_snapshot
208
+ raise NotImplementedError, "please add method 'watched_keys_for_snapshot' to #{self.class}"
209
+ end
210
+
211
+ def associations_for_snapshot
212
+ children_to_snapshot.keys.filter { |key| respond_to?(key) }
213
+ end
214
+
215
+ def max_snapshots
216
+ 10
217
+ end
218
+ end
219
+ end
@@ -36,7 +36,7 @@ module Glib
36
36
  assert_equal JSON.parse(expected), JSON.parse(result), "Result mismatch! #{__git_is_available? ? `git diff #{__controller_log_dir}/#{__controller_log_file}` : ''}"
37
37
  end
38
38
 
39
- def crawl_json_pages(user, check_result: true, log_file: nil, &block)
39
+ def crawl_json_pages(user, check_result: true, log_file: nil, dump_actions: false, &block)
40
40
  __execute_crawler(user, check_result: true) do |router, http|
41
41
  path = user[:path] ? "#{user[:path]}?format=json" : '/users/me?format=json&redirect=default'
42
42
  router.host = HOST
@@ -47,6 +47,16 @@ module Glib
47
47
  'url' => user[:url] || "http://#{HOST}#{path}"
48
48
  }
49
49
  )
50
+
51
+ if dump_actions
52
+ csv_string = router.http_actions.map do |row|
53
+ action, url, params = row
54
+ [action, url, JSON.generate(params)].to_csv
55
+ end.join
56
+ filename = "#{user[:email]}[#{user[:device]}][#{user[:version] || 'current'}].csv"
57
+ filepath = File.join(__crawler_log_dir, filename)
58
+ File.write(filepath, csv_string)
59
+ end
50
60
  end
51
61
  end
52
62
 
data/lib/glib-web.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  require 'glib/version'
2
- require 'glib/engine' if defined?(::Rails)
2
+ if defined?(::Rails)
3
+ require 'glib/engine'
4
+ require 'glib/snapshot'
5
+ end
3
6
  require 'glib/value'
4
7
  require 'glib/json_crawler'
5
8
 
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glib-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.31.1
4
+ version: 4.33.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
  date: 2019-10-04 00:00:00.000000000 Z
@@ -80,6 +80,34 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '3.11'
83
+ - !ruby/object:Gem::Dependency
84
+ name: hashdiff
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: active_snapshot
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: rubocop
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -94,7 +122,7 @@ dependencies:
94
122
  - - ">="
95
123
  - !ruby/object:Gem::Version
96
124
  version: '0'
97
- description:
125
+ description:
98
126
  email: ''
99
127
  executables: []
100
128
  extensions: []
@@ -389,16 +417,17 @@ files:
389
417
  - lib/glib/json_crawler/http.rb
390
418
  - lib/glib/json_crawler/router.rb
391
419
  - lib/glib/mailer_tester.rb
420
+ - lib/glib/snapshot.rb
392
421
  - lib/glib/test_helpers.rb
393
422
  - lib/glib/time_freezable_mailer.rb
394
423
  - lib/glib/time_returning_mailer.rb
395
424
  - lib/glib/value.rb
396
425
  - lib/glib/version.rb
397
426
  - lib/tasks/db.rake
398
- homepage:
427
+ homepage:
399
428
  licenses: []
400
429
  metadata: {}
401
- post_install_message:
430
+ post_install_message:
402
431
  rdoc_options: []
403
432
  require_paths:
404
433
  - lib
@@ -414,7 +443,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
414
443
  version: '0'
415
444
  requirements: []
416
445
  rubygems_version: 3.4.6
417
- signing_key:
446
+ signing_key:
418
447
  specification_version: 4
419
448
  summary: ''
420
449
  test_files: []