dibbler 1.0.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.
@@ -0,0 +1,49 @@
1
+ # *****************************************************************************
2
+ # * Copyright (c) 2019 Matěj Outlý
3
+ # *****************************************************************************
4
+ # *
5
+ # * Request translation based on DB slugs
6
+ # *
7
+ # * Author: Matěj Outlý
8
+ # * Date : 21. 7. 2015
9
+ # *
10
+ # *****************************************************************************
11
+
12
+ module Dibbler
13
+ module Middlewares
14
+ class Slug
15
+
16
+ def initialize(app)
17
+ @app = app
18
+ end
19
+
20
+ def call(env)
21
+ if filter(env)
22
+ @app.call(env)
23
+ else
24
+
25
+ # Match locale from path
26
+ locale, translation = Dibbler.disassemble(env["PATH_INFO"])
27
+
28
+ # Translate to original and modify request
29
+ original = Dibbler.slug_model.translation_to_original(I18n.locale, translation) # Previously matched locale used
30
+ unless original.nil?
31
+ original = Dibbler.assemble(locale, original)
32
+ env["REQUEST_PATH"] = original
33
+ env["PATH_INFO"] = original
34
+ env["REQUEST_URI"] = original + "?" + env["QUERY_STRING"]
35
+ end
36
+ @app.call(env)
37
+ end
38
+ end
39
+
40
+ protected
41
+
42
+ def filter(env)
43
+ return true if env["PATH_INFO"].start_with?("/assets/")
44
+ false
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,148 @@
1
+ # *****************************************************************************
2
+ # * Copyright (c) 2019 Matěj Outlý
3
+ # *****************************************************************************
4
+ # *
5
+ # * Hierarchical slug generator
6
+ # *
7
+ # * Author: Matěj Outlý
8
+ # * Date : 21. 7. 2015
9
+ # *
10
+ # *****************************************************************************
11
+
12
+ module Dibbler
13
+ module Models
14
+ module HierarchicalSlugGenerator
15
+ extend ActiveSupport::Concern
16
+
17
+ included do
18
+
19
+ after_commit :generate_slugs
20
+ before_destroy :destroy_slugs_before, prepend: true
21
+ after_destroy :destroy_slugs_after
22
+
23
+ end
24
+
25
+ module ClassMethods
26
+
27
+ def generate_slugs(options = {})
28
+ self.roots.each do |item|
29
+ item.generate_slugs(options)
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ def disable_slug_generator
36
+ @disable_slug_generator = true
37
+ end
38
+
39
+ def enable_slug_generator
40
+ @disable_slug_generator = false
41
+ end
42
+
43
+ # *************************************************************
44
+ # Hooks
45
+ # *************************************************************
46
+
47
+ def generate_slugs(options = {})
48
+ return if @disable_slug_generator
49
+ ActiveRecord::Base.transaction do
50
+
51
+ # Generate slug in this model
52
+ unless Dibbler.slug_model.nil?
53
+ I18n.available_locales.each do |locale|
54
+ self._destroy_slug_was(Dibbler.slug_model, locale)
55
+ self._generate_slug(Dibbler.slug_model, locale)
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ # Propagate to other models
62
+ if options[:disable_propagation] != true
63
+
64
+ # Propagate to parent
65
+ self.parent.generate_slugs(disable_propagation: true) if self.parent
66
+
67
+ # Propagate to descendants
68
+ self.descendants.each do |descendant|
69
+ descendant.generate_slugs(disable_propagation: true)
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ def destroy_slugs_before(options = {})
77
+
78
+ # Destroy slug of this model
79
+ unless Dibbler.slug_model.nil?
80
+ I18n.available_locales.each do |locale|
81
+ self._destroy_slug(Dibbler.slug_model, locale)
82
+ end
83
+ end
84
+
85
+ # Propagate to other models
86
+ if options[:disable_propagation] != true
87
+
88
+ # Propagate to descendants
89
+ self.descendants.each do |descendant|
90
+ descendant.destroy_slugs_before(disable_propagation: true)
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+
97
+ def destroy_slugs_after(options = {})
98
+
99
+ # Propagate to other models
100
+ if options[:disable_propagation] != true
101
+
102
+ # Propagate to parent
103
+ self.parent.generate_slugs(disable_propagation: true) if self.parent
104
+
105
+ end
106
+
107
+ end
108
+
109
+ def url_original
110
+ if @url_original.nil?
111
+ @url_original = self._url_original
112
+ end
113
+ @url_original
114
+ end
115
+
116
+ def compose_slug_translation(locale)
117
+ self._compose_slug_translation(locale)
118
+ end
119
+
120
+ protected
121
+
122
+ # *************************************************************
123
+ # Callbacks to be defined in application
124
+ # *************************************************************
125
+
126
+ def _url_original
127
+ raise "To be defined in application."
128
+ end
129
+
130
+ def _compose_slug_translation(locale)
131
+ raise "To be defined in application."
132
+ end
133
+
134
+ def _generate_slug(slug_model, locale)
135
+ raise "To be defined in application."
136
+ end
137
+
138
+ def _destroy_slug(slug_model, locale)
139
+ raise "To be defined in application."
140
+ end
141
+
142
+ def _destroy_slug_was(slug_model, locale)
143
+ raise "To be defined in application."
144
+ end
145
+
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,414 @@
1
+ # *****************************************************************************
2
+ # * Copyright (c) 2019 Matěj Outlý
3
+ # *****************************************************************************
4
+ # *
5
+ # * Slug
6
+ # *
7
+ # * Author: Matěj Outlý
8
+ # * Date : 21. 7. 2015
9
+ # *
10
+ # *****************************************************************************
11
+
12
+ module Dibbler
13
+ module Models
14
+ module Slug
15
+ extend ActiveSupport::Concern
16
+
17
+ module ClassMethods
18
+
19
+ # Clear slug cache. Must be done if data changed in DB
20
+ def clear_cache
21
+ @o2t = nil
22
+ @t2o = nil
23
+ end
24
+
25
+ # Load specific locale to cache
26
+ def load_cache(locale)
27
+
28
+ # Initialize cache structures
29
+ @o2t = {} if @o2t.nil?
30
+ @t2o = {} if @t2o.nil?
31
+
32
+ # Fill cache if empty
33
+ if @o2t[locale.to_sym].nil? || @t2o[locale.to_sym].nil?
34
+
35
+ # Preset
36
+ @o2t[locale.to_sym] = {}
37
+ @t2o[locale.to_sym] = {}
38
+
39
+ # Static data from config
40
+ if Dibbler.static_slugs
41
+ Dibbler.static_slugs.each do |item|
42
+ if item[:locale].to_s == locale.to_s
43
+ translation_as_key = item[:translation]
44
+ translation_as_key = translation_as_key.downcase if Dibbler.downcase_translations == true
45
+ if Dibbler.use_filter
46
+ if Dibbler.current_app_filter.to_s == item[:filter] # Slug belongs to current application
47
+ @o2t[locale.to_sym][item[:original]] = item[:translation]
48
+ @t2o[locale.to_sym][translation_as_key] = item[:original]
49
+ elsif !item[:filter].blank? # Slug belongs to other application
50
+ url = Dibbler.available_filter_urls[item[:filter].to_sym]
51
+ @o2t[locale.to_sym][item[:original]] = url.trim("/") + item[:translation] unless url.blank?
52
+ end
53
+ else
54
+ @o2t[locale.to_sym][item[:original]] = item[:translation]
55
+ @t2o[locale.to_sym][translation_as_key] = item[:original]
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ # Dynamic data from DB
62
+ data = where(locale: locale.to_s)
63
+ data.each do |item|
64
+ translation_as_key = item.translation
65
+ translation_as_key = translation_as_key.downcase if Dibbler.downcase_translations == true
66
+ if Dibbler.use_filter
67
+ if Dibbler.current_app_filter.to_s == item.filter # Slug belongs to current application
68
+ @o2t[locale.to_sym][item.original] = item.translation
69
+ @t2o[locale.to_sym][translation_as_key] = item.original
70
+ elsif !item.filter.blank? # Slug belongs to other application
71
+ url = Dibbler.available_filter_urls[item.filter.to_sym]
72
+ @o2t[locale.to_sym][item.original] = url.trim("/") + item.translation unless url.blank?
73
+ end
74
+ else
75
+ @o2t[locale.to_sym][item.original] = item.translation
76
+ @t2o[locale.to_sym][translation_as_key] = item.original
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+
84
+ # Get translation according to original
85
+ def original_to_translation(locale, original)
86
+ return nil if original.nil?
87
+ load_cache(locale)
88
+
89
+ # First priority translation (without IDs)
90
+ result = @o2t[locale.to_sym][original.to_s]
91
+ if result.nil?
92
+
93
+ # Ensure single "/" on right
94
+ original = original.rtrim("/") + "/"
95
+
96
+ # Create array of all matched IDs alongside with string ":id" (i.e. [["1", ":id"], ["2", ":id"]])
97
+ # Substitute all numeric IDs in original to string ":id"
98
+ matched_ids = []
99
+ product_1 = []
100
+ original = original.gsub(/\/[0-9]+\//) do |matched|
101
+ matched_id = matched[1..-2]
102
+ matched_ids << matched_id
103
+ product_1 << [matched_id, ":id"]
104
+ "/:id/"
105
+ end
106
+
107
+ unless product_1.empty?
108
+
109
+ # Create product of matched IDs (i.e. [["1", "2"], ["1", ":id"], [":id", "2"], [":id", ":id"]])
110
+ product_2 = product_1.first.product(*product_1[1..-1])
111
+
112
+ # Try to find some result for all combinations (except first one, which is already tried and failed)
113
+ result_ids = nil
114
+ product_2[1..-1].each do |combined_ids|
115
+
116
+ # IDs missing in this combination
117
+ result_ids = []
118
+
119
+ # Generate original according to current combination (i.e. "/nodes/1/photos/:id" or "/nodes/:id/photos/:id")
120
+ index = 0
121
+ product_original = original.gsub(/\/:id\//) do
122
+ result_ids << matched_ids[index] if combined_ids.first == ":id"
123
+ index += 1
124
+ "/#{combined_ids.shift.to_s}/"
125
+ end
126
+
127
+ # Remove "/" on right
128
+ product_original = product_original.rtrim("/")
129
+
130
+ # Try to translate current combination and break if match found
131
+ result = @o2t[locale.to_sym][product_original.to_s]
132
+ break unless result.nil?
133
+
134
+ end
135
+
136
+ # Correct result if any
137
+ unless result.nil?
138
+
139
+ # Ensure single "/" on right
140
+ result = result.rtrim("/") + "/"
141
+
142
+ # Substitute :id to numeric IDS matched from translation
143
+ result = result.gsub(/\/:id\//) do
144
+ "/#{result_ids.shift.to_s}/"
145
+ end
146
+
147
+ # Remove "/" on right
148
+ result = result.rtrim("/")
149
+
150
+ end
151
+
152
+ end
153
+
154
+ end
155
+
156
+ result
157
+ end
158
+
159
+ # Get original according to translation
160
+ def translation_to_original(locale, translation)
161
+ return nil if translation.nil?
162
+ load_cache(locale)
163
+
164
+ # Downcase if necessary
165
+ translation = translation.downcase if Dibbler.downcase_translations
166
+
167
+ # First priority translation (without IDs)
168
+ result = @t2o[locale.to_sym][translation.to_s]
169
+ if result.nil?
170
+
171
+ # Ensure single "/" on right
172
+ translation = translation.rtrim("/") + "/"
173
+
174
+ # Substitute numeric parts to :id
175
+ matched_ids = []
176
+ translation = translation.gsub(/\/[0-9]+\//) do |matched|
177
+ matched_ids << matched[1..-2]
178
+ "/:id/"
179
+ end
180
+
181
+ # Remove "/" on right
182
+ translation = translation.rtrim("/")
183
+
184
+ # Second priority translation (width IDs)
185
+ result = @t2o[locale.to_sym][translation.to_s]
186
+ unless result.nil?
187
+
188
+ # Ensure single "/" on right
189
+ result = result.rtrim("/") + "/"
190
+
191
+ # Substitute :id to numeric IDS matched from translation
192
+ result = result.gsub(/\/:id\//) do
193
+ "/#{matched_ids.shift.to_s}/"
194
+ end
195
+
196
+ # Remove "/" on right
197
+ result = result.rtrim("/")
198
+
199
+ end
200
+ end
201
+
202
+ result
203
+ end
204
+
205
+ # Add new slug or edit existing
206
+ def add_slug(locale, original, translation, filter = nil, uniquer = "")
207
+
208
+ # Do not process blank
209
+ return if original.blank? # || translation.blank? blank translation means that original translates to root
210
+
211
+ # Prepare
212
+ locale = locale.to_s
213
+ original = "/" + original.to_s.trim("/")
214
+ translation = "/" + translation.to_s.trim("/")
215
+ not_uniq_translation = "/" + translation.gsub(":uniquer", "").to_s.trim("/")
216
+ if uniquer
217
+ uniq_translation = "/" + translation.gsub(":uniquer", uniquer).to_s.trim("/")
218
+ else
219
+ uniq_translation = not_uniq_translation
220
+ end
221
+
222
+ # Root is not slugged
223
+ return if original == "/"
224
+
225
+ # Find occupations, if found some, translation must be uniqued with token
226
+ occupations = all
227
+ occupations = occupations.where.not(original: original)
228
+ occupations = occupations.where(locale: locale, translation: not_uniq_translation)
229
+ occupations = occupations.where(filter: filter) if Dibbler.use_filter
230
+ if occupations.count > 0
231
+ translation = uniq_translation
232
+ else
233
+ translation = not_uniq_translation
234
+ end
235
+
236
+ # Try to find existing record
237
+ slug = where(locale: locale, original: original).first
238
+ if slug.nil?
239
+ slug = new
240
+ end
241
+
242
+ # TODO duplicate translations
243
+
244
+ # Save
245
+ slug.locale = locale
246
+ slug.filter = filter if Dibbler.use_filter
247
+ slug.original = original
248
+ slug.translation = translation
249
+ slug.save
250
+
251
+ # Clear cache
252
+ clear_cache
253
+
254
+ end
255
+
256
+ # Remove existing slug if exists
257
+ def remove_slug(locale, original)
258
+
259
+ # Prepare
260
+ locale = locale.to_s
261
+ original = "/" + original.to_s.trim("/")
262
+
263
+ # Try to find existing record
264
+ slug = where(locale: locale, original: original).first
265
+ unless slug.nil?
266
+ slug.destroy
267
+ end
268
+
269
+ # Clear cache
270
+ clear_cache
271
+
272
+ end
273
+
274
+ # Compose translation from various models
275
+ # Obsolete, please define own translation composition method
276
+ def compose_translation(locale, models)
277
+
278
+ # Convert to array
279
+ unless models.is_a? Array
280
+ models = [models]
281
+ end
282
+
283
+ # Result
284
+ result = ""
285
+ last_model = nil
286
+ last_model_is_category = false
287
+
288
+ models.each do |section_options|
289
+
290
+ # Input check
291
+ unless section_options.is_a? Hash
292
+ raise "Incorrect input, expecting hash with :label and :models or :model items."
293
+ end
294
+ if section_options[:models].nil? && !section_options[:model].nil?
295
+ section_options[:models] = [section_options[:model]]
296
+ end
297
+ if section_options[:models].nil? || section_options[:label].nil?
298
+ raise "Incorrect input, expecting hash with :label and :models or :model items."
299
+ end
300
+
301
+ # "Is category" option
302
+ last_model_is_category = section_options[:is_category] == true
303
+
304
+ section_options[:models].each do |model|
305
+
306
+ # Get part
307
+ if model.respond_to?("#{section_options[:label].to_s}_#{locale.to_s}".to_sym)
308
+ part = model.send("#{section_options[:label].to_s}_#{locale.to_s}".to_sym)
309
+ elsif model.respond_to?(section_options[:label].to_sym)
310
+ part = model.send(section_options[:label].to_sym)
311
+ else
312
+ part = nil
313
+ end
314
+
315
+ # Add part to result
316
+ result += "/" + part.to_url if part
317
+
318
+ # Save last model
319
+ last_model = model
320
+
321
+ end
322
+
323
+ end
324
+
325
+ # Truncate correctly
326
+ unless result.blank?
327
+ if last_model_is_category || (last_model.hierarchically_ordered? && !last_model.leaf?)
328
+ result += "/"
329
+ else
330
+ #result += ".html"
331
+ result += ""
332
+ end
333
+ end
334
+
335
+ result
336
+ end
337
+
338
+ # *********************************************************
339
+ # Columns
340
+ # *********************************************************
341
+
342
+ def permitted_columns
343
+ [
344
+ :locale,
345
+ :original,
346
+ :translation,
347
+ :filter,
348
+ ]
349
+ end
350
+
351
+ def filter_columns
352
+ [
353
+ :locale,
354
+ :original,
355
+ :translation,
356
+ :filter,
357
+ ]
358
+ end
359
+
360
+ # *********************************************************
361
+ # Scopes
362
+ # *********************************************************
363
+
364
+ def filter(params = {})
365
+
366
+ # Preset
367
+ result = all
368
+
369
+ # Locale
370
+ unless params[:locale].blank?
371
+ if Dibbler.disable_unaccent
372
+ result = result.where("lower(locale) LIKE ('%' || lower(trim(?)) || '%')", params[:locale].to_s)
373
+ else
374
+ result = result.where("lower(unaccent(locale)) LIKE ('%' || lower(unaccent(trim(?))) || '%')", params[:locale].to_s)
375
+ end
376
+ end
377
+
378
+ # Original
379
+ unless params[:original].blank?
380
+ if Dibbler.disable_unaccent
381
+ result = result.where("lower(original) LIKE ('%' || lower(trim(?)) || '%')", params[:original].to_s)
382
+ else
383
+ result = result.where("lower(unaccent(original)) LIKE ('%' || lower(unaccent(trim(?))) || '%')", params[:original].to_s)
384
+ end
385
+ end
386
+
387
+ # Translation
388
+ unless params[:translation].blank?
389
+ if Dibbler.disable_unaccent
390
+ result = result.where("lower(translation) LIKE ('%' || lower(trim(?)) || '%')", params[:translation].to_s)
391
+ else
392
+ result = result.where("lower(unaccent(translation)) LIKE ('%' || lower(unaccent(trim(?))) || '%')", params[:translation].to_s)
393
+ end
394
+ end
395
+
396
+ # Filter
397
+ if Dibbler.use_filter == true
398
+ unless params[:filter].blank?
399
+ if Dibbler.disable_unaccent
400
+ result = result.where("lower(filter) LIKE ('%' || lower(trim(?)) || '%')", params[:filter].to_s)
401
+ else
402
+ result = result.where("lower(unaccent(filter)) LIKE ('%' || lower(unaccent(trim(?))) || '%')", params[:filter].to_s)
403
+ end
404
+ end
405
+ end
406
+
407
+ result
408
+ end
409
+
410
+ end
411
+
412
+ end
413
+ end
414
+ end