biovision-base 0.34.190331.1 → 0.36.190526.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/biovision/base/biovision.js +52 -7
  3. data/app/assets/javascripts/biovision/base/components/carousel.js +127 -17
  4. data/app/assets/javascripts/biovision/base/components/video-stretcher.js +51 -0
  5. data/app/assets/stylesheets/biovision/base/biovision.scss +18 -2
  6. data/app/controllers/admin/settings_controller.rb +7 -12
  7. data/app/controllers/admin_controller.rb +4 -0
  8. data/app/controllers/concerns/authentication.rb +3 -0
  9. data/app/controllers/editable_pages_controller.rb +2 -0
  10. data/app/controllers/my/profiles_controller.rb +1 -1
  11. data/app/helpers/biovision_users_helper.rb +1 -1
  12. data/app/jobs/editable_page_body_parser_job.rb +16 -0
  13. data/app/models/biovision_component.rb +18 -11
  14. data/app/models/editable_page.rb +21 -16
  15. data/app/models/foreign_site.rb +9 -3
  16. data/app/services/biovision/components/base_component.rb +17 -40
  17. data/app/services/canonizer.rb +21 -14
  18. data/app/services/oembed_receiver.rb +92 -0
  19. data/app/services/user_bouncer.rb +3 -4
  20. data/app/uploaders/simple_file_uploader.rb +15 -0
  21. data/app/views/admin/editable_pages/entity/_in_list.html.erb +1 -1
  22. data/app/views/admin/editable_pages/show.html.erb +2 -2
  23. data/app/views/admin/index/_biovision_base.html.erb +2 -2
  24. data/app/views/admin/settings/component/_new_parameter.html.erb +5 -13
  25. data/app/views/admin/settings/component/_parameters.html.erb +6 -6
  26. data/app/views/admin/settings/show.html.erb +1 -1
  27. data/app/views/admin/users/entity/_in_list.html.erb +3 -0
  28. data/app/views/admin/users/show.html.erb +1 -1
  29. data/app/views/editable_pages/_editable_page.html.erb +1 -1
  30. data/app/views/editable_pages/_form.html.erb +2 -2
  31. data/app/views/editable_pages/entity/_content.html.erb +1 -1
  32. data/app/views/editable_pages/entity/_metadata.html.erb +12 -10
  33. data/app/views/index/index/_default_dashboard.html.erb +3 -1
  34. data/app/views/my/index/index/_dashboard.html.erb +16 -0
  35. data/app/views/shared/_breadcrumbs.html.erb +4 -2
  36. data/app/views/shared/_track.html.erb +8 -8
  37. data/app/views/shared/admin/_breadcrumbs.html.erb +4 -2
  38. data/app/views/shared/editable_pages/_body.html.erb +2 -2
  39. data/app/views/shared/entity/_metadata.html.erb +13 -0
  40. data/app/views/shared/forms/_toggle_wysiwyg.html.erb +24 -0
  41. data/app/views/shared/forms/_wysiwyg.html.erb +1 -1
  42. data/app/views/simple_blocks/_form.html.erb +12 -0
  43. data/app/views/users/_form.html.erb +22 -22
  44. data/config/locales/components-ru.yml +2 -3
  45. data/config/routes.rb +41 -90
  46. data/db/migrate/20181217000000_create_biovision_components.rb +16 -39
  47. data/db/migrate/20181217000110_create_editable_pages.rb +11 -40
  48. data/db/migrate/20190326120000_create_simple_blocks.rb +7 -1
  49. data/db/migrate/20190423101010_add_parameters_to_biovision_components.rb +29 -0
  50. data/db/migrate/20190429111111_add_parsed_body_to_editable_pages.rb +18 -0
  51. data/lib/biovision/base/base_methods.rb +10 -0
  52. data/lib/biovision/base/privilege_methods.rb +5 -0
  53. data/lib/biovision/base/version.rb +1 -1
  54. metadata +9 -3
  55. data/app/views/layouts/profile.html.erb +0 -30
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Common administrative controller
1
4
  class AdminController < ApplicationController
2
5
  before_action :restrict_access
3
6
 
@@ -5,6 +8,7 @@ class AdminController < ApplicationController
5
8
 
6
9
  def restrict_access
