geo_labels 0.3.3 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +1 -0
  4. data/app/assets/javascripts/geo_labels/application.coffee +1 -1
  5. data/app/controllers/geo_labels/application_controller.rb +5 -0
  6. data/app/controllers/geo_labels/contacts_controller.rb +1 -1
  7. data/app/controllers/geo_labels/dashboard_controller.rb +30 -8
  8. data/app/helpers/geo_labels/application_helper.rb +1 -0
  9. data/app/models/geo_labels/contact.rb +15 -1
  10. data/app/models/geo_labels/contact_label.rb +1 -0
  11. data/app/models/geo_labels/label.rb +14 -0
  12. data/app/views/geo_labels/contacts/_form.html.slim +9 -2
  13. data/app/views/geo_labels/contacts/index.html.slim +6 -0
  14. data/app/views/geo_labels/contacts/show.html.slim +5 -1
  15. data/app/views/geo_labels/dashboard/about.html.slim +3 -0
  16. data/app/views/geo_labels/dashboard/export.html.slim +7 -3
  17. data/app/views/geo_labels/dashboard/main.html.slim +8 -1
  18. data/app/views/geo_labels/menu/_navigation.html.slim +23 -0
  19. data/app/views/geo_labels/menu/_right_menu_general.html.slim +11 -0
  20. data/app/views/geo_labels/users/_abilities.html.slim +20 -0
  21. data/app/views/layouts/geo_labels/application.html.slim +1 -1
  22. data/config/locales/geo_labels.en.yml +17 -0
  23. data/config/locales/geo_labels.es.yml +20 -0
  24. data/config/routes.rb +2 -0
  25. data/db/migrate/20231206183544_add_url_to_geo_labels_contacts.rb +5 -0
  26. data/lib/geo_labels/exporter.rb +5 -2
  27. data/lib/geo_labels/importer/csv_data.rb +43 -0
  28. data/lib/geo_labels/importer/yaml_data.rb +116 -0
  29. data/lib/geo_labels/importer.rb +33 -112
  30. data/lib/geo_labels/version.rb +1 -1
  31. data/lib/geo_labels.rb +2 -0
  32. metadata +12 -6
  33. data/app/views/geo_labels/application/_navigation.html.slim +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5dc60b00eeb1b554f6ee06e242c3c3a31586cdd609552a8ca91172fcb18de91f
4
- data.tar.gz: 39bfc36d4ff1551fb0ddd93596050f2381719e579f64df0398b1025f0866510f
3
+ metadata.gz: 30768188b560ce1db38ddd61a8f06c37eb44334bba0ea8309efbf1f0b6b809fe
4
+ data.tar.gz: b295252da7f1b305a7a7029884b3e50c6c239ba54b3914727b51469d38021f4f
5
5
  SHA512:
6
- metadata.gz: b39847ef962187e6f3d81cfe80bb3b71cab106aad5328ade39740aaf7f58a72bb4e296dc596ecad94633722358941971c6da62e1037f1accd7e7ac7c75f40580
7
- data.tar.gz: 4ea54296bf0b43579cd46deb8a58528b277eca56d12846ce3e3617a37f044ede01bc9e9901b6b402fd8a75e8c59fe76bea629fde46ab714f72ae75cb1afbc8f2
6
+ metadata.gz: d145b7d63a43d47d771b1a76c06b17afa032d035d385bc64f89fb0d565b1809b3021f6d970fb018e1ba9983c8a270443d41b5600261046cb612a6cb2e14ef209
7
+ data.tar.gz: fe7b5b251a2a7e51f76f4a39a0aafa8ac308f830c0ad63544cb7fad9d469a96162ea212c07cc41bdc3f48c3dcde8c49d744656a7c20420c33188c6267a33d34f
data/CHANGELOG.md CHANGED
@@ -1,6 +1,31 @@
1
1
  CHANGELOG of geo\_labels
2
2
  ============================
3
3
 
4
+ 2023-12-23 v0.4.3
5
+ -------------------------
6
+ * Optimize nested label representation to sort of N+1 query strategy
7
+ * Show labels on main query result page of contacts
8
+
9
+ 2023-10-25 v0.4.2
10
+ -------------------------
11
+ * Truncate label names to prevent ugly outputting
12
+
13
+ 2023-10-25 v0.4.1
14
+ -------------------------
15
+ * Add CSV import options
16
+ * Add authorization options
17
+ * Allow unsecure access by explicit authorization in `app/models/ability.rb`
18
+ <code>
19
+ can :manage, GeoLabels::Engine
20
+ can :manage, GeoLabels::Contact
21
+ can :manage, GeoLabels::Label
22
+ </code>
23
+
24
+ 2023-10-24 v0.4.0
25
+ -------------------------
26
+ * Set default map zoom level to 13
27
+ * Add CSV export option
28
+
4
29
  2023-09-07 v0.3.3
5
30
  -------------------------
6
31
  * OpenStreetMap gives coordinates in the url after double click. Support copy paste of that format
