spina-admin-journal 0.1.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.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +99 -0
  4. data/Rakefile +36 -0
  5. data/app/assets/config/spina_admin_journal_manifest.js +3 -0
  6. data/app/assets/javascripts/spina/admin/journal/application.es6 +72 -0
  7. data/app/assets/stylesheets/spina/admin/journal/application.css +27 -0
  8. data/app/controllers/spina/admin/journal/application_controller.rb +26 -0
  9. data/app/controllers/spina/admin/journal/articles_controller.rb +125 -0
  10. data/app/controllers/spina/admin/journal/authors_controller.rb +83 -0
  11. data/app/controllers/spina/admin/journal/institutions_controller.rb +71 -0
  12. data/app/controllers/spina/admin/journal/issues_controller.rb +129 -0
  13. data/app/controllers/spina/admin/journal/journals_controller.rb +73 -0
  14. data/app/controllers/spina/admin/journal/volumes_controller.rb +79 -0
  15. data/app/models/spina/admin/journal.rb +11 -0
  16. data/app/models/spina/admin/journal/affiliation.rb +62 -0
  17. data/app/models/spina/admin/journal/article.rb +55 -0
  18. data/app/models/spina/admin/journal/author.rb +47 -0
  19. data/app/models/spina/admin/journal/authorship.rb +19 -0
  20. data/app/models/spina/admin/journal/institution.rb +32 -0
  21. data/app/models/spina/admin/journal/issue.rb +49 -0
  22. data/app/models/spina/admin/journal/journal.rb +50 -0
  23. data/app/models/spina/admin/journal/volume.rb +37 -0
  24. data/app/validators/spina/admin/journal/uri_validator.rb +24 -0
  25. data/app/views/layouts/spina/admin/journal/articles.html.haml +10 -0
  26. data/app/views/layouts/spina/admin/journal/authors.html.haml +10 -0
  27. data/app/views/layouts/spina/admin/journal/institutions.html.haml +10 -0
  28. data/app/views/layouts/spina/admin/journal/issues.html.haml +10 -0
  29. data/app/views/layouts/spina/admin/journal/journals.html.haml +10 -0
  30. data/app/views/layouts/spina/admin/journal/volumes.html.haml +10 -0
  31. data/app/views/spina/admin/hooks/journal/_head.html.haml +2 -0
  32. data/app/views/spina/admin/hooks/journal/_primary_navigation.html.haml +27 -0
  33. data/app/views/spina/admin/hooks/journal/_website_secondary_navigation.html.haml +0 -0
  34. data/app/views/spina/admin/journal/affiliations/_affiliation.html.haml +8 -0
  35. data/app/views/spina/admin/journal/application/_empty_list.html.haml +3 -0
  36. data/app/views/spina/admin/journal/articles/_article.html.haml +10 -0
  37. data/app/views/spina/admin/journal/articles/_form.html.haml +29 -0
  38. data/app/views/spina/admin/journal/articles/_form_authors.html.haml +10 -0
  39. data/app/views/spina/admin/journal/articles/_form_details.html.haml +52 -0
  40. data/app/views/spina/admin/journal/articles/edit.html.haml +1 -0
  41. data/app/views/spina/admin/journal/articles/index.html.haml +19 -0
  42. data/app/views/spina/admin/journal/articles/new.html.haml +1 -0
  43. data/app/views/spina/admin/journal/authors/_author.html.haml +8 -0
  44. data/app/views/spina/admin/journal/authors/_form.html.haml +29 -0
  45. data/app/views/spina/admin/journal/authors/_form_affiliation.html.haml +20 -0
  46. data/app/views/spina/admin/journal/authors/_form_articles.html.haml +18 -0
  47. data/app/views/spina/admin/journal/authors/_form_details.html.haml +6 -0
  48. data/app/views/spina/admin/journal/authors/edit.html.haml +1 -0
  49. data/app/views/spina/admin/journal/authors/index.html.haml +17 -0
  50. data/app/views/spina/admin/journal/authors/new.html.haml +1 -0
  51. data/app/views/spina/admin/journal/institutions/_form.html.haml +29 -0
  52. data/app/views/spina/admin/journal/institutions/_form_details.html.haml +9 -0
  53. data/app/views/spina/admin/journal/institutions/_form_view_affiliations.html.haml +10 -0
  54. data/app/views/spina/admin/journal/institutions/_institution.html.haml +9 -0
  55. data/app/views/spina/admin/journal/institutions/edit.html.haml +1 -0
  56. data/app/views/spina/admin/journal/institutions/index.html.haml +16 -0
  57. data/app/views/spina/admin/journal/institutions/new.html.haml +1 -0
  58. data/app/views/spina/admin/journal/issues/_form.html.haml +29 -0
  59. data/app/views/spina/admin/journal/issues/_form_articles.html.haml +14 -0
  60. data/app/views/spina/admin/journal/issues/_form_details.html.haml +32 -0
  61. data/app/views/spina/admin/journal/issues/_issue.html.haml +9 -0
  62. data/app/views/spina/admin/journal/issues/edit.html.haml +1 -0
  63. data/app/views/spina/admin/journal/issues/index.html.haml +18 -0
  64. data/app/views/spina/admin/journal/issues/new.html.haml +1 -0
  65. data/app/views/spina/admin/journal/journals/_form.html.haml +35 -0
  66. data/app/views/spina/admin/journal/journals/edit.html.haml +1 -0
  67. data/app/views/spina/admin/journal/journals/index.html.haml +27 -0
  68. data/app/views/spina/admin/journal/journals/new.html.haml +1 -0
  69. data/app/views/spina/admin/journal/volumes/_form.html.haml +15 -0
  70. data/app/views/spina/admin/journal/volumes/_form_details.html.haml +10 -0
  71. data/app/views/spina/admin/journal/volumes/_form_issues.html.haml +13 -0
  72. data/app/views/spina/admin/journal/volumes/_volume.html.haml +7 -0
  73. data/app/views/spina/admin/journal/volumes/edit.html.haml +1 -0
  74. data/app/views/spina/admin/journal/volumes/index.html.haml +17 -0
  75. data/app/views/spina/admin/journal/volumes/new.html.haml +1 -0
  76. data/config/locales/en.yml +184 -0
  77. data/config/routes.rb +21 -0
  78. data/db/migrate/20201216152147_create_spina_admin_journal_journals.rb +13 -0
  79. data/db/migrate/20201216153822_create_spina_admin_journal_volumes.rb +13 -0
  80. data/db/migrate/20201216155113_create_spina_admin_journal_issues.rb +15 -0
  81. data/db/migrate/20201216161122_create_spina_admin_journal_articles.rb +16 -0
  82. data/db/migrate/20201216205633_create_spina_admin_journal_institutions.rb +11 -0
  83. data/db/migrate/20201216211224_create_spina_admin_journal_authors.rb +7 -0
  84. data/db/migrate/20201216221146_create_spina_admin_journal_affiliations.rb +15 -0
  85. data/db/migrate/20201216230816_create_spina_admin_journal_authorships.rb +14 -0
  86. data/db/migrate/20201231165231_create_spina_admin_journal_parts.rb +12 -0
  87. data/db/migrate/20210424123450_add_json_attributes_to_spina_admin_journal_journals.rb +7 -0
  88. data/db/migrate/20210424123521_add_json_attributes_to_spina_admin_journal_issues.rb +7 -0
  89. data/db/migrate/20210424123555_add_json_attributes_to_spina_admin_journal_articles.rb +7 -0
  90. data/lib/spina/admin/journal.rb +18 -0
  91. data/lib/spina/admin/journal/engine.rb +17 -0
  92. data/lib/spina/admin/journal/version.rb +9 -0
  93. data/lib/tasks/spina/admin/journal_tasks.rake +5 -0
  94. data/vendor/assets/javascripts/spina/admin/journal/html5sortable.js +1295 -0
  95. metadata +388 -0
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Controller for {Institution} records
7
+ class InstitutionsController < ApplicationController
8
+ before_action :set_breadcrumb
9
+ before_action :set_tabs, except: %i[index destroy]
10
+ before_action :set_institution, only: %i[edit update destroy]
11
+
12
+ def index
13
+ @institutions = Institution.all
14
+ end
15
+
16
+ def new
17
+ @institution = Institution.new
18
+ add_breadcrumb t('.new')
19
+ end
20
+
21
+ def edit; end
22
+
23
+ def create
24
+ @institution = Institution.new(institution_params)
25
+
26
+ if @institution.save
27
+ redirect_to admin_journal_institutions_path, success: t('.saved')
28
+ else
29
+ render :new
30
+ end
31
+ end
32
+
33
+ def update
34
+ if @institution.update(institution_params)
35
+ redirect_to admin_journal_institutions_path, success: t('.saved')
36
+ else
37
+ render :edit
38
+ end
39
+ end
40
+
41
+ def destroy
42
+ @institution.destroy
43
+ respond_to do |format|
44
+ format.html do
45
+ redirect_to admin_journal_institutions_path, success: t('.deleted')
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def institution_params
53
+ params.require(:admin_journal_institution).permit(:name)
54
+ end
55
+
56
+ def set_breadcrumb
57
+ add_breadcrumb Institution.model_name.human(count: :many), admin_journal_institutions_path
58
+ end
59
+
60
+ def set_tabs
61
+ @tabs = %w[details view_affiliations]
62
+ end
63
+
64
+ def set_institution
65
+ @institution = Institution.find(params[:id])
66
+ add_breadcrumb @institution.name
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Controller for {Issue} records.
7
+ class IssuesController < ApplicationController
8
+ PARTS_PARAMS = [
9
+ :name, :title, :type, :content, :filename, :signed_blob_id, :alt, :attachment_id, :image_id,
10
+ { images_attributes: %i[filename signed_blob_id image_id alt],
11
+ content_attributes: [
12
+ :name, :title,
13
+ { parts_attributes: [
14
+ :name, :title, :type, :content, :filename, :signed_blob_id, :alt, :attachment_id, :image_id,
15
+ { images_attributes: %i[filename signed_blob_id image_id alt] }
16
+ ] }
17
+ ] }
18
+ ].freeze
19
+ CONTENT_PARAMS = Spina.config.locales.inject({}) do |params, locale|
20
+ params.merge("#{locale}_content_attributes": [*PARTS_PARAMS])
21
+ end
22
+ PARAMS = [:volume_id, :title, :date, { **CONTENT_PARAMS }].freeze
23
+ PARTS = %w[cover_img description].freeze
24
+
25
+ before_action :set_breadcrumb
26
+ before_action :set_tabs, except: %i[index destroy sort]
27
+ before_action :set_issue, only: %i[edit update destroy]
28
+ before_action :set_parts_attributes, only: %i[new edit]
29
+ before_action :build_parts, only: %i[edit]
30
+
31
+ def index
32
+ @issues = Issue.sorted_desc
33
+ end
34
+
35
+ def new
36
+ @issue = Issue.new
37
+ build_parts
38
+ add_breadcrumb t('.new')
39
+ end
40
+
41
+ def edit
42
+ add_breadcrumb t('spina.admin.journal.volumes.volume_number', number: @issue.volume.number),
43
+ edit_admin_journal_volume_path(@issue.volume)
44
+ add_breadcrumb t('spina.admin.journal.issues.issue_number', number: @issue.number)
45
+ end
46
+
47
+ def create
48
+ @issue = Issue.new(issue_params)
49
+ sister_issues = Issue.where(volume: @issue.volume_id)
50
+ @issue.number = sister_issues.any? ? sister_issues.sorted_desc.first.number + 1 : 1
51
+
52
+ if @issue.save
53
+ redirect_to admin_journal_issues_path, success: t('.saved')
54
+ else
55
+ render :new
56
+ end
57
+ end
58
+
59
+ def update
60
+ if @issue.update(issue_params)
61
+ redirect_to admin_journal_issues_path, success: t('.saved')
62
+ else
63
+ render :edit
64
+ end
65
+ end
66
+
67
+ def destroy
68
+ @issue.destroy
69
+ respond_to do |format|
70
+ format.html do
71
+ redirect_to admin_journal_issues_path, success: t('.deleted')
72
+ end
73
+ end
74
+ end
75
+
76
+ def sort
77
+ ActiveRecord::Base.transaction do
78
+ sort_params.each do |id, new_pos|
79
+ # ignore uniqueness validation for now
80
+ Issue.find(id.to_i).update_attribute(:number, new_pos.to_i) # rubocop:disable Rails/SkipsModelValidations
81
+ end
82
+ validate_sort_order
83
+ end
84
+ render json: { success: true, message: t('.sort_success') }
85
+ rescue ActiveRecord::RecordInvalid
86
+ render json: { success: false, message: t('.sort_error') }
87
+ end
88
+
89
+ private
90
+
91
+ def set_issue
92
+ @issue = Issue.find(params[:id])
93
+ end
94
+
95
+ def issue_params
96
+ params.require(:admin_journal_issue).permit(PARAMS)
97
+ end
98
+
99
+ def sort_params
100
+ params.require(:admin_journal_issues).require(:list).permit!
101
+ end
102
+
103
+ def validate_sort_order
104
+ Issue.where(volume_id: params[:volume_id]).each do |issue|
105
+ raise ActiveRecord::RecordInvalid if issue.invalid?
106
+ end
107
+ end
108
+
109
+ def set_breadcrumb
110
+ add_breadcrumb Issue.model_name.human(count: :many), admin_journal_issues_path
111
+ end
112
+
113
+ def set_tabs
114
+ @tabs = %w[details articles]
115
+ end
116
+
117
+ def set_parts_attributes
118
+ @parts_attributes = current_theme.parts.select { |part| PARTS.include? part[:name] }
119
+ end
120
+
121
+ def build_parts
122
+ return unless @parts_attributes.is_a? Array
123
+
124
+ @parts = @parts_attributes.collect { |part_attributes| @issue.part(part_attributes) }
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Controller for {Journal} records.
7
+ # A site only ever has a single journal, so the index action is not needed.
8
+ class JournalsController < ApplicationController
9
+ PARTS_PARAMS = [
10
+ :name, :title, :type, :content, :filename, :signed_blob_id, :alt, :attachment_id, :image_id,
11
+ { images_attributes: %i[filename signed_blob_id image_id alt],
12
+ content_attributes: [
13
+ :name, :title,
14
+ { parts_attributes: [
15
+ :name, :title, :type, :content, :filename, :signed_blob_id, :alt, :attachment_id, :image_id,
16
+ { images_attributes: %i[filename signed_blob_id image_id alt] }
17
+ ] }
18
+ ] }
19
+ ].freeze
20
+ CONTENT_PARAMS = Spina.config.locales.inject({}) do |params, locale|
21
+ params.merge("#{locale}_content_attributes": [*PARTS_PARAMS])
22
+ end
23
+ PARAMS = [:name, { **CONTENT_PARAMS }].freeze
24
+ PARTS = %w[logo description].freeze
25
+
26
+ before_action :set_journal
27
+ before_action :set_parts_attributes
28
+ before_action :build_parts, only: %i[edit]
29
+
30
+ def edit
31
+ add_breadcrumb @journal.name
32
+ end
33
+
34
+ def update
35
+ if @journal.update(journal_params)
36
+ redirect_to edit_admin_journal_journal_path(@journal), success: t('.saved')
37
+ else
38
+ render :edit
39
+ end
40
+ end
41
+
42
+ def destroy
43
+ @journal.destroy
44
+ respond_to do |format|
45
+ format.html do
46
+ redirect_to edit_admin_journal_journal_path(Journal.instance), success: t('.deleted')
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def set_journal
54
+ @journal = Journal.instance
55
+ end
56
+
57
+ def set_parts_attributes
58
+ @parts_attributes = current_theme.parts.select { |part| PARTS.include? part[:name] }
59
+ end
60
+
61
+ def build_parts
62
+ return unless @parts_attributes.is_a? Array
63
+
64
+ @parts = @parts_attributes.collect { |part_attributes| @journal.part(part_attributes) }
65
+ end
66
+
67
+ def journal_params
68
+ params.require(:admin_journal_journal).permit(PARAMS)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Controller for {Volume} records.
7
+ class VolumesController < ApplicationController
8
+ before_action :set_breadcrumb
9
+ before_action :set_tabs, except: %i[index destroy]
10
+ before_action :set_volume, only: %i[edit destroy]
11
+
12
+ def index
13
+ @volumes = Volume.sorted_asc
14
+ end
15
+
16
+ def edit; end
17
+
18
+ def create # rubocop:disable Metrics/AbcSize
19
+ @volume = Volume.new
20
+ @volume.journal_id = Journal.instance.id
21
+ @volume.number = Volume.any? ? Volume.sorted_desc.first.number + 1 : 1
22
+ @volume.save!
23
+ redirect_to admin_journal_volumes_path, success: t('.created', number: @volume.number)
24
+ rescue ActiveRecord::RecordNotUnique
25
+ # can only happen because of some race condition where two Volumes are created at the same time
26
+ logger.error 'Error when creating new volume. Retrying...'
27
+ retry
28
+ end
29
+
30
+ def destroy
31
+ @volume.destroy
32
+ respond_to do |format|
33
+ format.html do
34
+ redirect_to admin_journal_volumes_path, success: 'Volume deleted.'
35
+ end
36
+ end
37
+ end
38
+
39
+ def sort
40
+ ActiveRecord::Base.transaction do
41
+ sort_params.each do |id, new_pos|
42
+ # ignore uniqueness validation for now
43
+ Volume.find(id.to_i).update_attribute(:number, new_pos.to_i) # rubocop:disable Rails/SkipsModelValidations
44
+ end
45
+ validate_sort_order
46
+ end
47
+ render json: { success: true, message: t('.sort_success') }
48
+ rescue ActiveRecord::RecordInvalid
49
+ render json: { success: false, message: t('.sort_error') }
50
+ end
51
+
52
+ private
53
+
54
+ def sort_params
55
+ params.require(:admin_journal_volumes).require(:list).permit!
56
+ end
57
+
58
+ def validate_sort_order
59
+ Volume.where(journal_id: params[:journal_id]).each do |volume|
60
+ raise ActiveRecord::RecordInvalid if volume.invalid?
61
+ end
62
+ end
63
+
64
+ def set_breadcrumb
65
+ add_breadcrumb Volume.model_name.human(count: :many), admin_journal_volumes_path
66
+ end
67
+
68
+ def set_volume
69
+ @volume = Volume.find(params[:id])
70
+ add_breadcrumb t('spina.admin.journal.volumes.volume_number', number: @volume.number)
71
+ end
72
+
73
+ def set_tabs
74
+ @tabs = %w[details issues]
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal # rubocop:disable Style/Documentation
6
+ def self.table_name_prefix
7
+ 'spina_admin_journal_'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Rich join between an Institution and an Author. Associated with Articles via Authorships.
7
+ #
8
+ # Since authors can in principle use different names in different articles, names are associated
9
+ # with affiilations, rather than with authors directly.
10
+ #
11
+ # === Validators
12
+ # Presence:: {#first_name}, {#surname}
13
+ #
14
+ # === Scopes
15
+ # sorted:: sorted by surname in alphabetical order
16
+ #
17
+ # @see Institution
18
+ # @see Author
19
+ # @see Article
20
+ # @see Authorship
21
+ class Affiliation < ApplicationRecord
22
+ # @!attribute [rw] first_name
23
+ # @return [String] the first name(s) of the author
24
+ # @!attribute [rw] surname
25
+ # @return [String] the last name(s) of the author
26
+ # @!attribute [rw] status
27
+ # @return [Integer] the status of the author, which can be either primary (1) or other (0)
28
+ # @!attribute [rw] author
29
+ # @return [ActiveRecord::Relation] the {Author} whose affiliation this record holds
30
+ belongs_to :author
31
+ # @!attribute [rw] institution
32
+ # @return [ActiveRecord::Relation] the {Institution} to which the author is affiliated
33
+ belongs_to :institution
34
+ # @!attribute [rw] authorships
35
+ # @return [ActiveRecord::Relation] directly associated {Authorship}s
36
+ has_many :authorships, dependent: :destroy
37
+ # @!attribute [rw] articles
38
+ # @return [ActiveRecord::Relation] {Article}s associated through {Authorship}s
39
+ has_many :articles, through: :authorships
40
+
41
+ validates :first_name, presence: true
42
+ validates :surname, presence: true
43
+
44
+ enum status: { primary: 1, other: 0 }
45
+
46
+ scope :sorted, -> { order(surname: :asc) }
47
+
48
+ # @!attribute [r] name
49
+ # @return [String] The full name of the author.
50
+ def name
51
+ "#{first_name} #{surname}"
52
+ end
53
+
54
+ # @!attribute [r] reversed_name
55
+ # @return [String] The full name of the author, surname first.
56
+ def reversed_name
57
+ "#{surname}, #{first_name}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Record for an individual article.
7
+ #
8
+ # === Validators
9
+ # Presence:: {#number}, {#title}
10
+ # Uniqueness:: {#number} (scope: issue)
11
+ # URI:: {#url}
12
+ #
13
+ # === Scopes
14
+ # sorted_asc:: sorted in order of increasing number
15
+ # sorted_desc:: sorted highest number first
16
+ #
17
+ # @see Issue
18
+ # @see Author
19
+ # @see Authorship
20
+ class Article < ApplicationRecord
21
+ include AttrJson::Record
22
+ include AttrJson::NestedAttributes
23
+ include Spina::Partable
24
+ include Spina::TranslatedContent
25
+ # @!attribute [rw] number
26
+ # @return [Integer] The position of the article within its issue.
27
+ # @!attribute [rw] title
28
+ # @return [String] The article's title.
29
+ # @!attribute [rw] url
30
+ # @return [String] An external link to the article.
31
+ # @!attribute [rw] doi
32
+ # @return [String] A digital object intentifier for the article.
33
+ # @!attribute [rw] issue
34
+ # @return [Issue] The issue that contains this article.
35
+ belongs_to :issue
36
+ # @!attribute [rw] file
37
+ # @return [Spina::Attachment] The attached file
38
+ belongs_to :file, class_name: 'Spina::Attachment', optional: true
39
+
40
+ # @!attribute [rw] authorships
41
+ has_many :authorships, dependent: :destroy
42
+ # @!attribute [rw] affiliations
43
+ # @return [ActiveRecord::Relation] The authors of the article.
44
+ has_many :affiliations, through: :authorships
45
+
46
+ validates :number, presence: true, uniqueness: { scope: :issue_id }
47
+ validates :title, presence: true
48
+ validates :url, 'spina/admin/journal/uri': true
49
+
50
+ scope :sorted_asc, -> { includes(:issue).order('spina_admin_journal_issues.number ASC', number: :asc) }
51
+ scope :sorted_desc, -> { includes(:issue).order('spina_admin_journal_issues.number DESC', number: :desc) }
52
+ end
53
+ end
54
+ end
55
+ end