7
10
  return if UserPrivilege.user_has_any_privilege?(current_user)
11
+
8
12
  handle_http_401("User #{current_user&.id} has no privileges")
9
13
  end
10
14
  end
@@ -10,6 +10,9 @@ module Authentication
10
10
 
11
11
  # @param [User] user
12
12
  def create_token_for_user(user)
13
+ forced_user = User.find_by(id: user.native_id)
14
+ user = forced_user unless forced_user.nil?
15
+
13
16
  token = user.tokens.create!(tracking_for_entity)
14
17
 
15
18
  cookies['token'] = {
@@ -20,6 +20,7 @@ class EditablePagesController < AdminController
20
20
  def create
21
21
  @entity = EditablePage.new(entity_parameters)
22
22
  if @entity.save
23
+ EditablePageBodyParserJob.perform_later(@entity.id)
23
24
  form_processed_ok(admin_editable_page_path(id: @entity.id))
24
25
  else
25
26
  form_processed_with_error(:new)
@@ -33,6 +34,7 @@ class EditablePagesController < AdminController
33
34
  # patch /editable_pages/:id
34
35
  def update
35
36
  if @entity.update(entity_parameters)
37
+ EditablePageBodyParserJob.perform_later(@entity.id)
36
38
  form_processed_ok(admin_editable_page_path(id: @entity.id))
37
39
  else
38
40
  form_processed_with_error(:edit)
@@ -8,7 +8,7 @@ class My::ProfilesController < ApplicationController
8
8
  before_action :restrict_anonymous_access, except: %i[check new create]
9
9
  before_action :set_handler
10
10
 
11
- layout 'profile', only: %i[show edit]
11
+ # layout 'profile', only: %i[show edit]
12
12
 
13
13
  # post /my/profile/check
14
14
  def check
@@ -24,7 +24,7 @@ module BiovisionUsersHelper
24
24
  return I18n.t(:anonymous) if entity.nil? || entity.deleted?
25
25
 
26
26
  link_options = { class: 'profile' }.merge(options)
27
- link_to(text, user_profile_path(slug: entity.screen_name), link_options)
27
+ link_to(text, user_profile_path(slug: entity.slug), link_options)
28
28
  end
29
29
 
30
30
  # @param [User] entity
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Parse editable page body
4
+ class EditablePageBodyParserJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ # @param [Integer] id
8
+ def perform(id)
9
+ entity = EditablePage.find_by(id: id)
10
+
11
+ return if entity.nil?
12
+
13
+ entity.parsed_body = OembedReceiver.convert(entity.body)
14
+ entity.save
15
+ end
16
+ end
@@ -10,7 +10,10 @@
10
10
  class BiovisionComponent < ApplicationRecord
11
11
  include RequiredUniqueSlug
12
12
 
13
- has_many :biovision_parameters, dependent: :delete_all
13
+ SLUG_LIMIT = 250
14
+ SLUG_PATTERN = /\A[a-z][-a-z0-9_]+[a-z0-9]\z/i.freeze
15
+ SLUG_PATTERN_HTML = '^[a-zA-Z][-a-zA-Z0-9_]+[a-zA-Z0-9]$'
16
+ VALUE_LIMIT = 65_535
14
17
 
15
18
  # Find component by slug
16
19
  #
@@ -20,24 +23,28 @@ class BiovisionComponent < ApplicationRecord
20
23
  end
21
24
 
22
25
  # @param [String] slug
26
+ # @param [String] default_value
27
+ def get(slug, default_value = '')
28
+ parameters.fetch(slug.to_s) { default_value }
29
+ end
30
+
31
+ # @param [String] slug
32
+ # @deprecated use #get
23
33
  def receive(slug)
24
- biovision_parameters.find_by(slug: slug)&.value
34
+ parameters[slug.to_s]
25
35
  end
26
36
 
27
37
  # @param [String] slug
28
- # @param [String] default
29
- def receive!(slug, default = '')
30
- biovision_parameters.find_by(slug: slug)&.value || default
38
+ # @param [String] default_value
39
+ # @deprecated use #get
40
+ def receive!(slug, default_value = '')
41
+ get(slug, default_value)
31
42
  end
32
43
 
33
44
  # @param [String] slug
34
45
  # @param [String] value
35
46
  def []=(slug, value)
36
- parameter = biovision_parameters.find_by(slug: slug)
37
- if parameter.nil?
38
- biovision_parameters.create!(slug: slug, value: value.to_s)
39
- else
40
- parameter.update!(value: value.to_s)
41
- end
47
+ parameters[slug.to_s] = value.to_s
48
+ save!
42
49
  end
43
50
  end
@@ -3,19 +3,20 @@
3
3
  # Editable page for site
4
4
  #
5
5
  # Attributes:
6
- # - body [String]
7
- # - image [EditablePageImageUploader], optional
8
- # - image_alt_text [String], optional
9
- # - language_id [Language]
10
- # - meta_description [String], optional
11
- # - meta_keywords [String], optional
12
- # - meta_title [String], optional
13
- # - name [String]
14
- # - nav_group [String], optional
15
- # - priority [Integer]
16
- # - slug [String]
17
- # - url [String], optional
18
- # - visible [Boolean]
6
+ # body [String]
7
+ # image [EditablePageImageUploader], optional
8
+ # image_alt_text [String], optional
9
+ # language_id [Language]
10
+ # meta_description [String], optional
11
+ # meta_keywords [String], optional
12
+ # meta_title [String], optional
13
+ # name [String]
14
+ # nav_group [String], optional
15
+ # parsed_body [Text]
16
+ # priority [Integer]
17
+ # slug [String]
18
+ # url [String], optional
19
+ # visible [Boolean]
19
20
  class EditablePage < ApplicationRecord
20
21
  include RequiredUniqueName
21
22
  include FlatPriority
@@ -23,21 +24,21 @@ class EditablePage < ApplicationRecord
23
24
  include Checkable
24
25
  include Toggleable
25
26
 
26
- BODY_LIMIT = 65_535
27
+ BODY_LIMIT = 16_777_215
27
28
  META_LIMIT = 255
28
29
  NAME_LIMIT = 100
29
30
  SLUG_LIMIT = 100
30
31
 
31
32
  toggleable :visible
32
33
 
33
- mount_uploader :image, EditablePageImageUploader
34
+ mount_uploader :image, SimpleImageUploader
34
35
 
35
36
  belongs_to :language, optional: true
36
37
 
37
38
  before_validation { self.slug = slug.strip unless slug.nil? }
38
39
 
39
40
  validates_presence_of :slug
40
- validates_uniqueness_of :slug, scope: [:language_id]
41
+ validates_uniqueness_of :slug, scope: :language_id
41
42
  validates_length_of :body, maximum: BODY_LIMIT
42
43
  validates_length_of :image_alt_text, maximum: META_LIMIT
43
44
  validates_length_of :name, maximum: NAME_LIMIT
@@ -89,6 +90,10 @@ class EditablePage < ApplicationRecord
89
90
  name
90
91
  end
91
92
 
93
+ def text
94
+ parsed_body.blank? ? body : parsed_body
95
+ end
96
+
92
97
  # @deprecated use #meta_keywords
93
98
  def keywords
94
99
  meta_keywords
@@ -1,3 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Model for foreign site for external authentication
4
+ #
5
+ # Attributes:
6
+ # foreign_users_count [Integer]
7
+ # name [String]
8
+ # slug [String]
1
9
  class ForeignSite < ApplicationRecord
2
10
  include RequiredUniqueName
3
11
  include RequiredUniqueSlug
@@ -10,9 +18,7 @@ class ForeignSite < ApplicationRecord
10
18
  validates_length_of :name, maximum: NAME_LIMIT
11
19
  validates_length_of :slug, maximum: SLUG_LIMIT
12
20
 
13
- def self.page_for_administration
14
- ordered_by_name
15
- end
21
+ scope :list_for_administration, -> { ordered_by_name }
16
22
 
17
23
  # @param [Hash] data
18
24
  # @param [Hash] tracking
@@ -26,51 +26,28 @@ module Biovision
26
26
  handler_class.new(entity)
27
27
  end
28
28
 
29
- # @param [Hash] data
30
- def settings=(data)
31
- @component.settings.merge!(normalize_settings(data))
32
- @component.save!
29
+ def self.default_privilege_name
30
+ self.class.to_s.demodulize.underscore.gsub('component', 'manager')
33
31
  end
34
32
 
35
- def settings
36
- @component.settings
37
- end
33
+ # @param [User] user
34
+ # @param [Hash] options
35
+ def self.allow?(user, options = {})
36
+ return false if user.nil?
38
37
 
39
- def parameters
40
- @component.biovision_parameters.list_for_administration
41
- end
38
+ privilege = options[:privilege] || default_privilege_name
42
39
 
43
- # Get instance of BiovisionParameter with given slug
44
- #
45
- # @param [String] slug
46
- def parameter(slug)
47
- @component.biovision_parameters.find_by(slug: slug)
40
+ UserPrivilege.user_has_privilege?(user, privilege)
48
41
  end
49
42
 
50
- # Create or update parameter values
51
- #
52
- # @param [String] slug
53
- # @param [String] value
54
- # @param [String] name
55
- # @param [String] description
56
- def set_parameter(slug, value, name = '', description = '')
57
- @component[slug] = value
58
-
59
- item = parameter(slug)
60
- item.update(name: name, description: description) if item.deletable?
61
-
62
- item
43
+ # @param [Hash] data
44
+ def settings=(data)
45
+ @component.settings.merge!(normalize_settings(data))
46
+ @component.save!
63
47
  end
64
48
 
65
- # Delete parameter with given slug (if it is deletable)
66
- #
67
- # @param [String] slug
68
- def delete_parameter(slug)
69
- item = parameter(slug)
70
-
71
- return unless item&.deletable?
72
-
73
- item.destroy
49
+ def settings
50
+ @component.settings
74
51
  end
75
52
 
76
53
  # Receive parameter value with default
@@ -82,7 +59,7 @@ module Biovision
82
59
  # @param [String] default
83
60
  # @return [String]
84
61
  def receive(key, default = '')
85
- @component.receive!(key, default)
62
+ @component.get(key, default)
86
63
  end
87
64
 
88
65
  # Receive parameter value or nil
@@ -90,9 +67,9 @@ module Biovision
90
67
  # Returns value of component's parameter of nil when it's not found
91
68
  #
92
69
  # @param [String] key
93
- # @return [String|nil]
70
+ # @return [String]
94
71
  def [](key)
95
- @component.receive(key)
72
+ @component.get(key)
96
73
  end
97
74
 
98
75
  # Set parameter
@@ -1,31 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helper for canonizing and transliterating strings
1
4
  class Canonizer
5
+ # Keys are not latin letters
2
6
  TRANSLITERATION_MAP = {
3
- 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e',
4
- 'ё' => 'yo', 'ж' => 'zh', 'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k',
5
- 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r',
6
- 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'kh', 'ц' => 'c',
7
- 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'shh', 'ъ' => '', 'ы' => 'y', 'ь' => '',
8
- 'э' => 'e', 'ю' => 'yu', 'я' => 'ya',
9
- 'å' => 'ao', 'ä' => 'ae', 'ö' => 'oe', 'é' => 'e'
10
- }
7
+ 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e',
8
+ 'ё' => 'yo', 'ж' => 'zh', 'з' => 'z', 'и' => 'i', 'й' => 'j', 'к' => 'k',
9
+ 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r',
10
+ 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'kh', 'ц' => 'c',
11
+ 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'shh', 'ъ' => '', 'ы' => 'y', 'ь' => '',
12
+ 'э' => 'e', 'ю' => 'yu', 'я' => 'ya',
13
+ 'å' => 'ao', 'ä' => 'ae', 'ö' => 'oe', 'é' => 'e'
14
+ }.freeze
11
15
 