data/README.md CHANGED
@@ -35,6 +35,7 @@ In your `config/routes.rb` file add:
35
35
  ```
36
36
 
37
37
  ### Add the authorizations
38
+ Authorization is handled by [cancan](https://github.com/CanCanCommunity/cancancan).
38
39
  In your `app/models/ability.rb` file add the authorizations.
39
40
  This is a custom operation that you have to adjust to your needs.
40
41
  To allow all users full controll to the contracts add:
@@ -7,7 +7,7 @@ window.initMap = ->
7
7
  if map_canvas = document.getElementById('map-canvas')
8
8
  window.google_map = new google.maps.Map map_canvas,
9
9
  center: map_points[0]
10
- zoom: 9
10
+ zoom: 13
11
11
 
12
12
  map_points.forEach (map_point) ->
13
13
  marker = new google.maps.Marker
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module GeoLabels
3
3
  class ApplicationController < ::ApplicationController
4
+ before_action :authorize_engine!
4
5
 
5
6
  helper_method :query
6
7
 
@@ -13,5 +14,9 @@ module GeoLabels
13
14
  def query
14
15
  params[:q]
15
16
  end
17
+
18
+ def authorize_engine!
19
+ authorize! :read, GeoLabels::Engine
20
+ end
16
21
  end
17
22
  end
@@ -9,7 +9,7 @@ module GeoLabels
9
9
  authorize! :index, record_class
10
10
  @q = record_class.ransack query
11
11
  @q.sorts = 'name asc' if @q.sorts.empty?
12
- @records = @q.result.page(params[:page]).per(100)
12
+ @records = @q.result.includes(:labels).page(params[:page]).per(100)
13
13
  respond_with(@records)
14
14
  end
15
15
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GeoLabels
3
4
  class DashboardController < ApplicationController
4
5
 
@@ -9,27 +10,48 @@ module GeoLabels
9
10
  # fomantic-ui uses comma separated id values
10
11
  label_ids = label_ids.to_s.split(',') unless label_ids.is_a?(Array)
11
12
  states_array = (params[:states].presence || 'approved').split(',')
12
- @contacts = Contact.geocoded.for_label_ids(label_ids, predication: params[:predication], states: states_array)
13
+ @contacts = Contact.geocoded.includes(:labels).for_label_ids(label_ids, predication: params[:predication], states: states_array)
13
14
  end
14
15
 
16
+ def about; end
17
+
18
+ # SHOW action for the view to call the export_file action
15
19
  def export
16
- # SHOW action for the view to call the export_file action
20
+ authorize! :read, GeoLabels::Contact
21
+ authorize! :read, GeoLabels::Label
17
22
  end
18
23
 
19
24
  def export_file
25
+ authorize! :read, GeoLabels::Contact
26
+ authorize! :read, GeoLabels::Label
27
+
20
28
  timestamp = Time.now.utc.iso8601.first(19).gsub(/-|:/, '')
21
- send_data GeoLabels::Exporter.export_str,
22
- filename: "GeoLabels-export-#{timestamp}.yml",
23
- type: :text,
24
- disposition: :attachment
29
+ respond_to do |format|
30
+ format.yaml do
31
+ send_data GeoLabels::Exporter.export_str,
32
+ filename: "GeoLabels-export-#{timestamp}.yml",
33
+ type: :text,
34
+ disposition: :attachment
35
+ end
36
+ format.csv do
37
+ send_data GeoLabels::Contact.to_csv,
38
+ filename: "GeoLabels-export-#{timestamp}.csv",
39
+ type: :text,
40
+ disposition: :attachment
41
+ end
42
+ end
25
43
  end
26
44
 
27
45
  def import
28
- # SHOW action for the view to call the import_file action
46
+ authorize! :update, GeoLabels::Contact
47
+ authorize! :update, GeoLabels::Label
29
48
  end
30
49
 
31
50
  def import_file
32
- Importer.import params[:file]
51
+ authorize! :update, GeoLabels::Contact
52
+ authorize! :update, GeoLabels::Label
53
+
54
+ GeoLabels::Importer.import params[:file]
33
55
  redirect_to root_path, notice: 'File imported'
34
56
  rescue => e
35
57
  redirect_to import_path, alert: e.message
@@ -13,6 +13,7 @@ module GeoLabels
13
13
  label_name = link_to(item[:name], label_path(item[:id]))
14
14
  edit_link = table_edit_link(item, geo_labels.edit_label_path(item[:id]))
15
15
  add_child_link = link_to(tag.i(class: 'plus icon'), geo_labels.new_label_path(parent_id: item[:id]))
16
+ add_child_link = edit_link = '' unless can? :edit, GeoLabels::Label
16
17
  output += "#{lead_space}#{tag.li(label_name + add_child_link + edit_link)}\n".html_safe
17
18
  next unless item[:children].present?
18
19
 
@@ -7,7 +7,7 @@ module GeoLabels
7
7
  include RatingsSupport
8
8
 
9
9
  RANSACKABLE_ATTRIBUTES = %w[
10
- city country created_at department description
10
+ city country created_at department url description
11
11
  id latitude longitude name state street
12
12
  subsection updated_at
13
13
  ].freeze
@@ -42,6 +42,20 @@ module GeoLabels
42
42
  end
43
43
  end
44
44
 
45
+ def self.to_csv
46
+ require 'csv'
47
+ contacts = includes(:labels).all
48
+ additional_attributes = %w[description state street subsection city department country url latitude longitude]
49
+ CSV.generate do |csv|
50
+ csv << %w[name labels] + additional_attributes
51
+ contacts.each do |contact|
52
+ csv << [contact.name, contact.labels.map(&:name).compact.sort.join('|')] +
53
+ contact.attributes.fetch_values(*additional_attributes)
54
+ end
55
+ end
56
+ end
57
+
58
+
45
59
  def label_ids=(array_or_string)
46
60
  super array_or_string.is_a?(Array) ? array_or_string : array_or_string.split(',')
47
61
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GeoLabels
3
4
  class ContactLabel < ApplicationRecord
4
5
 
@@ -9,6 +9,7 @@ module GeoLabels
9
9
 
10
10
  acts_as_nested_set
11
11
 
12
+ normalizes :name, with: ->(name) { name.strip }
12
13
  validates :name, presence: true
13
14
 
14
15
  def self.descendants_for_ids(ids)
@@ -20,9 +21,22 @@ module GeoLabels
20
21
  descendants_scope
21
22
  end
22
23
 
24
+ def self.each_h_with_level(&blk)
25
+ returner = proc do |label, level|
26
+ blk.call(label, level)
27
+ label[:children].try :each do |child|
28
+ returner.call(child, level + 1)
29
+ end
30
+ end
31
+ Exporter.labels_tree_h.each do |label| # {name: 'Food', id: 3, children: [{name: 'Italian'...}]}
32
+ returner.call(label, 0)
33
+ end
34
+ end
35
+
23
36
  def associated_contacts
24
37
  ids = ContactLabel.joins(:label).merge(self_and_descendants).pluck(:contact_id).uniq
25
38
  Contact.order(:name).find(ids)
26
39
  end
40
+
27
41
  end
28
42
  end
@@ -36,9 +36,12 @@
36
36
  .menu
37
37
  - GeoLabels::Contact::STATE_OPTIONS.each do |option|
38
38
  .item data-value=option = option
39
+ .field
40
+ label= at :url
41
+ = f.text_field :url
39
42
  .field
40
43
  label= at :description
41
- = f.text_field :description
44
+ = f.text_area :description, rows: 3
42
45
  .field
43
46
  .ui.compact.accordion
44
47
  .title
@@ -53,7 +56,11 @@
53
56
  i.dropdown.icon
54
57
  .default.text= GeoLabels::Label.model_name.human_plural
55
58
  .menu
56
- - nested_set_options(GeoLabels::Label) {|i| "#{'-' * i.level} #{i.name}" }.each do |(label_name, label_id)|
59
+ - GeoLabels::Label.each_h_with_level do |label_h, level|
60
+ .item data-value=label_h[:id]
61
+ => '-' * level
62
+ = label_h[:name]
63
+ /- nested_set_options(GeoLabels::Label) {|i| "#{'-' * i.level} #{i.name}" }.each do |(label_name, label_id)|
57
64
  .item data-value=label_id
58
65
  = label_name
59
66
  .ui.bottom.attached.segment
@@ -11,6 +11,7 @@
11
11
  th= sort_link @q, :city, at(:city), default_order: :asc
12
12
  th= sort_link @q, :department, at(:department), default_order: :asc
13
13
  th= sort_link @q, :country, at(:country), default_order: :asc
14
+ th= GeoLabels::Label.model_name.human_plural
14
15
  th.collapsing.column-filter.actions
15
16
  = search_result_info @records
16
17
  = search_row class: 'ui mini form' do
@@ -18,6 +19,7 @@
18
19
  th= f.search_field :city_cont, placeholder: at(:city)
19
20
  th= f.search_field :department_cont, placeholder: at(:department)
20
21
  th= f.search_field :country_cont, placeholder: at(:country)
22
+ th
21
23
  th.collapsing.column-filter.actions
22
24
  button.ui.mini.primary.icon.button
23
25
  i.filter.icon
@@ -32,6 +34,10 @@
32
34
  td= record.city
33
35
  td= record.department
34
36
  td= record.country
37
+ td
38
+ .ui.horizontal.divided.list
39
+ - record.labels.each do |label|
40
+ = link_to label.name, label, class: 'item'
35
41
  td.collapsing.actions
36
42
  = table_show_link record
37
43
  = table_edit_link record
@@ -27,9 +27,13 @@
27
27
  = link_to 'Approve', geo_labels.approve_contact_path(@record.id), method: :put, class: 'ui right floated button positive'
28
28
  .ui.basic.label class=(@record.state == 'rejected' ? 'red' : (@record.state == 'approved' ? 'green' : 'yelow'))
29
29
  = @record.state
30
+ tr
31
+ td= at :url
32
+ td
33
+ a href=@record.url target='_blank' = @record.url
30
34
  tr
31
35
  td= at :description
32
- td= @record.description
36
+ td== @record.description
33
37
  - if @record.geocoded?
34
38
  tr
35
39
  td Location
@@ -0,0 +1,3 @@
1
+ .ui.container
2
+ = page_title t('geo_labels.about.title')
3
+ .ui.segment== t('geo_labels.about.content')
@@ -1,4 +1,8 @@
1
- .ui.grid.container
1
+ .ui.container
2
2
  = page_title t('geo_labels.export')
3
- .ui.segment
4
- p= link_to t('geo_labels.export'), geo_labels.export_file_path, class: 'ui primary button'
3
+ .ui.top.attached.segment
4
+ = link_to t('geo_labels.export_yml.title'), geo_labels.export_file_path(format: :yml), class: 'ui primary button'
5
+ .ui.basic.left.pointing.label= t 'geo_labels.export_yml.explanation'
6
+ .ui.bottom.attached.segment
7
+ = link_to t('geo_labels.export_csv.title'), geo_labels.export_file_path(format: :csv), class: 'ui primary button'
8
+ .ui.basic.left.pointing.label= t 'geo_labels.export_csv.explanation'
@@ -11,7 +11,11 @@
11
11
  .default.text= GeoLabels::Label.model_name.human_plural
12
12
  .menu
13
13
  /- ISO3166::Country.all.each do |country|
14
- - nested_set_options(GeoLabels::Label) {|i| "#{'-' * i.level} #{i.name}" }.each do |(label_name, label_id)|
14
+ - GeoLabels::Label.each_h_with_level do |label_h, level|
15
+ .item data-value=label_h[:id]
16
+ => '-' * level
17
+ = label_h[:name]
18
+ /- nested_set_options(GeoLabels::Label) {|i| "#{'-' * i.level} #{i.name}" }.each do |(label_name, label_id)|
15
19
  .item data-value=label_id
16
20
  = label_name
17
21
  .four.wide.field
@@ -47,6 +51,9 @@
47
51
  .ui.item
48
52
  = link_to contact.name, contact
49
53
  = address_tag contact.address, title: contact.description
54
+ - contact.labels.sort_by(&:name).each do |label|
55
+ span.ui.label= label.name
56
+
50
57
  .ui.bottom.attached.tab.segment data-tab='map'
51
58
  #map-canvas
52
59
  /.ui.horizontal.divider= GeoLabels::Contact.model_name.human_plural
@@ -0,0 +1,23 @@
1
+ .ui.inverted.menu.fixed
2
+ = link_to main_app.root_path, class: 'header item'
3
+ = Rails.application.config.x.geo_labels.link_home_content.call.to_s.html_safe
4
+ = link_to geo_labels.root_path, class: 'header item'
5
+ .ui.icon
6
+ i.map.icon
7
+ = Rails.application.config.x.geo_labels.application_title.call.to_s.html_safe
8
+ - if can? :read, GeoLabels::Contact
9
+ = link_to GeoLabels::Contact.model_name.human_plural, [GeoLabels::Contact], class: ['item', active_class('geo_labels/contacts')]
10
+ - if can? :read, GeoLabels::Label
11
+ = link_to GeoLabels::Label.model_name.human_plural, [GeoLabels::Label], class: ['item', active_class('geo_labels/labels')]
12
+ .right.menu
13
+ .ui.dropdown.item
14
+ span= current_user.try(:nickname) || current_user.try(:email) || 'Menu'
15
+ i.dropdown.icon
16
+ .menu
17
+ = render 'geo_labels/menu/right_menu_general'
18
+ - if current_user.present?
19
+ = link_to main_app.destroy_user_session_path, method: :delete, class: 'item'
20
+ i.sign.out.icon
21
+ span.text= t 'user.sign_out'
22
+ - else
23
+ = link_to t('devise.sessions.new.sign_in'), main_app.new_user_session_path, class: 'item'
@@ -0,0 +1,11 @@
1
+ = link_to geo_labels.about_path, class: 'item'
2
+ i.question.circle.outline.icon
3
+ span.text= t 'geo_labels.about.title'
4
+ - if can? :read, GeoLabels::Contact and can? :read, GeoLabels::Label
5
+ = link_to geo_labels.export_path, class: 'item'
6
+ i.file.export.icon
7
+ span.text= t('geo_labels.export')
8
+ - if can? :update, GeoLabels::Contact and can? :update, GeoLabels::Label
9
+ = link_to geo_labels.import_path, class: 'item'
10
+ i.file.import.icon
11
+ span.text= t('geo_labels.import')
@@ -0,0 +1,20 @@
1
+ .ui.top.attached.block.header= Rails.application.config.x.geo_labels.application_title.call
2
+ table.roles.ui.bottom.attached.compact.celled.table
3
+ thead
4
+ /th= HawkEye::Analysis.model_name.human
5
+ th
6
+ th read
7
+ th manage
8
+ tbody
9
+ tr
10
+ td Engine
11
+ td= dunlop_ability :read, 'engine', 'geo_labels'
12
+ td= dunlop_ability :manage, 'engine', 'geo_labels'
13
+ tr
14
+ td= GeoLabels::Contact.model_name.human
15
+ td= dunlop_class_ability :read, GeoLabels::Contact
16
+ td= dunlop_class_ability :manage, GeoLabels::Contact
17
+ tr
18
+ td= GeoLabels::Label.model_name.human
19
+ td= dunlop_class_ability :read, GeoLabels::Label
20
+ td= dunlop_class_ability :manage, GeoLabels::Label
@@ -15,7 +15,7 @@ html lang="en"
15
15
  window.geo_labels = #{{paths: GeoLabels::Engine.client_paths}.to_json.html_safe};
16
16
  = content_for :head
17
17
  body
18
- header= render 'navigation'
18
+ header= render 'geo_labels/menu/navigation'
19
19
  span= Rails.env
20
20
  main role="main"
21
21
  = render 'messages'
@@ -1,6 +1,23 @@
1
1
  en:
2
+ activerecord:
3
+ models:
4
+ geo_labels/contact: Contact
5
+ geo_labels/label: Label
6
+ plural:
7
+ geo_labels/contact: Contacts
8
+ geo_labels/label: Labels
9
+ attributes:
10
+ geo_labels/contact:
11
+ url: 'URL'
12
+
2
13
  geo_labels:
3
14
  export: Export
15
+ export_yml:
16
+ title: 'Export backup file (yaml)'
17
+ explanation: 'This export can be used to change and rebuild the data'
18
+ export_csv:
19
+ title: 'Export CSV'
20
+ explanation: 'This file is for spreadsheet views'
4
21
  import: Import
5
22
  tree_title: Tree
6
23
  select_labels_text: Select %{models} and search...
@@ -6,9 +6,19 @@ es:
6
6
  plural:
7
7
  geo_labels/contact: Contactos
8
8
  geo_labels/label: Etiquetas
9
+ attributes:
10
+ geo_labels/contact:
11
+ url: 'URL'
12
+
9
13
 
10
14
  geo_labels:
11
15
  export: Exportar
16
+ export_yml:
17
+ title: 'Exportar archivo de restaurar (yaml)'
18
+ explanation: 'Ese archivo se puede utilizar en rehacer el sistema'
19
+ export_csv:
20
+ title: 'Exportar CSV'
21
+ explanation: 'Ese archivo es para ver los datos en hoja de cálculo'
12
22
  import: Importar
13
23
  tree_title: Árbol
14
24
  select_labels_text: Seleccionar %{models} y busca...
@@ -20,3 +30,13 @@ es:
20
30
  Pues en Google maps se puede copiar y pegar<br>
21
31
  coordinatos manuál. Ejemplo&colon;<br>
22
32
  <code>4.973122, -75.623981</code>
33
+ about:
34
+ title: 'Sobre GEO'
35
+ content: >
36
+ <p>El sistema para manegar listas de contactos y poner etiquetas para buscarlos tiene
37
+ algunas opciones</p>
38
+
39
+ <h3>Manegar etiquetas</h3>
40
+ <p>Las etiquetas son flexibles en como estructurar los contactos. Etiquetas pueden tener
41
+ una jerarquia.
42
+ </p>
data/config/routes.rb CHANGED
@@ -7,6 +7,8 @@ GeoLabels::Engine.routes.draw do
7
7
  get :export_file
8
8
  get :import
9
9
  post :import_file
10
+
11
+ get :about
10
12
  end
11
13
 
12
14
  scope 'ratings', controller: :ratings do
@@ -0,0 +1,5 @@
1
+ class AddUrlToGeoLabelsContacts < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :geo_labels_contacts, :url, :string
4
+ end
5
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'yaml'
3
4
  module GeoLabels
5
+ # Yaml exporter of the engine
4
6
  class Exporter
5
7
 
6
8
  def self.export_h
@@ -20,7 +22,7 @@ module GeoLabels
20
22
  output = ''
21
23
  tree_formatter = proc do |items, lead_space|
22
24
  items.each do |item|
23
- output += "#{lead_space}#{item[:name]}\n"
25
+ output = "#{output}#{lead_space}#{item[:name]}\n"
24
26
  tree_formatter.call(item[:children], "#{lead_space} ") if item[:children].present?
25
27
  end
26
28
  end
@@ -33,7 +35,7 @@ module GeoLabels
33
35
  GeoLabels::Contact.includes(:labels).map do |contact|
34
36
  contact
35
37
  .attributes
36
- .slice(*%w[name street subsection city department country state description latitude longitude])
38
+ .slice(*%w[name street subsection city department country state url description latitude longitude])
37
39
  .merge('labels' => contact.labels.map(&:name))
38
40
  .select { |_, v| v.present? }
39
41
  .transform_values do |value|
@@ -58,5 +60,6 @@ module GeoLabels
58
60
  end
59
61
  tree[nil][:children]
60
62
  end
63
+
61
64
  end
62
65
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ require 'csv'
3
+
4
+ module GeoLabels
5
+ module Importer
6
+ class CsvData
7
+ attr_reader :new_contacts
8
+ ROW_ATTRIBUTES = %w[name description state street subsection city department country url latitude longitude]
9
+
10
+ def import(data)
11
+ @new_contacts = []
12
+
13
+ counter = 2 # start counting from 1 and skip the header. Works with Apple -> Numbers application
14
+ CSV.parse(data, headers: true).each do |row|
15
+ name = row['name']
16
+ raise "Row #{counter} has no name" unless name.present?
17
+
18
+ label_names = row['labels'].to_s.strip.split('|').map(&:strip).select(&:present?)
19
+ raise "Row #{counter} (#{name}) has no labels" unless label_names.any?
20
+ labels = label_names.map do |label_name|
21
+ labels_h[label_name] or raise "Row #{counter} has a label: '#{label_name}' that is not yet in the system. Please add before using it in an import"
22
+ end
23
+
24
+ contact = Contact.new(row.to_h.slice(*ROW_ATTRIBUTES))
25
+ contact.labels = labels
26
+ raise "Row #{counter} creates an invalid contact. Errors: #{contact.errors.full_messages.to_sentence}" unless contact.valid?
27
+ new_contacts.push contact
28
+ counter += 1
29
+ end
30
+
31
+ Contact.delete_all
32
+ ContactLabel.delete_all
33
+ new_contacts.each do |contact|
34
+ contact.save!
35
+ end
36
+ end
37
+
38
+ def labels_h
39
+ @labels_h ||= Label.all.map{ |l| [l.name, l] }.to_h
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GeoLabels
4
+ module Importer
5
+ class YamlData
6
+ def self.import(yaml_h)
7
+ GeoLabels::Contact.delete_all
8
+ GeoLabels::Label.delete_all
9
+ GeoLabels::ContactLabel.delete_all
10
+ labels_dict = persist_labels_tree yaml_h['labels']
11
+ yaml_h['contacts'].each do |contact_attributes|
12
+ label_names = Array(contact_attributes.delete('labels'))
13
+ contact = GeoLabels::Contact.new(contact_attributes)
14
+ contact.labels = label_names.map{ labels_dict[_1] }.compact
15
+ contact.save
16
+ end
17
+ GeoLabels::Contact.count
18
+ end
19
+
20
+ def self.labels_text_to_tree(tree_text)
21
+ txt = tree_text.gsub("\n\u0001", "\n") # remove START OF HEADING TRASH
22
+ txt.gsub! /^(\s*)[-*] /, '\1 ' # remove list punctuations and interpret just the spaces
23
+ lines = txt.split("\n")
24
+ root_label, current_indentation = ImportLabel.new('root'), -1
25
+ working_object = root_label
26
+ lines.each do |line|
27
+ line_indentation = (line.match(/\s+/).try(:[], 0).try(:size) || 0) / 2
28
+ # name = line[(2*line_indentation)..] # strip the leading spaces
29
+ name = line.strip
30
+ label = ImportLabel.new(name)
31
+ if line_indentation > current_indentation # level deeper
32
+ working_object = working_object.add_child(label)
33
+ elsif line_indentation < current_indentation
34
+ (current_indentation - line_indentation).times do
35
+ working_object = working_object.parent unless working_object.parent.is_root?
36
+ end
37
+ working_object = working_object.add_sibling(label)
38
+ else
39
+ working_object = working_object.add_sibling label
40
+ end
41
+ current_indentation = line_indentation
42
+ end
43
+ root_label
44
+ end
45
+
46
+ def self.persist_labels_tree(tree_or_text = nil)
47
+ tree = case tree_or_text
48
+ when ImportLabel then tree_or_text
49
+ when String then labels_text_to_tree(tree_or_text)
50
+ else
51
+ raise 'Must provide a tree label object or tree text'
52
+ end
53
+ record_lookup_dict = {}
54
+ persister = proc do |items, parent|
55
+ items.each do |item|
56
+ record = Label.create name: item.name, parent: parent
57
+ record_lookup_dict[item.name] = record
58
+ persister.call item.children, record if item.children.present?
59
+ end
60
+ end
61
+ persister.call(tree.children, nil)
62
+ record_lookup_dict
63
+ end
64
+
65
+ class LabelList < Array
66
+ attr_accessor :parent
67
+ end
68
+
69
+ class ImportLabel
70
+ attr_accessor :name, :parent
71
+
72
+ def initialize(name)
73
+ @name = name
74
+ end
75
+
76
+ def is_root?
77
+ name == 'root'
78
+ end
79
+
80
+ def children
81
+ @children ||= LabelList.new
82
+ end
83
+
84
+ def add_sibling(label)
85
+ label.parent = parent
86
+ parent.children.push label
87
+ label
88
+ end
89
+
90
+ def add_child(label)
91
+ children.push label
92
+ label.parent = self
93
+ label
94
+ end
95
+
96
+ def inspect
97
+ # "#{name} => #{children.inspect}"
98
+ # children.any? ? "<#{name}> => #{children.inpsect}" : "#{name}"
99
+ base = name.to_s
100
+ base = "#{base} => #{children.inspect}" if children.any?
101
+ base
102
+ end
103
+
104
+ def to_h
105
+ if is_root?
106
+ {labels: children.map(&:to_h)}
107
+ else
108
+ r = {name: name}
109
+ r[:children] = children.map(&:to_h) if children.any?
110
+ r
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -1,128 +1,49 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module GeoLabels
3
4
  module Importer
4
5
  def self.import(subject)
5
- yaml_h = case subject
6
- when String
7
- if subject.start_with?('/') or subject.start_with?('./')
8
- YAML.load_file(subject)
9
- else
10
- YAML.safe_load(subject)
11
- end
12
- when File then YAML.safe_load(subject.read)
13
- when Pathname then YAML.safe_load(subject.read)
14
- when ActionDispatch::Http::UploadedFile then YAML.safe_load(subject.read)
15
- else
16
- raise 'Argument not recognized'
17
- end
18
- GeoLabels::Contact.delete_all
19
- GeoLabels::Label.delete_all
20
- GeoLabels::ContactLabel.delete_all
21
- labels_dict = persist_labels_tree yaml_h['labels']
22
- yaml_h['contacts'].each do |contact_attributes|
23
- label_names = Array(contact_attributes.delete('labels'))
24
- contact = GeoLabels::Contact.new(contact_attributes)
25
- contact.labels = label_names.map{ labels_dict[_1] }.compact
26
- contact.save
27
- end
28
- GeoLabels::Contact.count
29
- end
6
+ type, data = get_type_and_data(subject)
30
7
 
31
- def self.labels_text_to_tree(tree_text)
32
- txt = tree_text.gsub("\n\u0001", "\n") # remove START OF HEADING TRASH
33
- txt.gsub! /^(\s*)[-*] /, '\1 ' # remove list punctuations and interpret just the spaces
34
- lines = txt.split("\n")
35
- root_label, current_indentation = ImportLabel.new('root'), -1
36
- working_object = root_label
37
- lines.each do |line|
38
- line_indentation = (line.match(/\s+/).try(:[], 0).try(:size) || 0) / 2
39
- # name = line[(2*line_indentation)..] # strip the leading spaces
40
- name = line.strip
41
- label = ImportLabel.new(name)
42
- if line_indentation > current_indentation # level deeper
43
- working_object = working_object.add_child(label)
44
- elsif line_indentation < current_indentation
45
- (current_indentation - line_indentation).times do
46
- working_object = working_object.parent unless working_object.parent.is_root?
47
- end
48
- working_object = working_object.add_sibling(label)
49
- else
50
- working_object = working_object.add_sibling label
51
- end
52
- current_indentation = line_indentation
8
+ if type == :yml
9
+ GeoLabels::Importer::YamlData.import(YAML.safe_load(data))
10
+ elsif type == :csv
11
+ GeoLabels::Importer::CsvData.new.import(data)
12
+ else
13
+ raise 'Argument not recognized'
53
14
  end
54
- root_label
55
15
  end
56
16
 
57
- def self.persist_labels_tree(tree_or_text = nil)
58
- tree = case tree_or_text
59
- when ImportLabel then tree_or_text
60
- when String then labels_text_to_tree(tree_or_text)
61
- else
62
- raise 'Must provide a tree label object or tree text'
63
- end
64
- record_lookup_dict = {}
65
- persister = proc do |items, parent|
66
- items.each do |item|
67
- record = Label.create name: item.name, parent: parent
68
- record_lookup_dict[item.name] = record
69
- persister.call item.children, record if item.children.present?
17
+ def self.get_type_and_data(subject)
18
+ case subject
19
+ when String
20
+ if subject.start_with?('/') or subject.start_with?('./') # is a path spec
21
+ return recognize_path(subject), File.read(subject)
22
+ else
23
+ if subject.start_with? "---\n" or subject.start_with?("labels: |\n")
24
+ return [:yml, subject]
25
+ elsif subject.start_with?('name,labels,')
26
+ return [:csv, subject]
27
+ else
28
+ raise 'The import string could not be recognized as either YAML or CSV'
29
+ end
70
30
  end
31
+ when File
32
+ return recognize_path(subject.path), subject.read
33
+ when Pathname
34
+ return recognize_path(subject.to_s), subject.read
35
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
36
+ return recognize_path(subject.original_filename), subject.read
37
+ else
38
+ raise 'Format of import not recognized'
71
39
  end
72
- persister.call(tree.children, nil)
73
- record_lookup_dict
74
- end
75
-
76
- class LabelList < Array
77
-
78
- attr_accessor :parent
79
40
  end
80
41
 
81
- class ImportLabel
82
-
83
- attr_accessor :name, :parent
84
-
85
- def initialize(name)
86
- @name = name
87
- end
42
+ def self.recognize_path(path)
43
+ return :yml if path.end_with?('.yml')
44
+ return :csv if path.end_with?('.csv')
88
45
 
89
- def is_root?
90
- name == 'root'
91
- end
92
-
93
- def children
94
- @children ||= LabelList.new
95
- end
96
-
97
- def add_sibling(label)
98
- label.parent = parent
99
- parent.children.push label
100
- label
101
- end
102
-
103
- def add_child(label)
104
- children.push label
105
- label.parent = self
106
- label
107
- end
108
-
109
- def inspect
110
- # "#{name} => #{children.inspect}"
111
- # children.any? ? "<#{name}> => #{children.inpsect}" : "#{name}"
112
- base = name.to_s
113
- base = "#{base} => #{children.inspect}" if children.any?
114
- base
115
- end
116
-
117
- def to_h
118
- if is_root?
119
- {labels: children.map(&:to_h)}
120
- else
121
- r = {name: name}
122
- r[:children] = children.map(&:to_h) if children.any?
123
- r
124
- end
125
- end
46
+ raise 'Imported path is neither YAML or CSV file'
126
47
  end
127
48
  end
128
49
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GeoLabels
4
- VERSION = '0.3.3'
4
+ VERSION = '0.4.3'
5
5
  end
data/lib/geo_labels.rb CHANGED
@@ -2,6 +2,8 @@
2
2
  require 'geo_labels/version'
3
3
  require 'geo_labels/engine'
4
4
  require 'geo_labels/importer'
5
+ require 'geo_labels/importer/csv_data'
6
+ require 'geo_labels/importer/yaml_data'
5
7
  require 'geo_labels/exporter'
6
8
  require 'fomantic-ui-sass'
7
9
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geo_labels
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin ter Kuile
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-07 00:00:00.000000000 Z
11
+ date: 2023-12-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_nested_set
@@ -128,14 +128,14 @@ dependencies:
128
128
  requirements:
129
129
  - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: '7.1'
132
132
  type: :runtime
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: '7.1'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: ransack
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -228,12 +228,12 @@ files:
228
228
  - app/views/geo_labels/application/_button-edit-record.html.slim
229
229
  - app/views/geo_labels/application/_button-new-record.html.slim
230
230
  - app/views/geo_labels/application/_messages.html.slim
231
- - app/views/geo_labels/application/_navigation.html.slim
232
231
  - app/views/geo_labels/contacts/_form.html.slim
233
232
  - app/views/geo_labels/contacts/edit.html.slim
234
233
  - app/views/geo_labels/contacts/index.html.slim
235
234
  - app/views/geo_labels/contacts/new.html.slim
236
235
  - app/views/geo_labels/contacts/show.html.slim
236
+ - app/views/geo_labels/dashboard/about.html.slim
237
237
  - app/views/geo_labels/dashboard/export.html.slim
238
238
  - app/views/geo_labels/dashboard/import.html.slim
239
239
  - app/views/geo_labels/dashboard/main.html.slim
@@ -242,6 +242,9 @@ files:
242
242
  - app/views/geo_labels/labels/index.html.slim
243
243
  - app/views/geo_labels/labels/new.html.slim
244
244
  - app/views/geo_labels/labels/show.html.slim
245
+ - app/views/geo_labels/menu/_navigation.html.slim
246
+ - app/views/geo_labels/menu/_right_menu_general.html.slim
247
+ - app/views/geo_labels/users/_abilities.html.slim
245
248
  - app/views/layouts/geo_labels/application.html.slim
246
249
  - config/environment.rb
247
250
  - config/initializers/human_plural.rb
@@ -254,10 +257,13 @@ files:
254
257
  - db/migrate/20230602182522_geo_labels_contacts_complex_name_to_name.rb
255
258
  - db/migrate/20230726154822_add_state_to_geo_labels_contacts.rb
256
259
  - db/migrate/20230801145902_add_food_rating_to_geo_labels_contacts.rb
260
+ - db/migrate/20231206183544_add_url_to_geo_labels_contacts.rb
257
261
  - lib/geo_labels.rb
258
262
  - lib/geo_labels/engine.rb
259
263
  - lib/geo_labels/exporter.rb
260
264
  - lib/geo_labels/importer.rb
265
+ - lib/geo_labels/importer/csv_data.rb
266
+ - lib/geo_labels/importer/yaml_data.rb
261
267
  - lib/geo_labels/version.rb
262
268
  - lib/tasks/geo_labels_tasks.rake
263
269
  homepage: https://gitlab.com/benja-2/geo_labels
@@ -282,7 +288,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
282
288
  - !ruby/object:Gem::Version
283
289
  version: '0'
284
290
  requirements: []
285
- rubygems_version: 3.4.10
291
+ rubygems_version: 3.3.26
286
292
  signing_key:
287
293
  specification_version: 4
288
294
  summary: Rails mountable engine to label search locations
@@ -1,29 +0,0 @@
1
- .ui.inverted.menu.fixed
2
- = link_to main_app.root_path, class: 'header item'
3
- = Rails.application.config.x.geo_labels.link_home_content.call.to_s.html_safe
4
- = link_to geo_labels.root_path, class: 'header item'
5
- .ui.icon
6
- i.map.icon
7
- = Rails.application.config.x.geo_labels.application_title.call.to_s.html_safe
8
- - if current_user.present?
9
- - if can? :read, GeoLabels::Contact
10
- = link_to GeoLabels::Contact.model_name.human_plural, [GeoLabels::Contact], class: ['item', active_class('geo_labels/contacts')]
11
- - if can? :read, GeoLabels::Label
12
- = link_to GeoLabels::Label.model_name.human_plural, [GeoLabels::Label], class: ['item', active_class('geo_labels/labels')]
13
- .right.menu
14
- - if current_user.present?
15
- .ui.dropdown.item
16
- span= current_user.respond_to?(:nickname) ? current_user.nickname : current_user.email
17
- i.dropdown.icon
18
- .menu
19
- = link_to geo_labels.export_path, class: 'item'
20
- i.file.export.icon
21
- span.text= t('geo_labels.export')
22
- = link_to geo_labels.import_path, class: 'item'
23
- i.file.import.icon
24
- span.text= t('geo_labels.import')
25
- = link_to main_app.destroy_user_session_path, method: :delete, class: 'item'
26
- i.sign.out.icon
27
- span.text= t 'user.sign_out'
28
- - else
29
- = link_to t('devise.sessions.new.sign_in'), main_app.new_user_session_path, class: 'item'