works_cited 0.1.11 → 0.1.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -8
  3. data/Gemfile.lock +9 -10
  4. data/README.md +27 -2
  5. data/VERSION +1 -1
  6. data/app/assets/javascripts/works_cited/addFields.js +44 -0
  7. data/app/assets/javascripts/works_cited/application.js +6 -1
  8. data/app/assets/javascripts/works_cited/loadPreview.js +45 -0
  9. data/app/assets/javascripts/works_cited/removeFields.js +37 -0
  10. data/app/assets/javascripts/works_cited/shared.js +59 -0
  11. data/app/assets/javascripts/works_cited/showFields.js +9 -0
  12. data/app/assets/javascripts/works_cited/submitForm.js +33 -0
  13. data/app/controllers/concerns/works_cited/params.rb +24 -0
  14. data/app/controllers/works_cited/citations_controller.rb +26 -13
  15. data/app/helpers/works_cited/application_helper.rb +46 -1
  16. data/app/models/works_cited/citation.rb +31 -31
  17. data/app/models/works_cited/contributor.rb +11 -28
  18. data/app/views/works_cited/citation_types/fields/_book.html.haml +1 -1
  19. data/app/views/works_cited/citation_types/fields/_electronic.html.haml +1 -1
  20. data/app/views/works_cited/citation_types/fields/_email.html.haml +1 -1
  21. data/app/views/works_cited/citation_types/fields/_interview.html.haml +1 -1
  22. data/app/views/works_cited/citation_types/fields/_periodical.html.haml +1 -1
  23. data/app/views/works_cited/citation_types/fields/_tweet.html.haml +1 -1
  24. data/app/views/works_cited/citations/_citation_fields.html.haml +28 -0
  25. data/app/views/works_cited/citations/_fields.html.haml +14 -0
  26. data/app/views/works_cited/citations/_form.html.haml +16 -53
  27. data/app/views/works_cited/citations/preview.html.haml +1 -1
  28. data/app/views/works_cited/contributors/_contributor_fields.html.haml +11 -0
  29. data/config/routes.rb +8 -2
  30. data/db/migrate/20210915160902_add_index_to_works_cited_contributors.rb +9 -0
  31. data/lib/works_cited/mixins/has_works_cited.rb +14 -1
  32. data/spec/dummy/app/controllers/doodads_controller.rb +2 -1
  33. data/spec/dummy/app/controllers/things_controller.rb +1 -1
  34. data/spec/dummy/app/views/doodads/_form.html.haml +2 -0
  35. data/spec/dummy/db/development.sqlite3 +0 -0
  36. data/spec/dummy/db/migrate/{20210902202653_create_works_cited_citations.works_cited.rb → 20210915161011_create_works_cited_citations.works_cited.rb} +0 -0
  37. data/spec/dummy/db/migrate/{20210902202654_create_works_cited_contributors.works_cited.rb → 20210915161012_create_works_cited_contributors.works_cited.rb} +0 -0
  38. data/spec/dummy/db/migrate/20210915161013_add_index_to_works_cited_contributors.works_cited.rb +9 -0
  39. data/spec/dummy/db/schema.rb +2 -1
  40. data/spec/dummy/db/test.sqlite3 +0 -0
  41. data/spec/dummy/log/development.log +27312 -15
  42. data/spec/dummy/log/test.log +29188 -0
  43. data/spec/factories/things.rb +8 -0
  44. data/spec/models/doodad_spec.rb +13 -1
  45. data/spec/models/thing_spec.rb +13 -1
  46. data/spec/models/works_cited/citation_spec.rb +12 -0
  47. data/spec/requests/works_cited/citations_spec.rb +10 -8
  48. data/spec/routing/works_cited/citations_routing_spec.rb +5 -1
  49. data/works_cited.gemspec +28 -16
  50. metadata +51 -15
  51. data/app/views/works_cited/contributors/_fields.html.haml +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b6430ed9d0d1f18dc7bd27c9710815e90813925f19c88eebaf91e54509cef1f
