spina-admin-journal 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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