virgo 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +37 -0
- data/app/assets/javascripts/tinymce/manifest.coffee +1 -0
- data/app/assets/javascripts/tinymce/plugins/image_manager/plugin.coffee +234 -0
- data/app/assets/javascripts/virgo/admin/page_modules/edit_form.coffee +4 -0
- data/app/assets/javascripts/virgo/admin/page_modules/edit_subject_form.coffee +29 -0
- data/app/assets/javascripts/virgo/admin/posts/edit_locking.coffee +35 -0
- data/app/assets/javascripts/virgo/admin/posts/form.coffee +183 -0
- data/app/assets/javascripts/virgo/admin/slides/form.coffee +27 -0
- data/app/assets/javascripts/virgo/admin/slideshows/form.coffee +26 -0
- data/app/assets/javascripts/virgo/admin_only.coffee +1 -0
- data/app/assets/javascripts/virgo/application.coffee +51 -0
- data/app/assets/javascripts/virgo/common.coffee +83 -0
- data/app/assets/javascripts/virgo/common/character_counter.coffee +27 -0
- data/app/assets/javascripts/virgo/components/header.coffee +47 -0
- data/app/assets/javascripts/virgo/components/media_modal.coffee +151 -0
- data/app/assets/javascripts/virgo/components/share_bar.coffee +53 -0
- data/app/assets/javascripts/virgo/components/slideshow.coffee +35 -0
- data/app/assets/javascripts/virgo/lib/jquery-ui-multiselect.js +342 -0
- data/app/assets/javascripts/virgo/lib/jquery.browser.js +184 -0
- data/app/assets/javascripts/virgo/lib/jquery.cookie.js +114 -0
- data/app/assets/javascripts/virgo/lib/jquery.pjax.js +932 -0
- data/app/assets/javascripts/virgo/lib/jquery_ext.coffee +0 -0
- data/app/assets/javascripts/virgo/lib/manifest.coffee +5 -0
- data/app/assets/javascripts/virgo/lib/ouibounce.js +194 -0
- data/app/assets/javascripts/virgo/page_modules.coffee +175 -0
- data/app/assets/javascripts/virgo/posts/show.coffee +32 -0
- data/app/assets/stylesheets/tinymce/skin.sass +2 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/content.min.sass +82 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/fonts/tinymce-small.eot +0 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/fonts/tinymce-small.svg +62 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/fonts/tinymce-small.ttf +0 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/fonts/tinymce-small.woff +0 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/fonts/tinymce.eot +0 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/fonts/tinymce.svg +63 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/fonts/tinymce.ttf +0 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/fonts/tinymce.woff +0 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/img/anchor.gif +0 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/img/loader.gif +0 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/img/object.gif +0 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/img/trans.gif +0 -0
- data/app/assets/stylesheets/tinymce/skins/railspress/skin.min.sass +1703 -0
- data/app/assets/stylesheets/virgo/_common.sass +65 -0
- data/app/assets/stylesheets/virgo/admin.sass +301 -0
- data/app/assets/stylesheets/virgo/admin/columns.sass +3 -0
- data/app/assets/stylesheets/virgo/admin/images.sass +14 -0
- data/app/assets/stylesheets/virgo/admin/media_modal.sass +72 -0
- data/app/assets/stylesheets/virgo/admin/post.sass +102 -0
- data/app/assets/stylesheets/virgo/admin/slideshow.sass +35 -0
- data/app/assets/stylesheets/virgo/admin/users.sass +2 -0
- data/app/assets/stylesheets/virgo/admin_only.sass +1 -0
- data/app/assets/stylesheets/virgo/application.sass +605 -0
- data/app/assets/stylesheets/virgo/columns.sass +51 -0
- data/app/assets/stylesheets/virgo/install.sass +15 -0
- data/app/assets/stylesheets/virgo/lib/bootstrap/bootstrap-imports.sass +9 -0
- data/app/assets/stylesheets/virgo/lib/bootstrap/bootstrap-overrides.sass +51 -0
- data/app/assets/stylesheets/virgo/lib/bootstrap/bootstrap-variables.sass +27 -0
- data/app/assets/stylesheets/virgo/lib/jquery-ui-multiselect.css +30 -0
- data/app/assets/stylesheets/virgo/lib/manifest.sass +3 -0
- data/app/assets/stylesheets/virgo/lib/ouibounce.css +222 -0
- data/app/assets/stylesheets/virgo/lib/select2-imports.sass +8 -0
- data/app/assets/stylesheets/virgo/page_modules.sass +54 -0
- data/app/assets/stylesheets/virgo/pages/authors.sass +30 -0
- data/app/assets/stylesheets/virgo/posts.sass +782 -0
- data/app/assets/stylesheets/virgo/search.sass +36 -0
- data/app/assets/stylesheets/virgo/ui.sass +434 -0
- data/app/assets/stylesheets/virgo/users.sass +9 -0
- data/app/controllers/virgo/admin/base_controller.rb +22 -0
- data/app/controllers/virgo/admin/categories_controller.rb +90 -0
- data/app/controllers/virgo/admin/columns_controller.rb +65 -0
- data/app/controllers/virgo/admin/images_controller.rb +89 -0
- data/app/controllers/virgo/admin/media_modal_controller.rb +101 -0
- data/app/controllers/virgo/admin/newsletters_controller.rb +37 -0
- data/app/controllers/virgo/admin/page_modules_controller.rb +62 -0
- data/app/controllers/virgo/admin/pages_controller.rb +6 -0
- data/app/controllers/virgo/admin/posts_controller.rb +173 -0
- data/app/controllers/virgo/admin/sites_controller.rb +31 -0
- data/app/controllers/virgo/admin/slides_controller.rb +71 -0
- data/app/controllers/virgo/admin/slideshows_controller.rb +66 -0
- data/app/controllers/virgo/admin/tags_controller.rb +98 -0
- data/app/controllers/virgo/admin/users/sessions_controller.rb +21 -0
- data/app/controllers/virgo/admin/users_controller.rb +84 -0
- data/app/controllers/virgo/application_controller.rb +151 -0
- data/app/controllers/virgo/categories_controller.rb +20 -0
- data/app/controllers/virgo/columns_controller.rb +24 -0
- data/app/controllers/virgo/images_controller.rb +9 -0
- data/app/controllers/virgo/install_controller.rb +46 -0
- data/app/controllers/virgo/page_modules_controller.rb +23 -0
- data/app/controllers/virgo/pages_controller.rb +29 -0
- data/app/controllers/virgo/posts_controller.rb +101 -0
- data/app/controllers/virgo/search_controller.rb +35 -0
- data/app/controllers/virgo/subscribers_controller.rb +54 -0
- data/app/controllers/virgo/tags_controller.rb +15 -0
- data/app/controllers/virgo/users/confirmations_controller.rb +18 -0
- data/app/controllers/virgo/users/passwords_controller.rb +31 -0
- data/app/controllers/virgo/users/registrations_controller.rb +41 -0
- data/app/controllers/virgo/users/sessions_controller.rb +32 -0
- data/app/controllers/virgo/users_controller.rb +15 -0
- data/app/helpers/virgo/admin/post_helper.rb +54 -0
- data/app/helpers/virgo/admin_helper.rb +15 -0
- data/app/helpers/virgo/application_helper.rb +124 -0
- data/app/helpers/virgo/hooks_helper.rb +30 -0
- data/app/helpers/virgo/page_modules_helper.rb +4 -0
- data/app/helpers/virgo/post_helper.rb +159 -0
- data/app/helpers/virgo/render_helper.rb +33 -0
- data/app/helpers/virgo/search_helper.rb +49 -0
- data/app/helpers/virgo/shortcode_helper.rb +7 -0
- data/app/helpers/virgo/text_helper.rb +12 -0
- data/app/helpers/virgo/video_helper.rb +34 -0
- data/app/models/concerns/virgo/common/slug_history.rb +36 -0
- data/app/models/concerns/virgo/common/uuid.rb +22 -0
- data/app/models/concerns/virgo/post/recommendations.rb +15 -0
- data/app/models/concerns/virgo/post/search.rb +100 -0
- data/app/models/concerns/virgo/user/search.rb +39 -0
- data/app/models/virgo/ability.rb +59 -0
- data/app/models/virgo/category.rb +44 -0
- data/app/models/virgo/column.rb +25 -0
- data/app/models/virgo/form_model.rb +19 -0
- data/app/models/virgo/image.rb +63 -0
- data/app/models/virgo/page_module.rb +42 -0
- data/app/models/virgo/page_module_post.rb +8 -0
- data/app/models/virgo/post.rb +391 -0
- data/app/models/virgo/post_category.rb +8 -0
- data/app/models/virgo/post_search.rb +7 -0
- data/app/models/virgo/post_tag.rb +10 -0
- data/app/models/virgo/site.rb +36 -0
- data/app/models/virgo/slide.rb +23 -0
- data/app/models/virgo/slideshow.rb +37 -0
- data/app/models/virgo/slug_history.rb +5 -0
- data/app/models/virgo/subscriber.rb +5 -0
- data/app/models/virgo/tag.rb +34 -0
- data/app/models/virgo/user.rb +145 -0
- data/app/uploaders/virgo/application_uploader.rb +11 -0
- data/app/uploaders/virgo/avatar_uploader.rb +21 -0
- data/app/uploaders/virgo/featured_image_uploader.rb +28 -0
- data/app/uploaders/virgo/image_uploader.rb +40 -0
- data/app/uploaders/virgo/slide_uploader.rb +19 -0
- data/app/views/layouts/virgo/admin.haml +16 -0
- data/app/views/layouts/virgo/admin/site_settings.haml +14 -0
- data/app/views/layouts/virgo/application.haml +63 -0
- data/app/views/layouts/virgo/errors.haml +42 -0
- data/app/views/layouts/virgo/main.haml +5 -0
- data/app/views/layouts/virgo/posts.haml +10 -0
- data/app/views/virgo/admin/categories/_form.haml +19 -0
- data/app/views/virgo/admin/categories/edit.haml +7 -0
- data/app/views/virgo/admin/categories/index.haml +38 -0
- data/app/views/virgo/admin/categories/modal_form.haml +11 -0
- data/app/views/virgo/admin/categories/new.haml +7 -0
- data/app/views/virgo/admin/categories/success_modal.haml +14 -0
- data/app/views/virgo/admin/columns/_form.haml +30 -0
- data/app/views/virgo/admin/columns/edit.haml +1 -0
- data/app/views/virgo/admin/columns/index.haml +30 -0
- data/app/views/virgo/admin/columns/new.haml +1 -0
- data/app/views/virgo/admin/images/_form.haml +46 -0
- data/app/views/virgo/admin/images/edit.haml +1 -0
- data/app/views/virgo/admin/images/index.haml +32 -0
- data/app/views/virgo/admin/images/new.haml +1 -0
- data/app/views/virgo/admin/media_modal/_image_settings.haml +17 -0
- data/app/views/virgo/admin/media_modal/_library_panel.haml +26 -0
- data/app/views/virgo/admin/media_modal/_upload_panel.haml +21 -0
- data/app/views/virgo/admin/media_modal/_upload_success.haml +7 -0
- data/app/views/virgo/admin/media_modal/index.haml +19 -0
- data/app/views/virgo/admin/newsletters/changelog.haml +25 -0
- data/app/views/virgo/admin/newsletters/edit.haml +6 -0
- data/app/views/virgo/admin/page_modules/edit.haml +33 -0
- data/app/views/virgo/admin/page_modules/edit_subject.haml +43 -0
- data/app/views/virgo/admin/pages/help.haml +114 -0
- data/app/views/virgo/admin/posts/_author_dropdown.haml +1 -0
- data/app/views/virgo/admin/posts/_categories_form.haml +7 -0
- data/app/views/virgo/admin/posts/_category.haml +22 -0
- data/app/views/virgo/admin/posts/_form.haml +201 -0
- data/app/views/virgo/admin/posts/_revisions.haml +40 -0
- data/app/views/virgo/admin/posts/edit.haml +9 -0
- data/app/views/virgo/admin/posts/index.haml +59 -0
- data/app/views/virgo/admin/posts/new.haml +1 -0
- data/app/views/virgo/admin/posts/revision_detail.haml +28 -0
- data/app/views/virgo/admin/posts/revisions.haml +3 -0
- data/app/views/virgo/admin/sites/edit.haml +12 -0
- data/app/views/virgo/admin/slides/_form.haml +41 -0
- data/app/views/virgo/admin/slides/edit.haml +1 -0
- data/app/views/virgo/admin/slides/new.haml +1 -0
- data/app/views/virgo/admin/slideshows/_form.haml +49 -0
- data/app/views/virgo/admin/slideshows/edit.haml +1 -0
- data/app/views/virgo/admin/slideshows/index.haml +43 -0
- data/app/views/virgo/admin/slideshows/new.haml +1 -0
- data/app/views/virgo/admin/tags/_form.haml +16 -0
- data/app/views/virgo/admin/tags/edit.haml +7 -0
- data/app/views/virgo/admin/tags/index.haml +33 -0
- data/app/views/virgo/admin/tags/modal_form.haml +12 -0
- data/app/views/virgo/admin/tags/new.haml +7 -0
- data/app/views/virgo/admin/tags/success_modal.haml +14 -0
- data/app/views/virgo/admin/users/_form.haml +62 -0
- data/app/views/virgo/admin/users/edit.haml +1 -0
- data/app/views/virgo/admin/users/index.haml +34 -0
- data/app/views/virgo/admin/users/new.haml +1 -0
- data/app/views/virgo/application/sitemap.xml.builder +0 -0
- data/app/views/virgo/authors/_profile_long.haml +13 -0
- data/app/views/virgo/authors/_profile_short.haml +10 -0
- data/app/views/virgo/categories/show.haml +24 -0
- data/app/views/virgo/columns/index.haml +19 -0
- data/app/views/virgo/columns/show.haml +18 -0
- data/app/views/virgo/common/_admin_navbar.haml +100 -0
- data/app/views/virgo/common/_alerts.haml +40 -0
- data/app/views/virgo/common/_analytics.html.erb +1 -0
- data/app/views/virgo/common/_base_errors.haml +8 -0
- data/app/views/virgo/common/_category_heading.haml +26 -0
- data/app/views/virgo/common/_disqus.html.erb +24 -0
- data/app/views/virgo/common/_disqus_comment_count.html.erb +12 -0
- data/app/views/virgo/common/_fb_api.html.erb +30 -0
- data/app/views/virgo/common/_fonts.html.erb +1 -0
- data/app/views/virgo/common/_footer.haml +13 -0
- data/app/views/virgo/common/_footer_javascript.html.erb +0 -0
- data/app/views/virgo/common/_header_analytics.html.erb +9 -0
- data/app/views/virgo/common/_list_signup.haml +20 -0
- data/app/views/virgo/common/_ouibounce_modal.haml +26 -0
- data/app/views/virgo/common/_public_nav.haml +58 -0
- data/app/views/virgo/common/_search_form.haml +8 -0
- data/app/views/virgo/common/_share_bar.haml +45 -0
- data/app/views/virgo/common/_user_vars.haml +2 -0
- data/app/views/virgo/errors/404.haml +10 -0
- data/app/views/virgo/errors/500.haml +10 -0
- data/app/views/virgo/images/_shortcode.haml +4 -0
- data/app/views/virgo/install/index.haml +58 -0
- data/app/views/virgo/install/success.haml +12 -0
- data/app/views/virgo/page_modules/_latest_posts.haml +29 -0
- data/app/views/virgo/page_modules/_popular_posts.haml +51 -0
- data/app/views/virgo/pages/authors.haml +8 -0
- data/app/views/virgo/pages/home.haml +0 -0
- data/app/views/virgo/pages/show.haml +6 -0
- data/app/views/virgo/posts/_author_info.haml +16 -0
- data/app/views/virgo/posts/_post_full.haml +63 -0
- data/app/views/virgo/posts/_post_short.haml +30 -0
- data/app/views/virgo/posts/_post_thumb_box.haml +13 -0
- data/app/views/virgo/posts/_recommendations.haml +12 -0
- data/app/views/virgo/posts/_sidebar_thumb.haml +4 -0
- data/app/views/virgo/posts/index.haml +22 -0
- data/app/views/virgo/posts/latest.haml +16 -0
- data/app/views/virgo/posts/more.haml +10 -0
- data/app/views/virgo/posts/rss.xml.builder +17 -0
- data/app/views/virgo/posts/show.haml +17 -0
- data/app/views/virgo/search/_form.haml +5 -0
- data/app/views/virgo/search/_item.haml +22 -0
- data/app/views/virgo/search/index.haml +11 -0
- data/app/views/virgo/search/new.haml +4 -0
- data/app/views/virgo/shortcode_templates/blockquote.haml +9 -0
- data/app/views/virgo/shortcode_templates/h1.haml +1 -0
- data/app/views/virgo/shortcode_templates/h2.haml +1 -0
- data/app/views/virgo/shortcode_templates/h3.haml +1 -0
- data/app/views/virgo/shortcode_templates/h4.haml +1 -0
- data/app/views/virgo/shortcode_templates/image.haml +17 -0
- data/app/views/virgo/shortcode_templates/pullquote.haml +3 -0
- data/app/views/virgo/shortcode_templates/slideshow.haml +35 -0
- data/app/views/virgo/shortcode_templates/tweet.haml +3 -0
- data/app/views/virgo/shortcode_templates/video.haml +8 -0
- data/app/views/virgo/subscribers/_success_modal.haml +13 -0
- data/app/views/virgo/subscribers/modal.haml +15 -0
- data/app/views/virgo/tags/show.haml +16 -0
- data/app/views/virgo/users/confirmations/new.html.haml +10 -0
- data/app/views/virgo/users/devise_mailer/confirmation_instructions.html.erb +7 -0
- data/app/views/virgo/users/devise_mailer/reset_password_instructions.html.haml +6 -0
- data/app/views/virgo/users/devise_mailer/unlock_instructions.html.haml +5 -0
- data/app/views/virgo/users/passwords/edit.html.haml +14 -0
- data/app/views/virgo/users/passwords/new.html.haml +14 -0
- data/app/views/virgo/users/registrations/edit.html.haml +30 -0
- data/app/views/virgo/users/registrations/new.html.haml +13 -0
- data/app/views/virgo/users/sessions/new.html.haml +12 -0
- data/app/views/virgo/users/shared/_links.haml +26 -0
- data/app/views/virgo/users/show.haml +11 -0
- data/app/views/virgo/users/unlocks/new.html.haml +9 -0
- data/config/initializers/action_mailer.rb +7 -0
- data/config/initializers/active_record.rb +72 -0
- data/config/initializers/active_record_symbolize.rb +18 -0
- data/config/initializers/assets.rb +17 -0
- data/config/initializers/backtrace_silencers.rb +7 -0
- data/config/initializers/carrierwave.rb +36 -0
- data/config/initializers/cookies_serializer.rb +3 -0
- data/config/initializers/devise.rb +257 -0
- data/config/initializers/filter_parameter_logging.rb +4 -0
- data/config/initializers/hash.rb +5 -0
- data/config/initializers/inflections.rb +16 -0
- data/config/initializers/mime_types.rb +4 -0
- data/config/initializers/nil_class.rb +13 -0
- data/config/initializers/non_digest_assets.rb +3 -0
- data/config/initializers/paper_trail.rb +30 -0
- data/config/initializers/schema_plus.rb +5 -0
- data/config/initializers/session_store.rb +3 -0
- data/config/initializers/shortcode.rb +12 -0
- data/config/initializers/simple_form.rb +170 -0
- data/config/initializers/simple_form_bootstrap.rb +136 -0
- data/config/initializers/string.rb +5 -0
- data/config/initializers/time_formatting.rb +23 -0
- data/config/initializers/wrap_parameters.rb +14 -0
- data/config/locales/devise.en.yml +60 -0
- data/config/routes.rb +96 -0
- data/config/schedule.rb +3 -0
- data/db/migrate/20150729174510_create_virgo_schema.rb +225 -0
- data/lib/generators/virgo/install/install_generator.rb +16 -0
- data/lib/generators/virgo/views/views_generator.rb +22 -0
- data/lib/tasks/virgo_tasks.rake +7 -0
- data/lib/virgo.rb +4 -0
- data/lib/virgo/development_mail_interceptor.rb +6 -0
- data/lib/virgo/email_validator.rb +6 -0
- data/lib/virgo/engine.rb +73 -0
- data/lib/virgo/reduce_validator.rb +6 -0
- data/lib/virgo/upload_helpers.rb +89 -0
- data/lib/virgo/version.rb +3 -0
- data/lib/virgo/view_help.rb +3 -0
- metadata +1051 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
module Virgo
|
2
|
+
class User < ActiveRecord::Base
|
3
|
+
module Search
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
scope :search, ->(filters=nil) {
|
8
|
+
filters ||= {}
|
9
|
+
|
10
|
+
items = all
|
11
|
+
|
12
|
+
if filters[:term].present?
|
13
|
+
parts = filters[:term].split(" ")
|
14
|
+
queries = []
|
15
|
+
vals = {}
|
16
|
+
|
17
|
+
parts.each_with_index do |part, i|
|
18
|
+
vals[:"term_#{i}"] = "%#{part}%"
|
19
|
+
|
20
|
+
queries << \
|
21
|
+
"(users.email ILIKE :term_#{i} OR " +
|
22
|
+
"users.first_name ILIKE :term_#{i} OR " +
|
23
|
+
"users.last_name ILIKE :term_#{i} OR " +
|
24
|
+
"users.username ILIKE :term_#{i} OR " +
|
25
|
+
"users.byline ILIKE :term_#{i})"
|
26
|
+
end
|
27
|
+
|
28
|
+
query = queries.join(" OR ")
|
29
|
+
|
30
|
+
items = items.where(query, vals)
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
items
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# note: role inheritence implemented via Ryan Bates' method
|
2
|
+
# outlined in https://github.com/ryanb/cancan/wiki/Role-Based-Authorization
|
3
|
+
module Virgo
|
4
|
+
class Ability
|
5
|
+
include CanCan::Ability
|
6
|
+
|
7
|
+
def initialize(user)
|
8
|
+
@user = user || User.new # for guest
|
9
|
+
|
10
|
+
send(@user.role) if @user.role.present?
|
11
|
+
end
|
12
|
+
|
13
|
+
def anonymous
|
14
|
+
can :read, Post do |post|
|
15
|
+
post.published? && post.live
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def normal
|
20
|
+
anonymous
|
21
|
+
end
|
22
|
+
|
23
|
+
def editor
|
24
|
+
can :manage, :all
|
25
|
+
can :manage, Virgo::User do |u|
|
26
|
+
u == @user
|
27
|
+
end
|
28
|
+
cannot :index, Virgo::User
|
29
|
+
end
|
30
|
+
|
31
|
+
def contributor
|
32
|
+
can :manage, Virgo::Post do |post|
|
33
|
+
post.author == @user
|
34
|
+
end
|
35
|
+
|
36
|
+
can :manage, Virgo::Slideshow do |slideshow|
|
37
|
+
slideshow.author == @user
|
38
|
+
end
|
39
|
+
|
40
|
+
can :manage, Virgo::Slide do |slide|
|
41
|
+
can?(:manage, slide.slideshow)
|
42
|
+
end
|
43
|
+
|
44
|
+
can :read, Virgo::Post
|
45
|
+
|
46
|
+
cannot :manage, Virgo::User
|
47
|
+
cannot :manage, Virgo::Column
|
48
|
+
cannot :manage, Virgo::Site
|
49
|
+
end
|
50
|
+
|
51
|
+
def admin
|
52
|
+
can :manage, :all
|
53
|
+
end
|
54
|
+
|
55
|
+
def superuser
|
56
|
+
can :manage, :all
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Virgo
|
2
|
+
class Category < ActiveRecord::Base
|
3
|
+
enable_redis_tracking
|
4
|
+
|
5
|
+
extend FriendlyId
|
6
|
+
|
7
|
+
friendly_id :name, use: :slugged
|
8
|
+
|
9
|
+
has_many :post_categories, dependent: :destroy
|
10
|
+
has_many :posts, ->{ where(post_type: :post) }, through: :post_categories
|
11
|
+
|
12
|
+
belongs_to :parent, class_name: 'Virgo::Category'
|
13
|
+
|
14
|
+
belongs_to :created_by, class_name: 'Virgo::User'
|
15
|
+
|
16
|
+
has_many :children, class_name: 'Virgo::Category', foreign_key: :parent_id
|
17
|
+
|
18
|
+
validates :name, presence: true, uniqueness: true
|
19
|
+
|
20
|
+
scope :visible, ->{ where(visible: true) }
|
21
|
+
|
22
|
+
scope :by_weight, ->{ order(navbar_weight: :asc) }
|
23
|
+
|
24
|
+
scope :navbar, ->{ where(display_in_navbar: true).by_weight }
|
25
|
+
|
26
|
+
scope :bottom_level, ->{
|
27
|
+
where("virgo_categories.id NOT IN (SELECT DISTINCT virgo_categories.parent_id FROM virgo_categories WHERE virgo_categories.parent_id IS NOT NULL)")
|
28
|
+
}
|
29
|
+
|
30
|
+
scope :top_level, ->{
|
31
|
+
where(parent_id: nil)
|
32
|
+
}
|
33
|
+
|
34
|
+
scope :with_post_count, ->{
|
35
|
+
select("virgo_categories.*, " +
|
36
|
+
"(SELECT COUNT(*) FROM virgo_post_categories WHERE virgo_post_categories.category_id = virgo_categories.id) AS post_count"
|
37
|
+
)
|
38
|
+
}
|
39
|
+
|
40
|
+
def self.COLUMNS
|
41
|
+
find_by(name: "Columns")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Virgo
|
2
|
+
class Column < ActiveRecord::Base
|
3
|
+
enable_redis_tracking
|
4
|
+
|
5
|
+
extend FriendlyId
|
6
|
+
|
7
|
+
friendly_id :name, use: :slugged
|
8
|
+
|
9
|
+
mount_uploader :image, Virgo::FeaturedImageUploader
|
10
|
+
|
11
|
+
validates :name, presence: true
|
12
|
+
|
13
|
+
has_many :posts
|
14
|
+
|
15
|
+
after_save :expire_site_key
|
16
|
+
|
17
|
+
scope :with_post_count, ->{
|
18
|
+
select("virgo_columns.*, " +
|
19
|
+
"(SELECT COUNT(*) FROM virgo_posts WHERE virgo_posts.column_id = virgo_columns.id) AS post_count"
|
20
|
+
)
|
21
|
+
}
|
22
|
+
|
23
|
+
scope :by_weight, ->{ order(weight: :asc) }
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Virgo
|
2
|
+
class FormModel
|
3
|
+
include ActiveModel::Validations
|
4
|
+
include ActiveModel::Conversion
|
5
|
+
extend ActiveModel::Naming
|
6
|
+
|
7
|
+
def initialize(attributes = {})
|
8
|
+
if attributes
|
9
|
+
attributes.each do |name, value|
|
10
|
+
send("#{name}=", value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def persisted?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Virgo
|
2
|
+
class Image < ActiveRecord::Base
|
3
|
+
extend FriendlyId
|
4
|
+
|
5
|
+
friendly_id :name, use: :slugged
|
6
|
+
|
7
|
+
mount_uploader :image, Virgo::ImageUploader
|
8
|
+
|
9
|
+
validates :name, presence: true
|
10
|
+
validates :image, presence: true
|
11
|
+
|
12
|
+
belongs_to :user
|
13
|
+
|
14
|
+
before_validation :derive_name
|
15
|
+
|
16
|
+
scope :search, ->(filters={}){
|
17
|
+
items = all
|
18
|
+
|
19
|
+
if filters[:term].present?
|
20
|
+
items = items.where("images.name ILIKE :term", term: "%#{filters[:term]}%")
|
21
|
+
end
|
22
|
+
|
23
|
+
items
|
24
|
+
}
|
25
|
+
|
26
|
+
def url(arg)
|
27
|
+
image.url
|
28
|
+
end
|
29
|
+
|
30
|
+
def redactor_json
|
31
|
+
{
|
32
|
+
thumb: image.url(:thumb),
|
33
|
+
image: image.url
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def as_json(opts={})
|
38
|
+
{
|
39
|
+
id: id,
|
40
|
+
name: name,
|
41
|
+
alt_text: alt_text,
|
42
|
+
credit: credit,
|
43
|
+
description: description,
|
44
|
+
slug: slug,
|
45
|
+
image_url: image.try(:url),
|
46
|
+
small_thumb_image: image.try(:url, :small_thumb),
|
47
|
+
thumb_image: image.try(:url, :thumb),
|
48
|
+
med_thumb_image: image.try(:url, :med_thumb),
|
49
|
+
content_image: image.try(:url, :content),
|
50
|
+
featured_image: image.try(:url, :featured),
|
51
|
+
wide_image: image.try(:url, :wide)
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def derive_name
|
58
|
+
if defined?(image_changed?) && image_changed? && !name_changed? && name.blank?
|
59
|
+
self.name = image.file.try(:filename)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Virgo
|
2
|
+
class PageModule < ActiveRecord::Base
|
3
|
+
mount_uploader :image, Virgo::ImageUploader
|
4
|
+
|
5
|
+
attr_accessor :subject_headline # form hook
|
6
|
+
|
7
|
+
before_save :derive_template_path
|
8
|
+
|
9
|
+
validates :name, presence: true
|
10
|
+
|
11
|
+
belongs_to :subject, polymorphic: true
|
12
|
+
|
13
|
+
has_many :page_module_posts
|
14
|
+
has_many :posts, through: :page_module_posts
|
15
|
+
|
16
|
+
scope :enabled, ->{ where(enabled: true) }
|
17
|
+
|
18
|
+
scope :visible_to_admins, ->{ where(hidden_from_admins: false) }
|
19
|
+
|
20
|
+
after_save :expire_site_key
|
21
|
+
|
22
|
+
def self.POPULAR
|
23
|
+
@_popular ||= find_by(name: 'popular posts')
|
24
|
+
end
|
25
|
+
|
26
|
+
def admin_image_path
|
27
|
+
"virgo/admin/page_modules/#{name.downcase.gsub(' ', '_').gsub('-', '_')}.png"
|
28
|
+
end
|
29
|
+
|
30
|
+
def subject_headline
|
31
|
+
subject.try :headline
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def derive_template_path(opts={})
|
37
|
+
if name_changed? || opts[:force]
|
38
|
+
self.template_path = "/virgo/page_modules/#{name.downcase.gsub(' ', '_').gsub('-', '_')}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,391 @@
|
|
1
|
+
module Virgo
|
2
|
+
class Post < ActiveRecord::Base
|
3
|
+
paginates_per 10
|
4
|
+
|
5
|
+
enable_redis_tracking
|
6
|
+
|
7
|
+
has_paper_trail ignore: [:updated_at, :editing_user_id, :editing_timestamp, :live, :view_count, :meta, :popularity]
|
8
|
+
|
9
|
+
include Post::Search, Post::Recommendations, ActionView::Helpers::SanitizeHelper, Common::Uuid, Common::SlugHistory
|
10
|
+
|
11
|
+
extend FriendlyId
|
12
|
+
|
13
|
+
attr_accessor :ordered_tag_ids
|
14
|
+
|
15
|
+
friendly_id :slug_candidates, use: :slugged
|
16
|
+
|
17
|
+
belongs_to :author, class_name: 'Virgo::User'
|
18
|
+
|
19
|
+
belongs_to :featured_image, class_name: 'Virgo::Image'
|
20
|
+
belongs_to :thumbnail_image, class_name: 'Virgo::Image'
|
21
|
+
|
22
|
+
belongs_to :created_by, class_name: 'Virgo::User'
|
23
|
+
|
24
|
+
belongs_to :editing_user, class_name: 'Virgo::User'
|
25
|
+
|
26
|
+
validates :author_id, presence: true
|
27
|
+
|
28
|
+
has_many :post_tags, dependent: :destroy
|
29
|
+
has_many :tags, through: :post_tags
|
30
|
+
|
31
|
+
has_many :post_categories, dependent: :destroy
|
32
|
+
has_many :categories, through: :post_categories
|
33
|
+
|
34
|
+
belongs_to :column, touch: true
|
35
|
+
|
36
|
+
validates :headline, presence: true, unless: :page?
|
37
|
+
validates :body, presence: true
|
38
|
+
|
39
|
+
validates :slug, presence: true, uniqueness: true
|
40
|
+
|
41
|
+
symbolize :status
|
42
|
+
symbolize :post_type
|
43
|
+
|
44
|
+
scope :with_relations, ->{ joins(:author).preload(:author, :tags, :categories) }
|
45
|
+
|
46
|
+
scope :publicly_viewable, ->{ where(status: :published, live: true).where.not(publish_at: nil) }
|
47
|
+
scope :published, ->{ where(status: :published) }
|
48
|
+
scope :draft, ->{ where(status: :draft) }
|
49
|
+
scope :pages, ->{ where(post_type: :page) }
|
50
|
+
scope :posts, ->{ where(post_type: :post) }
|
51
|
+
scope :for_month, ->(month){ where("posts.publish_at >= :month_start AND posts.publish_at <= :month_end",
|
52
|
+
month_start: month.beginning_of_month,
|
53
|
+
month_end: month.end_of_month) }
|
54
|
+
|
55
|
+
scope :for_weekly_email, ->{
|
56
|
+
publicly_viewable.where(include_in_weekly_email: true)
|
57
|
+
}
|
58
|
+
|
59
|
+
scope :exclude_front_page_feature, ->{
|
60
|
+
items = posts.publicly_viewable
|
61
|
+
|
62
|
+
if Post.front_page_feature
|
63
|
+
items = items.where.not(id: Post.front_page_feature.id)
|
64
|
+
end
|
65
|
+
|
66
|
+
items
|
67
|
+
}
|
68
|
+
|
69
|
+
scope :exclude_category_feature, ->(category){
|
70
|
+
items = posts.publicly_viewable
|
71
|
+
|
72
|
+
if Post.category_feature(category)
|
73
|
+
items = items.where.not(id: Post.category_feature(category).id)
|
74
|
+
end
|
75
|
+
|
76
|
+
items
|
77
|
+
}
|
78
|
+
|
79
|
+
scope :exclude, ->(item) {
|
80
|
+
if item.nil?
|
81
|
+
all
|
82
|
+
else
|
83
|
+
where.not(id: item.id)
|
84
|
+
end
|
85
|
+
}
|
86
|
+
|
87
|
+
scope :most_popular, ->{
|
88
|
+
posts.publicly_viewable.order(popularity: :desc)
|
89
|
+
}
|
90
|
+
|
91
|
+
scope :latest, ->{ posts.publicly_viewable.order(publish_at: :desc) }
|
92
|
+
|
93
|
+
scope :containing_slideshow, ->(slideshow){
|
94
|
+
where("virgo_posts.meta -> 'contains_slideshow' = 'true'").where("virgo_posts.meta -> 'slideshow_id' = ?", slideshow.id.to_s)
|
95
|
+
}
|
96
|
+
|
97
|
+
before_save :set_default_publish_at
|
98
|
+
before_save :set_live_status
|
99
|
+
before_save :generate_search_document
|
100
|
+
before_save :set_meta
|
101
|
+
|
102
|
+
after_save :expire_site_key
|
103
|
+
after_save :touch_dependent
|
104
|
+
after_save :set_columns_category
|
105
|
+
after_save :apply_tag_ordering!
|
106
|
+
|
107
|
+
if Rails.application.config.require_posts_to_have_category
|
108
|
+
validate :has_category
|
109
|
+
end
|
110
|
+
|
111
|
+
after_initialize :init
|
112
|
+
|
113
|
+
def self.category_feature(category)
|
114
|
+
category.posts.publicly_viewable.where(feature_on_category_page: true).order(publish_at: :asc).last
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.front_page_feature
|
118
|
+
posts.publicly_viewable.where(feature_on_front_page: true).order(publish_at: :asc).last
|
119
|
+
end
|
120
|
+
|
121
|
+
def permalink
|
122
|
+
urls.post_detail_url(self, protocol: 'https')
|
123
|
+
end
|
124
|
+
|
125
|
+
def rendered_body
|
126
|
+
@rendered_body ||= Shortcode.process(body || '').html_safe
|
127
|
+
end
|
128
|
+
|
129
|
+
def draft?
|
130
|
+
status == :draft
|
131
|
+
end
|
132
|
+
|
133
|
+
def assigned?
|
134
|
+
status == :assigned
|
135
|
+
end
|
136
|
+
|
137
|
+
def published?
|
138
|
+
status == :published
|
139
|
+
end
|
140
|
+
|
141
|
+
def killed?
|
142
|
+
status == :killed
|
143
|
+
end
|
144
|
+
|
145
|
+
def hidden?
|
146
|
+
status == :hidden
|
147
|
+
end
|
148
|
+
|
149
|
+
def page?
|
150
|
+
post_type == :page
|
151
|
+
end
|
152
|
+
|
153
|
+
def post?
|
154
|
+
post_type == :post
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.status_names
|
158
|
+
{
|
159
|
+
'Draft' => :draft,
|
160
|
+
'Assigned' => :assigned,
|
161
|
+
'Published' => :published,
|
162
|
+
'Killed' => :killed,
|
163
|
+
'Hidden' => :hidden
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
def disqus_identifier
|
168
|
+
"post_#{id}"
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.publish_scheduled!
|
172
|
+
candidates = Post.where.not(live: true).where("publish_at <= ?", Time.now.to_datetime)
|
173
|
+
|
174
|
+
candidates.each do |post|
|
175
|
+
post.update(live: true)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def has_citation?
|
180
|
+
citation_name.present? && citation_url.present?
|
181
|
+
end
|
182
|
+
|
183
|
+
# need to explicitly unscope this because papertrail
|
184
|
+
# imposes a forward chronological order with a default scope
|
185
|
+
def unscoped_versions
|
186
|
+
PaperTrail::Version.unscoped.where(item_id: id)
|
187
|
+
end
|
188
|
+
|
189
|
+
# i.e. if the lock timeout is "15 seconds" but the post's
|
190
|
+
# "editing_timestamp" has been bumped less than 15 seconds ago,
|
191
|
+
# we say the post is "mid-edit"
|
192
|
+
def is_mid_edit?
|
193
|
+
editing_timestamp && editing_timestamp > edit_lock_timeout.ago
|
194
|
+
end
|
195
|
+
|
196
|
+
def lock_for_editing!(user)
|
197
|
+
update!(editing_timestamp: Time.now.to_datetime, editing_user: user)
|
198
|
+
end
|
199
|
+
|
200
|
+
def attempt_edit_lock(user)
|
201
|
+
if is_mid_edit? && editing_user != user
|
202
|
+
false
|
203
|
+
else
|
204
|
+
lock_for_editing!(user) if persisted?
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def edit_lock_timeout
|
209
|
+
Rails.application.config.edit_lock_timeout
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.cache_key
|
213
|
+
"posts-#{maximum(:updated_at).try(:to_i)}-#{all.size}"
|
214
|
+
end
|
215
|
+
|
216
|
+
def track_view!
|
217
|
+
|
218
|
+
update_columns({
|
219
|
+
view_count: (view_count + 1),
|
220
|
+
popularity: calc_popularity(:increment)
|
221
|
+
})
|
222
|
+
end
|
223
|
+
|
224
|
+
def calc_popularity(opt=nil)
|
225
|
+
unless publish_at.nil?
|
226
|
+
# the number of days ago the very oldest post was made
|
227
|
+
days_ago_max = (Time.now - Post.minimum(:publish_at))/86400
|
228
|
+
days_ago = (Time.now - publish_at)/86400
|
229
|
+
|
230
|
+
# ex oldest post gets age_percentage 0.0, newest post gets 1.
|
231
|
+
age_percentage = (1 - (days_ago/(days_ago_max || 1.0)))
|
232
|
+
|
233
|
+
# boost from today to 10x...
|
234
|
+
age_percentage = 10 if publish_at.to_date == Date.today
|
235
|
+
|
236
|
+
# age factor is a number in the range of 0.1 - 1.00
|
237
|
+
# that is used to moderate the weight of votes for this post
|
238
|
+
age_factor = 0.1 + (0.9*age_percentage)
|
239
|
+
|
240
|
+
basis = opt == :increment ? (view_count + 1) : view_count
|
241
|
+
|
242
|
+
basis * age_factor
|
243
|
+
else
|
244
|
+
0
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def apply_tag_ordering!
|
249
|
+
if ordered_tag_ids.present?
|
250
|
+
if ordered_tag_ids.first.is_a?(String)
|
251
|
+
cleaned = ordered_tag_ids.first.gsub("[", "").gsub("]", "").split(",").map(&:to_i)
|
252
|
+
ordered_tag_ids = cleaned
|
253
|
+
end
|
254
|
+
|
255
|
+
ordered_tag_ids.each_with_index do |tag_id, index|
|
256
|
+
existing = post_tags.find_by(tag_id: tag_id)
|
257
|
+
|
258
|
+
if existing
|
259
|
+
existing.update!(position: index)
|
260
|
+
else
|
261
|
+
tag = Tag.find_by(id: tag_id)
|
262
|
+
|
263
|
+
if tag
|
264
|
+
post_tags.create!(tag_id: tag_id, position: index)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
post_tags.where.not(tag_id: ordered_tag_ids).map &:destroy
|
270
|
+
end
|
271
|
+
|
272
|
+
true
|
273
|
+
end
|
274
|
+
|
275
|
+
def primary_category
|
276
|
+
categories.first
|
277
|
+
end
|
278
|
+
|
279
|
+
def description
|
280
|
+
if meta_description_tag_value.present?
|
281
|
+
meta_description_tag_value
|
282
|
+
elsif excerpt.present?
|
283
|
+
view_help.truncate(excerpt, length: 160)
|
284
|
+
else
|
285
|
+
view_help.truncate(view_help.strip_tags(rendered_body || ''), length: 160)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def thumb_image
|
290
|
+
if thumbnail_image.present?
|
291
|
+
thumbnail_image
|
292
|
+
else
|
293
|
+
featured_image
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
private
|
299
|
+
|
300
|
+
def set_default_publish_at
|
301
|
+
if status_changed? && status == :published && publish_at.blank?
|
302
|
+
self.publish_at = Time.now.to_datetime
|
303
|
+
end
|
304
|
+
|
305
|
+
true
|
306
|
+
end
|
307
|
+
|
308
|
+
def init
|
309
|
+
self.post_type = :post if post_type.nil?
|
310
|
+
end
|
311
|
+
|
312
|
+
def generate_search_document(opts={})
|
313
|
+
if headline_changed? || body_changed? || author_id_changed? || excerpt_changed? || opts[:force]
|
314
|
+
self.search_document = "#{headline} by #{author.byline}#{column ? (' ' + column.name) : ''}#{excerpt.present? ? (' ' + excerpt) : ''} #{strip_tags(rendered_body)}"
|
315
|
+
end
|
316
|
+
|
317
|
+
true
|
318
|
+
end
|
319
|
+
|
320
|
+
def set_meta
|
321
|
+
self.meta = {} if (meta.nil? || meta.is_a?(String))
|
322
|
+
|
323
|
+
# note: dirty tracking does not workin with hstore and meta_will_change! must be called
|
324
|
+
# for changes to an existing hash to persist: https://github.com/rails/rails/issues/6127
|
325
|
+
|
326
|
+
if has_slideshow?
|
327
|
+
slideshow_id = body.match(/\[slideshow id=\"(?<slideshow_id>\d+)\"\]/)['slideshow_id']
|
328
|
+
_will_change = (meta['contains_slideshow'] != 'true' || meta['slideshow_id'] != slideshow_id)
|
329
|
+
self.meta['contains_slideshow'] = 'true'
|
330
|
+
self.meta['slideshow_id'] = slideshow_id
|
331
|
+
meta_will_change! if _will_change
|
332
|
+
else
|
333
|
+
_will_change = meta['contains_slideshow'] != 'false'
|
334
|
+
self.meta['contains_slideshow'] = 'false'
|
335
|
+
meta_will_change! if _will_change
|
336
|
+
end
|
337
|
+
|
338
|
+
true
|
339
|
+
end
|
340
|
+
|
341
|
+
def set_columns_category
|
342
|
+
if column_id_changed?
|
343
|
+
if column_id.present? && !categories.include?(Category.COLUMNS)
|
344
|
+
self.categories << Category.COLUMNS
|
345
|
+
elsif column_id.blank? && categories.include?(Category.COLUMNS)
|
346
|
+
self.post_categories.where(category_id: Category.COLUMNS.id).map &:destroy
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
true
|
351
|
+
end
|
352
|
+
|
353
|
+
def has_slideshow?
|
354
|
+
body.present? && body.match(/\[slideshow id=\"\d+\"\]/).present?
|
355
|
+
end
|
356
|
+
|
357
|
+
def has_category
|
358
|
+
if categories.size < 1 && post?
|
359
|
+
errors[:base] << "Post must have at least one category"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# if a post has a publish_at value set and it is
|
364
|
+
# in the future, set the post to not be live
|
365
|
+
def set_live_status
|
366
|
+
if publish_at.present? && publish_at >= Time.now
|
367
|
+
self.live = false
|
368
|
+
else
|
369
|
+
self.live = true
|
370
|
+
end
|
371
|
+
|
372
|
+
true
|
373
|
+
end
|
374
|
+
|
375
|
+
def touch_dependent
|
376
|
+
categories.map &:touch
|
377
|
+
end
|
378
|
+
|
379
|
+
def should_generate_new_friendly_id?
|
380
|
+
slug.blank?
|
381
|
+
end
|
382
|
+
|
383
|
+
def slug_candidates
|
384
|
+
[
|
385
|
+
:headline,
|
386
|
+
[:headline, :uuid],
|
387
|
+
:uuid
|
388
|
+
]
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|