biovision-courses 0.0.180221

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +26 -0
  5. data/app/assets/config/biovision_courses_manifest.js +2 -0
  6. data/app/assets/javascripts/biovision/courses/application.js +13 -0
  7. data/app/assets/stylesheets/biovision/courses/application.css +15 -0
  8. data/app/controllers/admin/course_categories_controller.rb +28 -0
  9. data/app/controllers/admin/course_tags_controller.rb +28 -0
  10. data/app/controllers/admin/courses_controller.rb +29 -0
  11. data/app/controllers/biovision/courses/application_controller.rb +7 -0
  12. data/app/controllers/course_categories_controller.rb +70 -0
  13. data/app/controllers/course_tags_controller.rb +66 -0
  14. data/app/controllers/courses_controller.rb +94 -0
  15. data/app/helpers/biovision/courses/application_helper.rb +6 -0
  16. data/app/helpers/biovision_courses_helper.rb +80 -0
  17. data/app/jobs/biovision/courses/application_job.rb +6 -0
  18. data/app/mailers/biovision/courses/application_mailer.rb +8 -0
  19. data/app/models/application_record.rb +3 -0
  20. data/app/models/biovision/courses/application_record.rb +7 -0
  21. data/app/models/course.rb +109 -0
  22. data/app/models/course_category.rb +137 -0
  23. data/app/models/course_course_tag.rb +13 -0
  24. data/app/models/course_tag.rb +49 -0
  25. data/app/uploaders/course_image_uploader.rb +46 -0
  26. data/app/views/admin/course_categories/_nav_item.html.erb +2 -0
  27. data/app/views/admin/course_categories/entity/_in_list.html.erb +43 -0
  28. data/app/views/admin/course_categories/index.html.erb +17 -0
  29. data/app/views/admin/course_categories/show.html.erb +85 -0
  30. data/app/views/admin/course_tags/_nav_item.html.erb +2 -0
  31. data/app/views/admin/course_tags/entity/_in_list.html.erb +21 -0
  32. data/app/views/admin/course_tags/index.html.erb +19 -0
  33. data/app/views/admin/course_tags/show.html.erb +39 -0
  34. data/app/views/admin/courses/_nav_item.html.erb +2 -0
  35. data/app/views/admin/courses/entity/_in_list.html.erb +40 -0
  36. data/app/views/admin/courses/index.html.erb +17 -0
  37. data/app/views/admin/courses/show.html.erb +117 -0
  38. data/app/views/admin/index/dashboard/_biovision_courses.html.erb +12 -0
  39. data/app/views/course_categories/_form.html.erb +96 -0
  40. data/app/views/course_categories/edit.html.erb +22 -0
  41. data/app/views/course_categories/edit.js.erb +1 -0
  42. data/app/views/course_categories/new.html.erb +15 -0
  43. data/app/views/course_categories/new.js.erb +1 -0
  44. data/app/views/course_tags/_form.html.erb +72 -0
  45. data/app/views/course_tags/edit.html.erb +17 -0
  46. data/app/views/course_tags/edit.js.erb +1 -0
  47. data/app/views/course_tags/new.html.erb +15 -0
  48. data/app/views/course_tags/new.js.erb +1 -0
  49. data/app/views/courses/_form.html.erb +244 -0
  50. data/app/views/courses/edit.html.erb +17 -0
  51. data/app/views/courses/edit.js.erb +1 -0
  52. data/app/views/courses/form/_wysiwyg.html.erb +14 -0
  53. data/app/views/courses/new.html.erb +15 -0
  54. data/app/views/courses/new.js.erb +1 -0
  55. data/app/views/layouts/biovision/courses/application.html.erb +14 -0
  56. data/config/locales/courses-ru.yml +138 -0
  57. data/config/routes.rb +26 -0
  58. data/db/migrate/20180218120000_prepare_course_privileges.rb +19 -0
  59. data/db/migrate/20180218120005_create_course_categories.rb +28 -0
  60. data/db/migrate/20180218120010_create_course_tags.rb +20 -0
  61. data/db/migrate/20180218120015_create_courses.rb +42 -0
  62. data/db/migrate/20180218120020_create_course_course_tags.rb +17 -0
  63. data/lib/biovision/courses.rb +7 -0
  64. data/lib/biovision/courses/engine.rb +18 -0
  65. data/lib/biovision/courses/version.rb +5 -0
  66. data/lib/tasks/biovision/courses_tasks.rake +4 -0
  67. metadata +207 -0