4
- data.tar.gz: 49fdafa4cb232c583ebe917ba1eb8bf33a007cf4cd66ef42734b91c0f03c0f34
3
+ metadata.gz: 033d5232ed69362edb7b0ab085c4c729375410926d86ede41585a409a82e83be
4
+ data.tar.gz: 54bb54fbfafa97b50e1485af06db35583ce20b8fa05bcc21738b73cd0a78cd36
5
5
  SHA512:
6
- metadata.gz: 9c9ec53fc50ad5051abddc755a06c20260cf72b8534039699bbcb68e1a586f5d348dd54b0126a3a4e717fd0a31f9f09edafa5cdc943035cd7f56611737220918
7
- data.tar.gz: 817041e6aff351dadc58f1ba2cc0b21a7b12e691e2b811268a5f17e1dee493b4fc6c555bb42e739a6274e27663ab079265f5cd3646defb1f552f3314ed804e1c
6
+ metadata.gz: d2d4d30e3e218374a33b2772117ecfa9b771eccb79f7e0e43a9d2b2e42b3fb2f5af47cd120de320852c9d757eb7bdd0f21c751520283f86c67e749c868d46ebe
7
+ data.tar.gz: 3cbab16a0fd3d123607e03a6b9dd5fd93938f1a5caa6bdbd34e77fd9b3f1a21411a393b7bb8581c4eedae271b4130596b9515e57893832bcc03781d38263307f
data/Gemfile CHANGED
@@ -5,7 +5,7 @@ source 'https://rubygems.org'
5
5
  gem 'rails', '>= 6', '< 7'
6
6
 
7
7
  gem 'haml-rails', '>= 2.0', '< 3'
8
- gem 'sass-rails', '>= 6', '< 7'
8
+ gem 'sass-rails', '>= 5', '< 7'
9
9
 
10
10
  gem 'cancancan', '>= 3.3', '< 4'
11
11
 
@@ -17,7 +17,7 @@ gem 'kaminari', '>= 1.2.1', '< 2'
17
17
  group :development, :test do
18
18
  gem 'database_cleaner', '>=1.8', '<2'
19
19
  gem 'devise', '>= 4.8', '< 5'
20
- gem 'faker', '>=0'
20
+ gem 'faker', '>= 0.2.19', '< 1'
21
21
  gem 'sqlite3', '>= 1.4.2', '< 2'
22
22
  end
23
23
 
@@ -25,16 +25,16 @@ group :development do
25
25
  gem 'bundler', '>= 2', '< 3'
26
26
  gem 'factory_bot_rails', '>=5.1', '<6'
27
27
  gem 'juwelier', '~> 2.1.0'
28
- gem 'rubocop', '>=0'
29
- gem 'rubocop-rails', '>=0'
30
- gem 'rubocop-rspec', '>=0'
28
+ gem 'rubocop', '>= 1.2.0', '<2'
29
+ gem 'rubocop-rails', '>=2.11.3', '<3'
30
+ gem 'rubocop-rspec', '>=2.4.0', '<3'
31
31
  end
32
32
 
33
33
  group :test do
34
34
  gem 'capybara', '>= 3.35', '< 4'
35
- gem 'rspec-html-matchers', '>= 0'
35
+ gem 'rspec-html-matchers', '>= 0.9.4', '< 1'
36
36
  gem 'rspec-rails', '~> 5.0.0'
37
- gem 'shoulda', '>= 0'
37
+ gem 'shoulda', '>= 4', '< 5'
38
38
  gem 'shoulda-matchers', '>=4.3', '<5'
39
- gem 'simplecov', '>= 0'
39
+ gem 'simplecov', '>= 0.21.2', '< 1'
40
40
  end