12
16
  # @param [String] text
13
17
  def self.transliterate(text)
14
- result = text.downcase
15
- TRANSLITERATION_MAP.each { |r, e| result.gsub!(r, e) }
16
- result.downcase.gsub(/[^-a-z0-9_]/, '-').gsub(/^[-_]*([-a-z0-9_]*[a-z0-9]+)[-_]*$/, '\1').gsub(/--+/, '-')
18
+ pattern = Regexp.new "[#{TRANSLITERATION_MAP.keys.join}]"
19
+ result = text.to_s.downcase.gsub(pattern, TRANSLITERATION_MAP)
20
+
21
+ a = /[^-a-z0-9_]/ # non-allowed characters will be replaced with dash
22
+ b = /\A[-_]*([-a-z0-9_]*[a-z0-9]+)[-_]*\z/ # chop leading and trailing dash
23
+ result.gsub(a, '-').gsub(b, '\1').gsub(/--+/, '-').gsub(/-+\z/, '')
17
24
  end
18
25
 
19
26
  # @param [String] input
20
27
  def self.canonize(input)
21
- lowered = input.downcase.strip
28
+ lowered = input.to_s.downcase.strip
22
29
  canonized = lowered.gsub(/[^a-zа-я0-9ё]/, '')
23
30
  canonized.empty? ? lowered : canonized