@@ -0,0 +1,6 @@
1
+ module Biovision
2
+ module Courses
3
+ module ApplicationHelper
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,80 @@
1
+ module BiovisionCoursesHelper
2
+ # @param [CourseTag] entity
3
+ # @param [String] text
4
+ def admin_course_tag_link(entity, text = entity.name)
5
+ link_to(text, admin_course_tag_path(entity.id))
6
+ end
7
+
8
+ # @param [CourseCategory] entity
9
+ # @param [String] text
10
+ def admin_course_category_link(entity, text = entity.name)
11
+ link_to(text, admin_course_category_path(entity.id))
12
+ end
13
+
14
+ # @param [Course] entity
15
+ # @param [String] text
16
+ def admin_course_link(entity, text = entity.title)
17
+ link_to(text, admin_course_path(entity.id))
18
+ end
19
+
20
+ def course_categories_for_select
21
+ options = []
22
+ CourseCategory.for_tree.each do |category|
23
+ options << [category.name, category.id]
24
+ if category.child_categories.any?
25
+ CourseCategory.for_tree(category.id).each do |subcategory|
26
+ options << ["-#{subcategory.name}", subcategory.id]
27
+ if subcategory.child_categories.any?
28
+ CourseCategory.for_tree(subcategory.id).each do |deep_category|
29
+ options << ["--#{deep_category.name}", deep_category.id]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ options
36
+ end
37
+
38
+ # @param [Course] entity
39
+ # @param [String] text
40
+ # @param [Hash] options
41
+ def course_link(entity, text = entity.title, options = {})
42
+ link_to(text, course_path(entity.id), options)
43
+ end
44
+
45
+ # Course image preview for displaying in "administrative" lists
46
+ #
47
+ # @param [Course] entity
48
+ def course_image_preview(entity)
49
+ return '' if entity.image.blank?
50
+
51
+ versions = "#{entity.image.preview_2x.url} 2x"
52
+ image_tag(entity.image.preview.url, alt: entity.title, srcset: versions)
53
+ end
54
+
55
+ # Small course image for displaying in lists of courses and feeds
56
+ #
57
+ # @param [Course] entity
58
+ # @param [Hash] add_options
59
+ def course_image_small(entity, add_options = {})
60
+ return '' if entity.image.blank?
61
+
62
+ alt_text = entity.image_alt_text.to_s
63
+ versions = "#{entity.image.medium.url} 2x"
64
+ options = { alt: alt_text, srcset: versions }.merge(add_options)
65
+ image_tag(entity.image.small.url, options)
66
+ end
67
+
68
+ # Medium course image for displaying on course page
69
+ #
70
+ # @param [Course] entity
71
+ # @param [Hash] add_options
72
+ def course_image_medium(entity, add_options = {})
73
+ return '' if entity.image.blank?
74
+
75
+ alt_text = entity.image_alt_text.to_s
76
+ versions = "#{entity.image.big.url} 2x"
77
+ options = { alt: alt_text, srcset: versions }.merge(add_options)
78
+ image_tag(entity.image.medium.url, options)
79
+ end
80
+ end
@@ -0,0 +1,6 @@
1
+ module Biovision
2
+ module Courses
3
+ class ApplicationJob < ActiveJob::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module Biovision
2
+ module Courses
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: 'from@example.com'
5
+ layout 'mailer'
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,7 @@
1
+ module Biovision
2
+ module Courses
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,109 @@
1
+ class Course < ApplicationRecord
2
+ include HasOwner
3
+ include Toggleable
4
+
5
+ PER_PAGE = 24
6
+
7
+ DESCRIPTION_LIMIT = 50000
8
+ DURATION_LIMIT = 50
9
+ LEAD_LIMIT = 1000
10
+ META_LIMIT = 255
11
+ PRIORITY_RANGE = (1..999)
12
+ SLUG_LIMIT = 250
13
+ SLUG_PATTERN = /\A[a-z][-0-9a-z]*[0-9a-z]\z/i
14
+ SLUG_PATTERN_HTML = '^[a-zA-Z][-0-9a-zA-Z]*[0-9a-zA-Z]$'
15
+ TITLE_LIMIT = 250
16
+
17
+ toggleable :visible, :highlight, :online
18
+
19
+ mount_uploader :image, CourseImageUploader
20
+
21
+ belongs_to :course_category, counter_cache: true
22
+ belongs_to :user, optional: true
23
+ belongs_to :agent, optional: true
24
+ has_many :course_course_tags, dependent: :destroy
25
+ has_many :course_tags, through: :course_course_tags
26
+
27
+ after_initialize :set_next_priority
28
+ before_validation { self.slug = Canonizer.transliterate(title.to_s) if slug.blank? }
29
+ before_validation { self.slug = slug.to_s.downcase }
30
+ before_validation :normalize_priority
31
+
32
+ validates_presence_of :title, :slug
33
+ validates_uniqueness_of :title
34
+ validates_uniqueness_of :slug
35
+ validates_length_of :title, maximum: TITLE_LIMIT
36
+ validates_length_of :subtitle, maximum: TITLE_LIMIT
37
+ validates_length_of :slug, maximum: SLUG_LIMIT
38
+ validates_format_of :slug, with: SLUG_PATTERN
39
+ validates_length_of :image_alt_text, maximum: META_LIMIT
40
+ validates_length_of :meta_title, maximum: META_LIMIT
41
+ validates_length_of :meta_keywords, maximum: META_LIMIT
42
+ validates_length_of :meta_description, maximum: META_LIMIT
43
+ validates_length_of :lead, maximum: LEAD_LIMIT
44
+ validates_length_of :description, maximum: DESCRIPTION_LIMIT
45
+ validates_length_of :duration, maximum: DURATION_LIMIT
46
+ validates_numericality_of :price, greater_than_or_equal_to: 0, allow_blank: true
47
+ validates_numericality_of :special_price, greater_than_or_equal_to: 0, allow_blank: true
48
+
49
+ scope :ordered_by_priority, -> { order 'priority asc, title asc' }
50
+ scope :visible, -> { where(visible: true, deleted: false) }
51
+ scope :for_tree, ->(course_category_id = nil) { siblings(course_category_id).ordered_by_priority }
52
+ scope :siblings, ->(course_category_id) { where(course_category_id: course_category_id) }
53
+ scope :list_for_visitors, -> { visible.ordered_by_priority }
54
+
55
+ # @param [Integer] page
56
+ def self.page_for_administration(page = 1)
57
+ ordered_by_priority.page(page).per(PER_PAGE)
58
+ end
59
+
60
+ # @param [Integer] page
61
+ def self.page_for_visitors(page = 1)
62
+ list_for_visitors.page(page).per(PER_PAGE)
63
+ end
64
+
65
+ def self.entity_parameters
66
+ texts = %i(title subtitle slug lead description duration)
67
+ decoration = %i(price special_price special_price_end image image_alt_text)
68
+ meta_texts = %i(meta_title meta_keywords meta_description metadata)
69
+ meta_data = %i(course_category_id priority visible highlight online)
70
+
71
+ texts + decoration + meta_texts + meta_data
72
+ end
73
+
74
+ # @param [User] user
75
+ def editable_by?(user)
76
+ owned_by?(user) || UserPrivilege.user_has_privilege?(user, :chief_course_manager)
77
+ end
78
+
79
+ # @param [User] user
80
+ def lockable_by?(user)
81
+ UserPrivilege.user_has_privilege?(user, :chief_course_manager)
82
+ end
83
+
84
+ # @param [Integer] delta
85
+ def change_priority(delta)
86
+ new_priority = priority + delta
87
+ criteria = { course_category_id: course_category_id, priority: new_priority }
88
+ adjacent = self.class.find_by(criteria)
89
+ if adjacent.is_a?(self.class) && (adjacent.id != id)
90
+ adjacent.update!(priority: priority)
91
+ end
92
+ self.update(priority: new_priority)
93
+
94
+ self.class.for_tree(course_category_id).map { |e| [e.id, e.priority] }.to_h
95
+ end
96
+
97
+ private
98
+
99
+ def set_next_priority
100
+ if id.nil? && priority == 1
101
+ self.priority = self.class.siblings(course_category_id).maximum(:priority).to_i + 1
102
+ end
103
+ end
104
+
105
+ def normalize_priority
106
+ self.priority = PRIORITY_RANGE.first if priority < PRIORITY_RANGE.first
107
+ self.priority = PRIORITY_RANGE.last if priority > PRIORITY_RANGE.last
108
+ end
109
+ end
@@ -0,0 +1,137 @@
1
+ class CourseCategory < ApplicationRecord
2
+ include Toggleable
3
+
4
+ PRIORITY_RANGE = (1..500)
5
+ NAME_LIMIT = 50
6
+ SLUG_LIMIT = 50
7
+ SLUG_PATTERN = /\A[a-z][-0-9a-z]*[0-9a-z]\z/i
8
+ SLUG_PATTERN_HTML = '^[a-zA-Z][-0-9a-zA-Z]*[0-9a-zA-Z]$'
9
+
10
+ toggleable :visible
11
+
12
+ mount_uploader :image, CourseImageUploader
13
+
14
+ belongs_to :parent, class_name: CourseCategory.to_s, optional: true, touch: true
15
+ has_many :child_categories, class_name: CourseCategory.to_s, foreign_key: :parent_id, dependent: :destroy
16
+ has_many :courses, dependent: :destroy
17
+
18
+ after_initialize :set_next_priority
19
+ before_validation { self.slug = Canonizer.transliterate(name.to_s) if slug.blank? }
20
+ before_validation { self.slug = slug.to_s.downcase }
21
+ before_validation :generate_long_slug
22
+ before_validation :normalize_priority
23
+ before_save { self.children_cache.uniq! }
24
+ after_create :cache_parents!
25
+ after_save { parent.cache_children! unless parent.nil? }
26
+
27
+ validates_presence_of :name, :slug
28
+ validates_uniqueness_of :name, scope: [:parent_id]
29
+ validates_uniqueness_of :slug, scope: [:parent_id]
30
+ validates_length_of :name, maximum: NAME_LIMIT
31
+ validates_length_of :slug, maximum: SLUG_LIMIT
32
+ validates_format_of :slug, with: SLUG_PATTERN
33
+
34
+ scope :ordered_by_priority, -> { order 'priority asc, name asc' }
35
+ scope :visible, -> { where(visible: true, deleted: false) }
36
+ scope :for_tree, ->(parent_id = nil) { siblings(parent_id).ordered_by_priority }
37
+ scope :siblings, ->(parent_id) { where(parent_id: parent_id) }
38
+
39
+ def self.entity_parameters
40
+ %i(image name slug priority visible)
41
+ end
42
+
43
+ def self.creation_parameters
44
+ entity_parameters + %i(parent_id)
45
+ end
46
+
47
+ def full_title
48
+ (parents.map { |parent| parent.name } + [name]).join ' / '
49
+ end
50
+
51
+ def depth
52
+ parent_ids.count
53
+ end
54
+
55
+ def parent_ids
56
+ parents_cache.split(',').compact
57
+ end
58
+
59
+ # @return [Array<Integer>]
60
+ def branch_ids
61
+ parents_cache.split(',').map(&:to_i).reject { |i| i < 1 }.uniq + [id]
62
+ end
63
+
64
+ # @return [Array<Integer>]
65
+ def subbranch_ids
66
+ [id] + children_cache
67
+ end
68
+
69
+ def image_alt_text
70
+ name
71
+ end
72
+
73
+ def title
74
+ name
75
+ end
76
+
77
+ def parents
78
+ return [] if parents_cache.blank?
79
+ CourseCategory.where(id: parent_ids).order('id asc')
80
+ end
81
+
82
+ def cache_parents!
83
+ return if parent.nil?
84
+ self.parents_cache = "#{parent.parents_cache},#{parent_id}".gsub(/\A,/, '')
85
+ save!
86
+ end
87
+
88
+ def cache_children!
89
+ child_categories.order('id asc').each do |child|
90
+ self.children_cache += [child.id] + child.children_cache
91
+ end
92
+ save!
93
+ end
94
+
95
+ def can_be_deleted?
96
+ child_categories.count < 1
97
+ end
98
+
99
+ # @param [Course] course
100
+ def course?(course)
101
+ course.course_category == self
102
+ end
103
+
104
+ # @param [Integer] delta
105
+ def change_priority(delta)
106
+ new_priority = priority + delta
107
+ criteria = { parent_id: parent_id, priority: new_priority }
108
+ adjacent = self.class.find_by(criteria)
109
+ if adjacent.is_a?(self.class) && (adjacent.id != id)
110
+ adjacent.update!(priority: priority)
111
+ end
112
+ self.update(priority: new_priority)
113
+
114
+ self.class.for_tree(parent_id).map { |e| [e.id, e.priority] }.to_h
115
+ end
116
+
117
+ private
118
+
119
+ def set_next_priority
120
+ if id.nil? && priority == 1
121
+ self.priority = self.class.siblings(parent_id).maximum(:priority).to_i + 1
122
+ end
123
+ end
124
+
125
+ def normalize_priority
126
+ self.priority = PRIORITY_RANGE.first if priority < PRIORITY_RANGE.first
127
+ self.priority = PRIORITY_RANGE.last if priority > PRIORITY_RANGE.last
128
+ end
129
+
130
+ def generate_long_slug
131
+ if parent.nil?
132
+ self.long_slug = slug
133
+ else
134
+ self.long_slug = "#{parent.long_slug}_#{slug}"
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,13 @@
1
+ class CourseCourseTag < ApplicationRecord
2
+ after_create :update_course_tags_cache
3
+ after_destroy :update_course_tags_cache
4
+
5
+ belongs_to :course
6
+ belongs_to :course_tag, counter_cache: :courses_count
7
+
8
+ private
9
+
10
+ def update_course_tags_cache
11
+ course.update tags_cache: course.course_tags.ordered_by_name.map(&:name)
12
+ end
13
+ end
@@ -0,0 +1,49 @@
1
+ class CourseTag < ApplicationRecord
2
+ include RequiredUniqueName
3
+ include RequiredUniqueSlug
4
+ include Toggleable
5
+
6
+ PER_PAGE = 20
7
+ NAME_LIMIT = 50
8
+ SLUG_LIMIT = 50
9
+ SLUG_PATTERN = /\A[a-z][-0-9a-z]*[0-9a-z]\z/i
10
+ SLUG_PATTERN_HTML = '^[a-zA-Z][-0-9a-zA-Z]*[0-9a-zA-Z]$'
11
+
12
+ toggleable :visible
13
+
14
+ mount_uploader :image, CourseImageUploader
15
+
16
+ has_many :course_course_tags, dependent: :destroy
17
+ has_many :courses, through: :course_course_tags
18
+
19
+ before_validation { self.slug = Canonizer.transliterate(name.to_s) if slug.blank? }
20
+ before_validation { self.slug = slug.to_s.downcase }
21
+ validates_length_of :name, maximum: NAME_LIMIT
22
+ validates_length_of :slug, maximum: SLUG_LIMIT
23
+ validates_format_of :slug, with: SLUG_PATTERN
24
+
25
+ scope :visible, -> { where(visible: true) }
26
+ scope :list_for_visitors, -> { visible.ordered_by_name }
27
+
28
+ # @param [Integer] page
29
+ def self.page_for_administration(page = 1)
30
+ ordered_by_name.page(page).per(PER_PAGE)
31
+ end
32
+
33
+ # @param [Integer] page
34
+ def self.page_for_visitors(page = 1)
35
+ list_for_visitors.page(page).per(PER_PAGE)
36
+ end
37
+
38
+ def self.entity_parameters
39
+ %i(visible image name slug)
40
+ end
41
+
42
+ def image_alt_text
43
+ name
44
+ end
45
+
46
+ def title
47
+ name
48
+ end
49
+ end
@@ -0,0 +1,46 @@
1
+ class CourseImageUploader < CarrierWave::Uploader::Base
2
+ include CarrierWave::MiniMagick
3
+ include CarrierWave::BombShelter
4
+
5
+ storage :file
6
+
7
+ def max_pixel_dimensions
8
+ [3840, 3840]
9
+ end
10
+
11
+ def store_dir
12
+ "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id/10000.floor}/#{model.id/100.floor}/#{model.id}"
13
+ end
14
+
15
+ process :auto_orient
16
+
17
+ def auto_orient
18
+ manipulate! do |image|
19
+ image.tap(&:auto_orient)
20
+ end
21
+ end
22
+
23
+ version :big do
24
+ resize_to_fit(1280, 1280)
25
+ end
26
+
27
+ version :medium, from_version: :big do
28
+ resize_to_fit(640, 640)
29
+ end
30
+
31
+ version :small, from_version: :medium do
32
+ resize_to_fit(320, 320)
33
+ end
34
+
35
+ version :preview_2x, from_version: :small do
36
+ resize_to_fit(160, 160)
37
+ end
38
+
39
+ version :preview, from_version: :preview_2x do
40
+ resize_to_fit(80, 80)
41
+ end
42
+
43
+ def extension_white_list
44
+ %w(jpg jpeg png)
45
+ end
46
+ end