data/Gemfile.lock CHANGED
@@ -94,8 +94,7 @@ GEM
94
94
  factory_bot_rails (5.2.0)
95
95
  factory_bot (~> 5.2.0)
96
96
  railties (>= 4.2.0)
97
- faker (2.19.0)
98
- i18n (>= 1.6, < 2)
97
+ faker (0.3.1)
99
98
  faraday (1.7.1)
100
99
  faraday-em_http (~> 1.0)
101
100
  faraday-em_synchrony (~> 1.0)
@@ -342,21 +341,21 @@ DEPENDENCIES
342
341
  database_cleaner (>= 1.8, < 2)
343
342
  devise (>= 4.8, < 5)
344
343
  factory_bot_rails (>= 5.1, < 6)
345
- faker
344
+ faker (>= 0.2.19, < 1)
346
345
  haml-rails (>= 2.0, < 3)
347
346
  juwelier (~> 2.1.0)
348
347
  kaminari (>= 1.2.1, < 2)
349
348
  rails (>= 6, < 7)
350
- rspec-html-matchers
349
+ rspec-html-matchers (>= 0.9.4, < 1)
351
350
  rspec-rails (~> 5.0.0)
352
- rubocop
353
- rubocop-rails
354
- rubocop-rspec
355
- sass-rails (>= 6, < 7)
356
- shoulda
351
+ rubocop (>= 1.2.0, < 2)
352
+ rubocop-rails (>= 2.11.3, < 3)
353
+ rubocop-rspec (>= 2.4.0, < 3)
354
+ sass-rails (>= 5, < 7)
355
+ shoulda (>= 4, < 5)
357
356
  shoulda-matchers (>= 4.3, < 5)
358
357
  simple_form (>= 5.1, < 6)
359
- simplecov
358
+ simplecov (>= 0.21.2, < 1)
360
359
  sqlite3 (>= 1.4.2, < 2)
361
360
  vanilla_nested (>= 1.3, < 2)
362
361
 
data/README.md CHANGED
@@ -3,8 +3,6 @@ Works Cited allows you to add a list of the works cited in ActiveRecord objects,
3
3
 
4
4
  Works Cited uses CanCanCan to authorize the editing of citations. This makes it easy for you to control access.
5
5
 
6
- Works Cited is compatible with, but does not require, Rails Admin.
7
-
8
6
  ## Installation
9
7
  Add this line to your application's Gemfile:
10
8
 
@@ -85,6 +83,33 @@ Add the helpers to the relevant views
85
83
  = works_cited_list @record