24
31
  end
25
32
 
26
33
  # @param [String] input
27
34
  def self.urlize(input)
28
- lowered = input.downcase.squish
29
- lowered.gsub(/[^a-zа-я0-9ё]/, '-').gsub(/-\z/, '')
35
+ lowered = input.to_s.downcase.squish
36
+ lowered.gsub(/[^a-zа-я0-9ё]/, '-').gsub(/-+\z/, '')
30
37
  end
31
38
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Receiver for OEmbed-wrapped content
4
+ class OembedReceiver
5
+ PATTERN = %r{<oembed url="([^"]+)"></oembed>}.freeze
6
+
7
+ attr_accessor :url
8
+
9
+ # @param [String] url
10
+ def initialize(url = '')
11
+ @url = url
12
+ end
13
+
14
+ # @param [String] text
15
+ def self.convert(text)
16
+ receiver = new
17
+ text.gsub(PATTERN) do |fragment|
18
+ receiver.url = fragment.match(PATTERN)[1]
19
+ receiver.code
20
+ end
21
+ end
22
+
23
+ def code
24
+ @host = URI.parse(@url).host
25
+ receive url_for_host
26
+ end
27
+
28
+ def fallback
29
+ attributes = %(rel="external nofollow noopener noreferrer" target="_blank")
30
+ %(<a href="#{@url}" #{attributes}>#{@host}</a>)
31
+ end
32
+
33
+ private
34
+
35
+ # @param [String] embed_url
36
+ def receive(embed_url)
37
+ response = RestClient.get(embed_url)
38
+ parse(response.body)
39
+ rescue RestClient::Exception => e
40
+ Rails.logger.warn("Cannot receive data for #{embed_url}: #{e}")
41
+ fallback
42
+ end
43
+
44
+ # @param [String] response
45
+ def parse(response)
46
+ json = JSON.parse(response)
47
+ json['html'] || fallback
48
+ rescue JSON::ParserError => e
49
+ Rails.logger.warn("Cannot parse response #{response}: #{e}")
50
+ fallback
51
+ end
52
+
53
+ def url_for_host
54
+ case @host
55
+ when 'www.youtube.com', 'youtube.com', 'youtu.be'
56
+ url_for_youtube
57
+ when 'twitter.com', 'www.twitter.com'
58
+ url_for_twitter
59
+ when 'www.facebook.com', 'facebook.com'
60
+ url_for_facebook
61
+ when 'www.instagram.com', 'instagr.am', 'instagram.com'
62
+ url_for_instagram
63
+ else
64
+ default_url
65
+ end
66
+ end
67
+
68
+ def url_for_youtube
69
+ "https://www.youtube.com/oembed?url=#{CGI.escape(@url)}&format=json"
70
+ end
71
+
72
+ def url_for_twitter
73
+ "https://publish.twitter.com/oembed?url=#{CGI.escape(@url)}"
74
+ end
75
+
76
+ def url_for_facebook
77
+ url = CGI.escape(@url)
78
+ if @url.match?('/videos/')
79
+ "https://www.facebook.com/plugins/video/oembed.json/?url=#{url}"
80
+ else
81
+ "https://www.facebook.com/plugins/post/oembed.json/?url=#{url}"
82
+ end
83
+ end
84
+
85
+ def url_for_instagram
86
+ "https://api.instagram.com/oembed?url=#{CGI.escape(@url)}"
87
+ end
88
+
89
+ def default_url
90
+ "https://#{@host}/oembed?url=#{CGI.escape(@url)}&format=json"
91
+ end
92
+ end