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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Authors are collections of {Affiliation}s.
7
+ #
8
+ # Since people can have multiple names, titles, institutions, etc., these details are all
9
+ # handled by the {Affiliation} record. This record groups said records together, in order
10
+ # that a coherent oeuvre of a single author be identified, which is more user-friendly.
11
+ #
12
+ # @note An author must have at least one Affiliation with +status: primary+, else validation
13
+ # will fail.
14
+ #
15
+ # @see Affiliation
16
+ class Author < ApplicationRecord
17
+ # @!attribute [rw] affiliations
18
+ # @return [ActiveRecord::Relation] Directly associated {Affiliation}s.
19
+ has_many :affiliations, inverse_of: :author, dependent: :destroy
20
+ accepts_nested_attributes_for :affiliations
21
+ # @!attribute [rw] institutions
22
+ # @return [ActiveRecord::Relation] The {Institution}s corresponding to this Author's affiliations.
23
+ has_many :institutions, through: :affiliations
24
+ # @!attribute [rw] articles
25
+ # @return [ActiveRecord::Relation] {Article}s associated through authorships.
26
+ has_many :articles, through: :affiliations
27
+
28
+ validate :must_have_one_primary_affiliation
29
+
30
+ # @!attribute [r] primary_affiliation
31
+ # @return [ActiveRecord::Relation] The author's primary affiliation.
32
+ def primary_affiliation
33
+ affiliations.primary.first || Affiliation.new(status: :primary)
34
+ end
35
+
36
+ private
37
+
38
+ def must_have_one_primary_affiliation
39
+ return if !affiliations.nil? && affiliations.any? \
40
+ && (affiliations.count { |affiliation| affiliation.status == 'primary' } == 1)
41
+
42
+ errors.add :affiliations, 'must have at least one primary affiliation'
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Joins Articles and Affiliations
7
+ # @see Article
8
+ # @see Affiliation
9
+ class Authorship < ApplicationRecord
10
+ # @!attribute [rw] article
11
+ # @return [ActiveRecord::Relation] the associated article
12
+ belongs_to :article
13
+ # @!attribute [rw] author_name
14
+ # @return [ActiveRecord::Relation] the associated affiliation of an author of the article
15
+ belongs_to :affiliation
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Record for an institution. Associated with {Author}s via {Affiliation}s.
7
+ #
8
+ # === Validators
9
+ # Presence:: {#name}
10
+ # Uniqueness:: {#name}
11
+ #
12
+ # === Scopes
13
+ # sorted:: sort institutions in alphabetical order
14
+ #
15
+ # @see Affiliation
16
+ class Institution < ApplicationRecord
17
+ # @!attribute [rw] name
18
+ # @return [String]
19
+ # @!attribute [rw] affiliations
20
+ # @return [ActiveRecord::Relation] the affiliations held at the institution
21
+ has_many :affiliations, dependent: :destroy
22
+ # @!attribute [rw] authors
23
+ # @return [ActiveRecord::Relation] associated authors
24
+ has_many :authors, through: :affiliations
25
+
26
+ validates :name, presence: true, uniqueness: { case_sensitive: false }
27
+
28
+ scope :sorted, -> { order(name: :asc) }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Record for an issue of a journal.
7
+ # Belongs to a particular volume of a journal. Has many articles.
8
+ #
9
+ # === Validators
10
+ # Presence:: {#number}, {#date}
11
+ # Uniqueness:: {#number} (scope: volume)
12
+ #
13
+ # === Scopes
14
+ # sorted_asc:: sorted in order of increasing number
15
+ # sorted_desc:: sorted highest number first
16
+ #
17
+ # @see Article
18
+ # @see Volume
19
+ class Issue < ApplicationRecord
20
+ include AttrJson::Record
21
+ include AttrJson::NestedAttributes
22
+ include Spina::Partable
23
+ include Spina::TranslatedContent
24
+ # @!attribute [rw] number
25
+ # @return [Integer] The number
26
+ # @!attribute [rw] title
27
+ # @return [String] The title of the issue (optional)
28
+ # @!attribute [rw] date
29
+ # @return [Date] The (planned) publication date of the issue.
30
+ # @!attribute [rw] volume
31
+ # @return [ActiveRecord::Relation] the volume that contains this issue
32
+ belongs_to :volume
33
+ # @!attribute [rw] cover_img
34
+ # @return [Spina::Image, nil] the issue's cover image
35
+ belongs_to :cover_img, class_name: 'Spina::Image', optional: true
36
+
37
+ # @!attribute [rw] issue
38
+ # @return [ActiveRecord::Relation] the articles within this issue
39
+ has_many :articles, dependent: :destroy
40
+
41
+ validates :number, presence: true, uniqueness: { scope: :volume_id }
42
+ validates :date, presence: true
43
+
44
+ scope :sorted_asc, -> { includes(:volume).order('spina_admin_journal_volumes.number ASC', number: :asc) }
45
+ scope :sorted_desc, -> { includes(:volume).order('spina_admin_journal_volumes.number DESC', number: :desc) }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Journal records. The top level of the database hierarchy.
7
+ #
8
+ # There should only ever be a single Journal record, which must always be accessed
9
+ # using {Journal.instance}
10
+ #
11
+ # === Validatorss
12
+ # Presence:: {#name}, {#singleton_guard}
13
+ # Uniqueness:: {#name}, {#singleton_guard}
14
+ class Journal < ApplicationRecord
15
+ include AttrJson::Record
16
+ include AttrJson::NestedAttributes
17
+ include Spina::Partable
18
+ include Spina::TranslatedContent
19
+ # @!attribute [rw] name
20
+ # @return [String] The name of the journal.
21
+ # @!attribute [rw] singleton_guard
22
+ # @return [Integer] Used to guarantee that a record is unique.
23
+ # @!attribute [rw] logo
24
+ # @return [Spina::Image, nil] The Spina::Image representing the logo of the journal.
25
+ belongs_to :logo, class_name: 'Spina::Image', optional: true
26
+
27
+ # @!attribute [rw] volumes
28
+ # @return [ActiveRecord::Relation] directly associated volumes
29
+ has_many :volumes, dependent: :destroy
30
+
31
+ validates :name, presence: true, uniqueness: true
32
+ validates :singleton_guard, presence: true, uniqueness: true
33
+
34
+ # Access the journal record.
35
+ #
36
+ # It is possible that Journal.instance gets called from two threads simultaneously.
37
+ # If this occurs, +Journal.create!+ will throw a validation error, since both records will
38
+ # be assigned the same value for +singleton_guard+. This is handled automatically, guaranteeing
39
+ # that there only ever be a single record.
40
+ def self.instance
41
+ Journal.first || Journal.create!(name: I18n.t('spina.admin.journal.unnamed_journal'), singleton_guard: 0)
42
+ rescue ActiveRecord::RecordNotUnique
43
+ # prevent race conditions leading to multiple records being created
44
+ logger.error 'Error when retrieving Journal instance. Retrying...'
45
+ retry
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spina
4
+ module Admin
5
+ module Journal
6
+ # Volumes of a journal.
7
+ # A journal has multiple volumes, a volume can have multiple issues.
8
+ #
9
+ # === Validators
10
+ # Presence:: {#number}
11
+ # Uniqueness:: {#number} (scope: journal)
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 Journal
19
+ class Volume < ApplicationRecord
20
+ # @!attribute [rw] number
21
+ # @return [Integer]
22
+ # @!attribute [rw] journal
23
+ # @return [ActiveRecord::Relation] the journal that contains this volume
24
+ belongs_to :journal
25
+
26
+ # @!attribute [rw] journal
27
+ # @return [ActiveRecord::Relation] the issues that comprise this volume
28
+ has_many :issues, dependent: :destroy
29
+
30
+ validates :number, presence: true, uniqueness: { scope: :journal_id }
31
+
32
+ scope :sorted_asc, -> { order(number: :asc) }
33
+ scope :sorted_desc, -> { order(number: :desc) }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Spina
6
+ module Admin
7
+ module Journal
8
+ # A simple validator for URIs.
9
+ class UriValidator < ActiveModel::EachValidator
10
+ def validate_each(record, attribute, value)
11
+ return if value == '' || value.nil?
12
+
13
+ uri = URI.parse(value)
14
+
15
+ return if uri.is_a?(URI::HTTP) && !uri.host.nil?
16
+
17
+ record.errors.add attribute, :invalid_uri
18
+ rescue URI::InvalidURIError
19
+ record.errors.add attribute, :invalid_uri
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ - content_for :application do
2
+ %header#header
3
+ #header_actions
4
+ = yield :header_actions
5
+
6
+ = render partial: 'spina/admin/shared/breadcrumbs'
7
+
8
+ = yield
9
+
10
+ = render template: 'layouts/spina/admin/admin'
@@ -0,0 +1,10 @@
1
+ - content_for :application do
2
+ %header#header
3
+ #header_actions
4
+ = yield :header_actions
5
+
6
+ = render partial: 'spina/admin/shared/breadcrumbs'
7
+
8
+ = yield
9
+
10
+ = render template: 'layouts/spina/admin/admin'
@@ -0,0 +1,10 @@
1
+ - content_for :application do
2
+ %header#header
3
+ #header_actions
4
+ = yield :header_actions
5
+
6
+ = render partial: 'spina/admin/shared/breadcrumbs'
7
+
8
+ = yield
9
+
10
+ = render template: 'layouts/spina/admin/admin'
@@ -0,0 +1,10 @@
1
+ - content_for :application do
2
+ %header#header
3
+ #header_actions
4
+ = yield :header_actions
5
+
6
+ = render partial: 'spina/admin/shared/breadcrumbs'
7
+
8
+ = yield
9
+
10
+ = render template: 'layouts/spina/admin/admin'
@@ -0,0 +1,10 @@
1
+ - content_for :application do
2
+ %header#header
3
+ #header_actions
4
+ = yield :header_actions
5
+
6
+ = render partial: 'spina/admin/shared/breadcrumbs'
7
+
8
+ = yield
9
+
10
+ = render template: 'layouts/spina/admin/admin'
@@ -0,0 +1,10 @@
1
+ - content_for :application do
2
+ %header#header
3
+ #header_actions
4
+ = yield :header_actions
5
+
6
+ = render partial: 'spina/admin/shared/breadcrumbs'
7
+
8
+ = yield
9
+
10
+ = render template: 'layouts/spina/admin/admin'
@@ -0,0 +1,2 @@
1
+ = stylesheet_link_tag 'spina/admin/journal/application', media: 'all', data: { turbolinks_track: true }
2
+ = javascript_include_tag 'spina/admin/journal/application'
@@ -0,0 +1,27 @@
1
+ %li{ class: ('active' if %w[articles issues volumes].include? controller_name) }
2
+ = link_to admin_journal_volumes_path do
3
+ = icon 'pages'
4
+ = ::Spina::Admin::Journal::Journal.instance.name
5
+ = icon 'caret-right'
6
+
7
+ %ul
8
+ %li{ class: ('active' if %w[volumes].include? controller_name) }
9
+ = link_to ::Spina::Admin::Journal::Volume.model_name.human(count: :many), admin_journal_volumes_path
10
+ %li{ class: ('active' if %w[issues].include? controller_name) }
11
+ = link_to ::Spina::Admin::Journal::Issue.model_name.human(count: :many), admin_journal_issues_path
12
+ %li{ class: ('active' if %w[articles].include? controller_name) }
13
+ = link_to ::Spina::Admin::Journal::Article.model_name.human(count: :many), admin_journal_articles_path
14
+
15
+ %li{ class: ('active' if %w[journals authors institutions].include? controller_name) }
16
+ = link_to edit_admin_journal_journal_path(::Spina::Admin::Journal::Journal.instance.id) do
17
+ = icon 'dots'
18
+ = t 'spina.admin.journal.navigation_title'
19
+ = icon 'caret-right'
20
+
21
+ %ul
22
+ %li{ class: ('active' if %w[journals].include? controller_name) }
23
+ = link_to ::Spina::Admin::Journal::Journal.model_name.human(count: :one), edit_admin_journal_journal_path(::Spina::Admin::Journal::Journal.instance.id)
24
+ %li{ class: ('active' if %w[authors].include? controller_name) }
25
+ = link_to ::Spina::Admin::Journal::Author.model_name.human(count: :many), admin_journal_authors_path
26
+ %li{ class: ('active' if %w[institutions].include? controller_name) }
27
+ = link_to ::Spina::Admin::Journal::Institution.model_name.human(count: :many), admin_journal_institutions_path
@@ -0,0 +1,8 @@
1
+ %tr
2
+ %td= affiliation.name
3
+ %td= affiliation.institution.name
4
+ %td= affiliation.articles.count
5
+ %td.nowrap.align-right
6
+ = link_to edit_admin_journal_author_path(affiliation.author.id), class: 'button button-link' do
7
+ = icon 'pencil-outline'
8
+ = t '.view'
@@ -0,0 +1,3 @@
1
+ %tr
2
+ %td.align-center{ colspan: 1000 }
3
+ %em= message
@@ -0,0 +1,10 @@
1
+ %tr{ data: { id: article.id } }
2
+ %td= article.issue.volume.number
3
+ %td= article.issue.number
4
+ %td.position-display= article.number
5
+ %td= time_tag article.issue.date, format: :iso8601
6
+ %td= article.title
7
+ %td.nowrap.align-right
8
+ = link_to edit_admin_journal_article_path(article.id), class: 'button button-link' do
9
+ = icon 'pencil-outline'
10
+ = t '.view'
@@ -0,0 +1,29 @@
1
+ - if @article.errors.any?
2
+ - content_for :notifications do
3
+ .notification.notification-danger.animated.fadeInRight
4
+ = icon 'exclamation'
5
+ .notification-message
6
+ = t 'spina.notifications.alert'
7
+ %small= @article.errors.full_messages.join('<br />').html_safe
8
+ = link_to '#', data: { close_notification: true } do
9
+ = icon 'cross'
10
+
11
+ = form_for @article, html: { autocomplete: "off" } do |f|
12
+ %header#header
13
+ = render partial: 'spina/admin/shared/breadcrumbs'
14
+
15
+ #header_actions
16
+ %button.button.button-primary{ type: 'submit', style: 'margin-right: 0', data: { disable_with: t('spina.pages.saving') } }
17
+ = icon 'check'
18
+ = t '.save'
19
+
20
+ = link_to t('spina.cancel'), admin_journal_articles_path, class: 'button', style: 'margin-right: 0;'
21
+
22
+ %nav#secondary.tabs
23
+ %ul
24
+ - @tabs.each_with_index do |tab, i|
25
+ %li{ class: ('active' if i == 0) }
26
+ = link_to t(".#{tab}"), "##{tab}"
27
+
28
+ #details.tab-content.active= render 'form_details', f: f
29
+ #authors.tab-content= render 'form_authors', f: f
@@ -0,0 +1,10 @@
1
+ .table-container
2
+ %table.table
3
+ %thead
4
+ %tr
5
+ %th= ::Spina::Admin::Journal::Affiliation.human_attribute_name :name
6
+ %th= ::Spina::Admin::Journal::Affiliation.human_attribute_name :institution
7
+ %th= t '.number_of_articles'
8
+ %th
9
+ %tbody
10
+ = render @article.affiliations.any? ? @article.affiliations : 'empty_list', message: t('.no_authors')