86
84
  ```
87
85
 
86
+ To add routes so that you can edit citations on their own pages:
87
+
88
+ ```ruby
89
+ mount WorksCited::Engine => '/works_cited'
90
+ ```
91
+
92
+ To add the fields, nested inside your forms
93
+
94
+ ```haml
95
+ = form_for(@doodad) do |f|
96
+ = works_cited_citations_fields f
97
+ ```
98
+
99
+ Don't forget to add the controller concern to enable nested attributes with strong parameters
100
+
101
+ ```ruby
102
+ class RecipesController < ApplicationController
103
+ include Cookbook::Params
104
+
105
+ #...
106
+
107
+ def recipe_params
108
+ params.require(:doodad).permit(:name, :description, works_cited_params)
109
+ end
110
+ end
111
+ ```
112
+
88
113
  ## Contributing
89
114
  * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
90
115
  * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.11
1
+ 0.1.16
@@ -0,0 +1,44 @@
1
+ class addFields {
2
+ // This executes when the function is instantiated.
3
+ constructor() {
4
+ this.links = document.querySelectorAll('.add_fields')
5
+ this.iterateLinks()
6
+ const event = new Event('works_cited:load')
7
+ window.dispatchEvent(event);
8
+ }
9
+
10
+ iterateLinks() {
11
+ // If there are no links on the page, stop the function from executing.
12
+ if (this.links.length === 0) return
13
+ // Loop over each link on the page. A page could have multiple nested forms.
14
+ this.links.forEach(link => {
15
+ link.addEventListener('click', e => {
16
+ this.handleClick(link, e)
17
+ })
18
+ })
19
+ }
20
+
21
+ handleClick(link, e) {
22
+ // Stop the function from executing if a link or event were not passed into the function.
23
+ if (!link || !e) return
24
+ // Prevent the browser from following the URL.
25
+ e.preventDefault()
26
+ // Save a unique timestamp to ensure the key of the associated array is unique.
27
+ let time = new Date().getTime()
28
+ // Save the data id attribute into a variable. This corresponds to `new_object.object_id`.
29
+ let linkId = link.dataset.id
30
+ // Create a new regular expression needed to find any instance of the `new_object.object_id` used in the fields data attribute if there's a value in `linkId`.
31
+ let regexp = linkId ? new RegExp(linkId, 'g') : null
32
+ // Replace all instances of the `new_object.object_id` with `time`, and save markup into a variable if there's a value in `regexp`.
33
+ let newFields = regexp ? link.dataset.fields.replace(regexp, time) : null
34
+ // Add the new markup to the form if there are fields to add.
35
+ newFields ? link.insertAdjacentHTML('beforebegin', newFields) : null
36
+ // Tell it new fields are there
37
+ const event = new Event('works_cited:add_fields')
38
+ window.dispatchEvent(event);
39
+ }
40
+ }
41
+
42
+ // Wait for turbolinks to load, otherwise `document.querySelectorAll()` won't work
43
+ window.addEventListener('load', () => new addFields())
44
+ window.addEventListener('works_cited:add_fields', () => new addFields())
@@ -1 +1,6 @@
1
- //= require vanilla_nested
1
+ //= require works_cited/shared.js
2
+ //= require works_cited/addFields.js
3
+ //= require works_cited/removeFields.js
4
+ //= require works_cited/showFields.js
5
+ //= require works_cited/loadPreview.js
6
+ //= require works_cited/submitForm.js
@@ -0,0 +1,45 @@
1
+ function startPreview(url, id, citationLocator) {
2
+ var element = document.getElementById(id)
3
+ var fields = element.querySelectorAll('input, select');
4
+ var index = 0;
5
+
6
+ for (index = 0; index < fields.length; ++index) {
7
+ var textField = fields[index];
8
+ textField.addEventListener('change', (e) => loadPreview(url, citationLocator))
9
+ }
10
+ loadPreview(url, citationLocator)
11
+ }
12
+
13
+ function loadPreview(url, locator) {
14
+ // Build formData object.
15
+ var formData = new FormData(document.querySelector('form'))
16
+ var data = {}
17
+ data.citation = getFieldByLocator(formData, locator)
18
+ var authenticity_token = document.head.querySelector('meta[name="csrf-token"]').content
19
+ var preview = document.getElementById('preview')
20
+ var container = document.getElementById('preview-html')
21
+
22
+ fetch(url + '.json',
23
+ {
24
+ method: 'post',
25
+ headers: {
26
+ 'X-Requested-With': 'XMLHttpRequest',
27
+ 'X-CSRF-Token': authenticity_token,
28
+ 'Content-type': 'application/json'
29
+ },
30
+ body: JSON.stringify(data),
31
+ credentials: 'same-origin'
32
+ })
33
+ .then(function(response) {
34
+ // When the page is loaded convert it to json
35
+ return response.json()
36
+ })
37
+ .then(function(json) {
38
+ // get the html value for it
39
+ return json.html
40
+ })
41
+ .then((html) => {
42
+ preview.style.display = 'block'
43
+ container.innerHTML = html
44
+ })
45
+ }
@@ -0,0 +1,37 @@
1
+ class removeFields {
2
+ // This executes when the function is instantiated.
3
+ constructor() {
4
+ this.iterateLinks()
5
+ }
6
+
7
+ iterateLinks() {
8
+ document.querySelectorAll('.remove_fields').forEach(link => {
9
+ link.addEventListener('click', e => {
10
+ this.handleClick(link, e)
11
+ })
12
+ })
13
+ }
14
+
15
+ handleClick(link, e) {
16
+ // Stop the function from executing if a link or event were not passed into the function.
17
+ if (!link || !e) return
18
+ // Prevent the browser from following the URL.
19
+ e.preventDefault()
20
+ // Find the parent wrapper for the set of nested fields.
21
+ let fieldParent = link.closest('.nested-fields')
22
+ // If there is a parent wrapper, find the hidden delete field.
23
+ let deleteField = fieldParent
24
+ ? fieldParent.querySelector('input[type="hidden"]')
25
+ : null
26
+ // If there is a delete field, update the value to `1` and hide the corresponding nested fields.
27
+ if (deleteField) {
28
+ deleteField.value = 1
29
+ fieldParent.style.display = 'none'
30
+ }
31
+ }
32
+ }
33
+
34
+ // Wait for turbolinks to load, otherwise `document.querySelectorAll()` won't work
35
+ window.addEventListener('load', () => new removeFields())
36
+ window.addEventListener('works_cited:load', () => new removeFields())
37
+ window.addEventListener('works_cited:add_fields', () => new removeFields())
@@ -0,0 +1,59 @@
1
+ function packInContainer(container, keyParts, value) {
2
+ var part = keyParts.shift()
3
+ var packed = value
4
+ if(keyParts.length > 0) {
5
+ var previous = container[part] || {}
6
+ packed = packInContainer(previous, keyParts, value)
7
+ }
8
+
9
+ return {...container, [part]: packed}
10
+ }
11
+
12
+ function getWritableField(key) {
13
+ var fields = document.getElementsByName(key)
14
+ var found
15
+
16
+ fields.forEach((field) => {
17
+ if(field.readonly === undefined || !field.readonly) {
18
+ if(field.value) {
19
+ found = field.value
20
+ return
21
+ }
22
+ }
23
+ })
24
+
25
+ return found || ''
26
+ }
27
+
28
+ function getFieldByLocator(data, locator) {
29
+ var regex = /^\[(.*)\]$/
30
+ var container = {}
31
+
32
+ // Display the key/value pairs
33
+ for(var pair of data.entries()) {
34
+ // We want to avoid using pair[1] even though it's there because some fields are readonly
35
+ var key = pair[0]
36
+ if(key.startsWith(locator)) {
37
+ var strippedKeyString = key.replace(locator, '').replace(regex, "$1")
38
+ var keyParts = strippedKeyString.split('][')
39
+ var value = getWritableField(key)
40
+ console.warn(strippedKeyString)
41
+ container = packInContainer(container, keyParts, value)
42
+ }
43
+ }
44
+
45
+ return container
46
+ }
47
+
48
+ function getFormDataPacked(data) {
49
+ var container = {}
50
+
51
+ // Display the key/value pairs
52
+ for(var pair of data.entries()) {
53
+ // We want to avoid using pair[1] even though it's there because some fields are readonly
54
+ var key = pair[0]
55
+ container[key] = getWritableField(key)
56
+ }
57
+
58
+ return container
59
+ }
@@ -0,0 +1,9 @@
1
+ function showFields(citation_id, citation_type) {
2
+ citation = document.getElementById(`citation-${citation_id}`)
3
+ for (let element of citation.getElementsByClassName('fields')){
4
+ element.style.display='none'
5
+ element.readonly=true
6
+ }
7
+ console.warn(citation.getElementsByClassName(`fields-${citation_type}`)[0])
8
+ citation.getElementsByClassName(`fields-${citation_type}`)[0].style.display = 'block'
9
+ }
@@ -0,0 +1,33 @@
1
+ function post(path, params, method = 'post') {
2
+
3
+ // The rest of this code assumes you are not using a library.
4
+ // It can be made less verbose if you use one.
5
+ const form = document.createElement('form')
6
+ form.method = method
7
+ form.action = path
8
+
9
+ for (const key in params) {
10
+ if (params.hasOwnProperty(key)) {
11
+ const hiddenField = document.createElement('input')
12
+ hiddenField.type = 'hidden'
13
+ hiddenField.name = key
14
+ hiddenField.value = params[key]
15
+
16
+ form.appendChild(hiddenField)
17
+ }
18
+ }
19
+
20
+ document.body.appendChild(form)
21
+ form.submit()
22
+ }
23
+
24
+ function addFilterToSubmit() {
25
+ var form = document.querySelector('form')
26
+
27
+ form.onsubmit = function (e) {
28
+ e.preventDefault()
29
+ var formData = new FormData(form)
30
+ var revisedData = getFormDataPacked(formData)
31
+ return post(form.action, revisedData)
32
+ }
33
+ }
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WorksCited
4
+ # Concern to allow strong parameters when using accepts_nested_attributes_for to edit Cookbook Uses
5
+ module Params
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # rescue_from Pundit::NotAuthorizedError, with: :authorization_error
10
+ # include Pundit
11
+ end
12
+
13
+ def works_cited_params
14
+ {
15
+ works_cited_citations_attributes: [
16
+ :id, :citation_type, :title, :container_title, :publisher, :city, :edition, :volume,
17
+ :number, :series, :year, :record, :media, :url, :pages, :published_at, :online_database, :doi,
18
+ :accessed_at, :_destroy,
19
+ { works_cited_contributors_attributes: %i[id contributor_role first middle last suffix handle _destroy] }
20
+ ]
21
+ }
22
+ end
23
+ end
24
+ end
@@ -16,11 +16,10 @@ module WorksCited
16
16
 
17
17
  # GET /citations/preview
18
18
  def preview
19
- @citation = Citation.new(citation_params)
20
- authorize! :preview, @citation
21
-
22
- @contributors = contributors_from_params
23
- render :preview, layout: false
19
+ authorize! :preview, Citation
20
+ locals = { citation: citation_for_preview, contributors: contributors_for_preview }
21
+ html = render_to_string('preview', formats: [:html], layout: false, locals: locals)
22
+ render json: { html: html }
24
23
  end
25
24
 
26
25
  # GET /citations/new
@@ -55,9 +54,15 @@ module WorksCited
55
54
 
56
55
  private
57
56
 
58
- def contributors_from_params
57
+ def citation_for_preview
58
+ return Citation.new(preview_params) unless params[:id].present?
59
+
60
+ Citation.find(params[:id]).assign_attributes(preview_params)
61
+ end
62
+
63
+ def contributors_for_preview
59
64
  contributors_array = []
60
- raw_contributors = citation_params[:works_cited_contributors_attributes]
65
+ raw_contributors = preview_params[:works_cited_contributors_attributes]
61
66
  raw_contributors&.each do |_index, contributor|
62
67
  destroy = contributor.delete(:_destroy)
63
68
  next if destroy == '1'
@@ -92,12 +97,8 @@ module WorksCited
92
97
 
93
98
  def model_option_group(items, model)
94
99
  model_name = model.model_name
95
- OpenStruct.new(
96
- {
97
- record_type: model_name,
98
- records: items.map { |item| item_option(item, model_name) }
99
- }
100
- )
100
+ records = items.map { |item| item_option(item, model_name) }
101
+ OpenStruct.new({ record_type: model_name, records: records })
101
102
  end
102
103
 
103
104
  def set_records
@@ -109,6 +110,18 @@ module WorksCited
109
110
  end.compact
110
111
  end
111
112
 
113
+ # Only allow a list of trusted parameters through.
114
+ def preview_params
115
+ json = JSON.parse(request.raw_post)
116
+ json_params = ActionController::Parameters.new(json)
117
+ json_params.require(:citation).permit(
118
+ :id, :citation_type, :title, :container_title, :publisher, :city, :edition, :volume,
119
+ :number, :series, :year, :record, :media, :url, :pages, :published_at, :online_database, :doi,
120
+ :accessed_at,
121
+ works_cited_contributors_attributes: %i[id contributor_role first middle last suffix handle _destroy]
122
+ )
123
+ end
124
+
112
125
  # Only allow a list of trusted parameters through.
113
126
  def citation_params
114
127
  params.require(:citation).permit(
@@ -18,11 +18,15 @@ module WorksCited
18
18
  render path, citation: citation, contributors: contributors
19
19
  end
20
20
 
21
- def works_cited_citation_fields(form, citation_type)
21
+ def works_cited_type_fields(form, citation_type)
22
22
  path = partial_path_or_default('fields', citation_type)
23
23
  render path, f: form, citation_type: citation_type
24
24
  end
25
25
 
26
+ def works_cited_citations_fields(form)
27
+ render 'works_cited/citations/fields', form: form
28
+ end
29
+
26
30
  def list_names(names)
27
31
  first = first_contributor(names)
28
32
  first_contributor_name = first&.full_name(first.name_order)
@@ -36,6 +40,47 @@ module WorksCited
36
40
  end
37
41
  end
38
42
 
43
+ # This method creates a link with `data-id` `data-fields` attributes. These attributes are
44
+ # used to create new instances of the nested fields through Javascript.
45
+ def works_cited_link_to_add_fields(name, form, association, partial = nil)
46
+ # Takes an object (@person) and creates a new instance of its associated model (:addresses)
47
+ # To better understand, run the following in your terminal:
48
+ # rails c --sandbox
49
+ # @person = Person.new
50
+ # new_object = @person.send(:addresses).klass.new
51
+ new_object = form.object.send(association).klass.new
52
+
53
+ # Saves the unique ID of the object into a variable.
54
+ # This is needed to ensure the key of the associated array is unique. This is makes
55
+ # parsing the content in the `data-fields` attribute easier through Javascript.
56
+ # We could use another method to achive this.
57
+ id = new_object.object_id
58
+
59
+ # https://api.rubyonrails.org/ fields_for(record_name, record_object = nil, fields_options = {}, &block)
60
+ # record_name = :addresses
61
+ # record_object = new_object
62
+ # fields_options = { child_index: id }
63
+ # child_index` is used to ensure the key of the associated array is unique, and that it
64
+ # matched the value in the `data-id` attribute.
65
+ # `person[addresses_attributes][child_index_value][_destroy]`
66
+ fields = form.simple_fields_for(association, new_object, child_index: id) do |builder|
67
+ # `association.to_s.singularize + "_fields"` ends up evaluating to `address_fields`
68
+ # The render function will then look for `views/people/_address_fields.html.erb`
69
+ # The render function also needs to be passed the value of 'builder', because
70
+ # `views/people/_address_fields.html.erb` needs this to render the form tags.
71
+ partial ||= "#{association.to_s.singularize}_fields"
72
+ render(partial, record: new_object, f: builder)
73
+ end
74
+
75
+ # This renders a simple link, but passes information into `data` attributes.
76
+ # This info can be named anything we want, but in this case we chose `data-id:` and `data-fields:`.
77
+ # The `id:` is from `new_object.object_id`.
78
+ # The `fields:` are rendered from the `fields` blocks.
79
+ # We use `gsub("\n", "")` to remove anywhite space from the rendered partial.
80
+ # The `id:` value needs to match the value used in `child_index: id`.
81
+ link_to(name, '#', class: 'add_fields button button-action', data: { id: id, fields: fields.gsub("\n", '') })
82
+ end
83
+
39
84
  private
40
85
 
41
86
  def partial_path_or_default(purpose, citation_type)
@@ -17,16 +17,24 @@ module WorksCited
17
17
 
18
18
  # Relationships
19
19
  belongs_to :record, polymorphic: true
20
- has_many :works_cited_contributors, -> { order(:last, :first, :middle, :suffix, :handle) }, inverse_of: :works_cited_citation, class_name: 'WorksCited::Contributor',
21
- foreign_key: :works_cited_citation_id
22
- has_many :works_cited_authors, -> { authors.order(:last, :first, :middle, :suffix, :handle) }, inverse_of: :works_cited_citation, class_name: 'WorksCited::Contributor',
23
- foreign_key: :works_cited_citation_id
24
- accepts_nested_attributes_for :works_cited_contributors, allow_destroy: true
20
+ has_many :works_cited_contributors, lambda {
21
+ order(:last, :first, :middle, :suffix, :handle)
22
+ }, inverse_of: :works_cited_citation, class_name: 'WorksCited::Contributor',
23
+ foreign_key: :works_cited_citation_id, dependent: :destroy
24
+ has_many :works_cited_authors, lambda {
25
+ authors.order(:last, :first, :middle, :suffix, :handle)
26
+ }, inverse_of: :works_cited_citation, class_name: 'WorksCited::Contributor',
27
+ foreign_key: :works_cited_citation_id
28
+ accepts_nested_attributes_for :works_cited_contributors, reject_if: :all_blank, allow_destroy: true
25
29
 
26
30
  # Scopes
27
31
  scope :ordered_by_author, (lambda do
28
32
  joins(:works_cited_authors)
29
- .order('MIN(works_cited_contributors.last) ASC')
33
+ .order('MIN(works_cited_contributors.last) ASC, '\
34
+ 'MIN(works_cited_contributors.first) ASC, '\
35
+ 'MIN(works_cited_contributors.middle) ASC, '\
36
+ 'MIN(works_cited_contributors.suffix) ASC, '\
37
+ 'MIN(works_cited_contributors.handle) ASC')
30
38
  .group(:id)
31
39
  end)
32
40
 
@@ -40,17 +48,31 @@ module WorksCited
40
48
  end
41
49
 
42
50
  # Instance Methods
51
+ def works_cited_contributors_attributes=(raw_contributors)
52
+ array = []
53
+ raw_contributors&.each do |_index, contributor|
54
+ destroy = contributor.delete(:_destroy)
55
+ if destroy == '1'
56
+ Contributor.find(contributor[:id]).destroy if contributor[:id]
57
+ next
58
+ end
59
+
60
+ array << contributor
61
+ end
62
+ super array
63
+ end
64
+
43
65
  def record=(value)
44
66
  unless value.is_a? String
45
67
  super(value)
46
68
  return
47
69
  end
48
70
 
49
- model_name, id = value.split(':')
50
- return unless model_name.present? && id.present?
71
+ model_name, my_id = value.split(':')
72
+ return unless model_name.present? && my_id.present?
51
73
 
52
74
  model = model_name.constantize
53
- super model.find(id)
75
+ super model.find(my_id)
54
76
  end
55
77
 
56
78
  def periodical?
@@ -72,27 +94,5 @@ module WorksCited
72
94
  def email?
73
95
  citation_type == 'email'
74
96
  end
75
-
76
- if defined?(RailsAdmin)
77
- rails_admin do
78
- visible false
79
- edit do
80
- field :citation_type, :enum do
81
- enum do
82
- WorksCited.configuration.valid_citation_types
83
- end
84
- end
85
- field :works_cited_contributors do
86
- label 'Contributors'
87
- end
88
- include_all_fields
89
- field :media
90
- field :record do
91
- # Can't remove this using :inverse_of because it's polymorphic
92
- visible false
93
- end
94
- end
95
- end
96
- end
97
97
  end
98
98
  end