diff --git a/Guardfile b/Guardfile index fbbeaf8e1..14dac7561 100644 --- a/Guardfile +++ b/Guardfile @@ -65,7 +65,7 @@ guard :rspec, rspec_options do [ rspec.spec.call("routing/#{m[1]}_routing"), rspec.spec.call("controllers/#{m[1]}_controller"), - rspec.spec.call("acceptance/#{m[1]}"), + rspec.spec.call("acceptance/#{m[1]}") ] end diff --git a/app/assets/javascripts/initializers/initializeBaseTracking.js.erb b/app/assets/javascripts/initializers/initializeBaseTracking.js.erb index 5aa7ac93e..85cdd275f 100644 --- a/app/assets/javascripts/initializers/initializeBaseTracking.js.erb +++ b/app/assets/javascripts/initializers/initializeBaseTracking.js.erb @@ -12,9 +12,6 @@ function initializeBaseTracking() { wait++; if (window.ga && ga.create) { ga('create', '<%= ApplicationConfig["GA_TRACKING_ID"] %>', 'auto'); - if (!checkUserLoggedIn() && Math.random() < 0.35) { - ga('require', '<%= ApplicationConfig["GA_OPTIMIZE_ID"] %>'); - } ga('send', 'pageview', location.pathname + location.search); if (document.location.search.indexOf('newly-registered-user=true') > -1) { ga('send', 'event', 'registration', 'New user registration', location.pathname, null); @@ -29,6 +26,7 @@ function initializeBaseTracking() { }, 25); facebookTrackingScript(); eventListening(); + trackCustomImpressions(); } function fallbackActivityRecording() { @@ -107,4 +105,50 @@ function logImpressions() { var data = el.dataset.featuredArticle; ga('send', 'event', 'view', 'featured-feed-impression', data, null); } +} + +function trackCustomImpressions() { + setTimeout(function(){ + var stickyNav = document.getElementById('article-show-primary-sticky-nav'); + var sidebarHTMLVariant = document.getElementById('html-variant-article-show-sidebar'); + var ArticleElement = document.getElementById('article-body'); + var tokenMeta = document.querySelector("meta[name='csrf-token']") + var randomNumber = Math.floor(Math.random() * 10); // 1 in 10; Only track 1 in 10 impressions + var isBot = /bot|google|baidu|bing|msn|duckduckbot|teoma|slurp|yandex/i.test(navigator.userAgent) // is crawler + if (sidebarHTMLVariant && ArticleElement && tokenMeta && !isBot) { + var dataBody = { + html_variant_id: sidebarHTMLVariant.dataset.variantId, + article_id: ArticleElement.dataset.articleId, + }; + var csrfToken = tokenMeta.getAttribute('content'); + if (randomNumber === 1) { + window.fetch('/html_variant_trials', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dataBody), + credentials: 'same-origin', + }); + } + var successLinks = stickyNav.querySelectorAll('a,button'); //track all links and button clicks within nav + for(var i = 0; i < successLinks.length; i++) + { + successLinks[i].addEventListener('click', function() { trackHtmlVariantSuccess(dataBody, csrfToken) }); + } + } + }, 1500) +} + +function trackHtmlVariantSuccess(dataBody, csrfToken) { + window.fetch('/html_variant_successes', { + method: 'POST', + headers: { + 'X-CSRF-Token': csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dataBody), + credentials: 'same-origin', + }) } \ No newline at end of file diff --git a/app/assets/stylesheets/article-show.scss b/app/assets/stylesheets/article-show.scss index d273e35ff..b5f20a03b 100644 --- a/app/assets/stylesheets/article-show.scss +++ b/app/assets/stylesheets/article-show.scss @@ -663,7 +663,7 @@ header{ button{ background:transparent; margin: 0px 0.22vw; - padding:calc(0.05vw + 3px) calc(0.88vw + 3px); + padding: 3px calc(0.88vw + 3px); border-radius:100px; border:2px solid darken($light-gray, 7%); font-family: $helvetica-condensed; @@ -702,7 +702,6 @@ header{ } &.user-activated { border:2px solid darken($bold-blue, 40%); - padding:calc(0.05vw + 3px) calc(0.88vw + 3px); background: linear-gradient(60deg, #f4d2d2, #efb6b6); img{ filter: none; @@ -1004,13 +1003,15 @@ header{ @keyframes reaction { 0% { - padding:calc(0.05vw + 3px) calc(0.88vw + 3px); + padding-left: calc(0.88vw + 3px); + padding-right: calc(0.88vw + 3px); } 50% { - padding:calc(0.05vw + 3px) calc(1.1vw + 3px); - margin: 0px 0.00vw; + padding-left: calc(1.1vw + 3px); + padding-left: calc(1.1vw + 3px); } 100% { - padding:calc(0.05vw + 3px) calc(0.88vw + 3px); + padding-left: calc(0.88vw + 3px); + padding-right: calc(0.88vw + 3px); } } \ No newline at end of file diff --git a/app/assets/stylesheets/settings.scss b/app/assets/stylesheets/settings.scss index 33c511d2a..dc51df73c 100644 --- a/app/assets/stylesheets/settings.scss +++ b/app/assets/stylesheets/settings.scss @@ -108,7 +108,6 @@ } } label { - width: 160px; display:inline-block; vertical-align:top; padding:14px 0px 8px; diff --git a/app/assets/stylesheets/sticky-nav.scss b/app/assets/stylesheets/sticky-nav.scss index f2fba9719..641020aa3 100644 --- a/app/assets/stylesheets/sticky-nav.scss +++ b/app/assets/stylesheets/sticky-nav.scss @@ -39,13 +39,12 @@ .primary-sticky-nav-author-follow{ padding-top:8px; button{ - width:136px; - font-size:1.1em; + width:100%; + font-size:1.25em; border:0px; border-radius:3px; padding: 3px; - height: 29px; - + height: 44px; } } } @@ -112,38 +111,67 @@ box-shadow: 3px 4px 0px darken($light-medium-gray, 17%); } } + .user-metadata-details { + margin-top: 10px; + margin-left: 2px; + font-family: $monospace; + .row{ + padding: calc(5px + 0.2vw) 0px; + display: inline-block; + width: 96%; + } + .key{ + font-size: 0.8em; + font-weight: 800; + margin-bottom: 2px; + color: $medium-gray; + } + .value{ + display: inline-block; + font-size: 0.92em; + margin-bottom: 4px; + } + } + .html-variant-wrapper{ + margin: 10px; + } } -.primary-sticky-nav-org{ - position:relative; - .sticky-box-connector{ - position:absolute; - top: - 10px; - height: 10px; - width: 5px; - background: $outline-color; - left: 15px; - &.sticky-box-right-connector{ - left: auto; - right: 15px; - } - } + .primary-sticky-nav-org-summary{ font-weight: 400; - color: $medium-gray; - padding: 0px 0px 5px; - font-size:0.88em; + color: $black; + padding: 4px 0px; + font-size:0.95em; + margin-top: 20px; + border-top: 1px solid $light-medium-gray; p{ - margin:5px 0px; + margin:12px 0px; + } + a.primary-sticky-nav-org-cta-link{ + width:100%; + display: block; + font-size:1.25em; + border:0px; + border-radius:3px; + padding: 8px 0px 3px; + margin-top: 20px; + background: white; + color: $black; + font-family: $helvetica-condensed; + border: 1px solid darken($light-medium-gray, 7%); + text-align: center; + .primary-sticky-nav-org-cta-link-domain-small{ + display: block; + padding-top: 2px; + font-size:0.8em; + color: darken($light-medium-gray, 4%); + font-family: $monospace; + } + &:hover { + border: 1px solid darken($light-medium-gray, 12%); + box-shadow: $shadow; + transition: all 0.1s; + } } } - button{ - min-width:135px; - font-size:1.1em; - border:0px; - border-radius:3px; - padding: 3px; - display: block; - margin-top:18px; - } -} diff --git a/app/black_box/google_analytics.rb b/app/black_box/google_analytics.rb index dcb4a8ea4..2c88af89b 100644 --- a/app/black_box/google_analytics.rb +++ b/app/black_box/google_analytics.rb @@ -61,12 +61,10 @@ class GoogleAnalytics metrics: [Metric.new(expression: metrics_string)], dimensions: [Dimension.new(name: "ga:segment")], segments: [Segment.new(segment_id: "gaid::-1"), - Segment.new(segment_id: "gaid::-2"), - Segment.new(segment_id: "gaid::-19"), - Segment.new(segment_id: "gaid::-7")], - date_ranges: [ - DateRange.new(start_date: @start_date, end_date: "2020-01-01"), - ], + Segment.new(segment_id: "gaid::-2"), + Segment.new(segment_id: "gaid::-19"), + Segment.new(segment_id: "gaid::-7")], + date_ranges: [DateRange.new(start_date: @start_date, end_date: "2020-01-01")], ) end diff --git a/app/controllers/admin/html_variants_controller.rb b/app/controllers/admin/html_variants_controller.rb new file mode 100644 index 000000000..0ca6e91f7 --- /dev/null +++ b/app/controllers/admin/html_variants_controller.rb @@ -0,0 +1,21 @@ +module Admin + class HtmlVariantsController < Admin::ApplicationController + # To customize the behavior of this controller, + # you can overwrite any of the RESTful actions. For example: + # + # def index + # super + # @resources = HtmlVariant. + # page(params[:page]). + # per(10) + # end + + # Define a custom finder by overriding the `find_resource` method: + # def find_resource(param) + # HtmlVariant.find_by!(slug: param) + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information + end +end diff --git a/app/controllers/html_variant_successes_controller.rb b/app/controllers/html_variant_successes_controller.rb new file mode 100644 index 000000000..38b267b39 --- /dev/null +++ b/app/controllers/html_variant_successes_controller.rb @@ -0,0 +1,6 @@ +class HtmlVariantSuccessesController < ApplicationController + def create + HtmlVariantSuccess.delay.create(html_variant_id: params[:html_variant_id], article_id: params[:article_id]) + head :ok + end +end diff --git a/app/controllers/html_variant_trials_controller.rb b/app/controllers/html_variant_trials_controller.rb new file mode 100644 index 000000000..00d212136 --- /dev/null +++ b/app/controllers/html_variant_trials_controller.rb @@ -0,0 +1,6 @@ +class HtmlVariantTrialsController < ApplicationController + def create + HtmlVariantTrial.delay.create(html_variant_id: params[:html_variant_id], article_id: params[:article_id]) + head :ok + end +end diff --git a/app/controllers/html_variants_controller.rb b/app/controllers/html_variants_controller.rb new file mode 100644 index 000000000..45dc1b5b7 --- /dev/null +++ b/app/controllers/html_variants_controller.rb @@ -0,0 +1,53 @@ +class HtmlVariantsController < ApplicationController + after_action :verify_authorized + + def index + authorize HtmlVariant + @user_variants = current_user.html_variants.order("created_at DESC") + @all_published_variants = HtmlVariant.where(published: true).order("created_at DESC") + @preview_path = Article.where(featured: true, published: true).order("published_at DESC").first&.path.to_s + end + + def new + authorize HtmlVariant + @html_variant = HtmlVariant.new + if params[:fork_id] + @fork = HtmlVariant.find(params[:fork_id]) + @html_variant.name = @fork.name + " FORK" + @html_variant.html = @fork.html + end + end + + def edit + @html_variant = HtmlVariant.find(params[:id]) + authorize @html_variant + end + + def create + authorize HtmlVariant + @html_variant = HtmlVariant.new(html_variant_params) + @html_variant.group = "article_show_sidebar_cta" + @html_variant.user_id = current_user.id + if @html_variant.save + redirect_to "/html_variants" + else + render :new + end + end + + def update + @html_variant = HtmlVariant.find(params[:id]) + authorize @html_variant + if @html_variant.update(html_variant_params) + redirect_to "/html_variants" + else + render :edit + end + end + + private + + def html_variant_params + params.require(:html_variant).permit(policy(HtmlVariant).permitted_attributes) + end +end diff --git a/app/dashboards/dashboard_manifest.rb b/app/dashboards/dashboard_manifest.rb index 37bb7fe12..50da85be5 100644 --- a/app/dashboards/dashboard_manifest.rb +++ b/app/dashboards/dashboard_manifest.rb @@ -25,6 +25,7 @@ class DashboardManifest display_ads badges badge_achievements + html_variants ].freeze # DASHBOARDS = [ # :users, diff --git a/app/dashboards/html_variant_dashboard.rb b/app/dashboards/html_variant_dashboard.rb new file mode 100644 index 000000000..fcc37fa1c --- /dev/null +++ b/app/dashboards/html_variant_dashboard.rb @@ -0,0 +1,78 @@ +require "administrate/base_dashboard" + +class HtmlVariantDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + user: Field::BelongsTo, + html_variant_trials: Field::HasMany, + html_variant_successes: Field::HasMany, + id: Field::Number, + group: Field::String, + name: Field::String, + html: Field::Text, + target_tag: Field::String, + success_rate: Field::Number.with_options(decimals: 2), + published: Field::Boolean, + approved: Field::Boolean, + created_at: Field::DateTime, + updated_at: Field::DateTime, + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = [ + :user, + :html_variant_trials, + :html_variant_successes, + :id, + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = [ + :user, + :html_variant_trials, + :html_variant_successes, + :id, + :group, + :name, + :html, + :target_tag, + :success_rate, + :published, + :approved, + :created_at, + :updated_at, + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = [ + :user, + :html_variant_trials, + :html_variant_successes, + :group, + :name, + :html, + :target_tag, + :success_rate, + :published, + :approved, + ].freeze + + # Overwrite this method to customize how html variants are displayed + # across all pages of the admin dashboard. + # + # def display_resource(html_variant) + # "HtmlVariant ##{html_variant.id}" + # end +end diff --git a/app/decorators/user_decorator.rb b/app/decorators/user_decorator.rb index fdbcad935..7bad166c2 100644 --- a/app/decorators/user_decorator.rb +++ b/app/decorators/user_decorator.rb @@ -44,8 +44,8 @@ class UserDecorator < ApplicationDecorator text: "#ffffff" }, { - bg: "#edebf6", - text: " #070126" + bg: "#2e0338", + text: " #ffffff" }, { bg: "#080E3B", diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 47f86110c..bd2787b66 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -29,6 +29,7 @@ module ApplicationHelper comments notifications reading_list_items + html_variants ).include?(controller_name) end @@ -153,7 +154,7 @@ module ApplicationHelper end def follow_button(followable, style = "full") - tag :button, + tag :button, #Yikes class: "cta follow-action-button", data: { info: { id: followable.id, className: followable.class.name, style: style }.to_json, diff --git a/app/liquid_tags/user_tag.rb b/app/liquid_tags/user_tag.rb index a21b58ef7..34964f2bc 100644 --- a/app/liquid_tags/user_tag.rb +++ b/app/liquid_tags/user_tag.rb @@ -1,5 +1,6 @@ class UserTag < LiquidTagBase include ApplicationHelper + include ActionView::Helpers::TagHelper attr_reader :user def initialize(_tag_name, user, _tokens) diff --git a/app/models/html_variant.rb b/app/models/html_variant.rb new file mode 100644 index 000000000..86f6134e9 --- /dev/null +++ b/app/models/html_variant.rb @@ -0,0 +1,40 @@ +class HtmlVariant < ApplicationRecord + validates :html, presence: true + validates :name, uniqueness: true + validates :group, inclusion: { in: %w(article_show_sidebar_cta) } + validates :success_rate, presence: true + validate :no_edits + belongs_to :user, optional: true + has_many :html_variant_trials + has_many :html_variant_successes + + def calculate_success_rate! + self.success_rate = html_variant_successes.size.to_f / (html_variant_trials.size * 10.0) # x10 because we only capture every 10th + save! + end + + def self.find_for_test(tags = []) + tags_array = tags + ["", nil] + if rand(10) == 1 # 10% return completely random + find_random_for_test(tags_array) + else # 90% chance return one in top 10 + find_top_for_test(tags_array) + end + end + + def self.find_top_for_test(tags_array) + where(group: "article_show_sidebar_cta", approved: true, published: true, target_tag: tags_array).order("success_rate DESC").limit(rand(1..10)).sample + end + + def self.find_random_for_test(tags_array) + where(group: "article_show_sidebar_cta", approved: true, published: true, target_tag: tags_array).order("RANDOM()").first + end + + private + + def no_edits + if (approved && html_changed? || name_changed? || group_changed?) && persisted? + errors.add(:base, "cannot change once published and approved") + end + end +end diff --git a/app/models/html_variant_success.rb b/app/models/html_variant_success.rb new file mode 100644 index 000000000..ef2d2d4aa --- /dev/null +++ b/app/models/html_variant_success.rb @@ -0,0 +1,5 @@ +class HtmlVariantSuccess < ApplicationRecord + validates :html_variant_id, presence: true + belongs_to :html_variant + belongs_to :article, optional: true +end diff --git a/app/models/html_variant_trial.rb b/app/models/html_variant_trial.rb new file mode 100644 index 000000000..3502bfcf5 --- /dev/null +++ b/app/models/html_variant_trial.rb @@ -0,0 +1,5 @@ +class HtmlVariantTrial < ApplicationRecord + validates :html_variant_id, presence: true + belongs_to :html_variant + belongs_to :article, optional: true +end diff --git a/app/models/organization.rb b/app/models/organization.rb index 227601dc8..11bf5ed09 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -36,7 +36,7 @@ class Organization < ApplicationRecord validates :cta_button_url, url: { allow_blank: true, no_local: true, schemes: ["https", "http"] }, if: :approved validates :cta_button_text, length: { maximum: 12 } - validates :cta_body_markdown, length: { maximum: 140 } + validates :cta_body_markdown, length: { maximum: 256 } before_save :remove_at_from_usernames after_save :bust_cache before_save :generate_secret diff --git a/app/models/user.rb b/app/models/user.rb index 2e3c0f988..7e4e9f81f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -32,6 +32,7 @@ class User < ApplicationRecord has_many :chat_channels, through: :chat_channel_memberships has_many :push_notification_subscriptions, dependent: :destroy has_many :feedback_messages + has_many :html_variants, dependent: :destroy has_many :mentor_relationships_as_mentee, class_name: "MentorRelationship", foreign_key: "mentee_id" has_many :mentor_relationships_as_mentor, diff --git a/app/policies/html_variant_policy.rb b/app/policies/html_variant_policy.rb new file mode 100644 index 000000000..d7d705087 --- /dev/null +++ b/app/policies/html_variant_policy.rb @@ -0,0 +1,33 @@ +class HtmlVariantPolicy < ApplicationPolicy + def index? + user_admin? + end + + def show? + user_admin? + end + + def edit? + user_admin? + end + + def update? + user_admin? + end + + def new? + user_admin? + end + + def create? + user_admin? + end + + def destroy? + user_admin? + end + + def permitted_attributes + %i[html name published approved target_tag] + end +end diff --git a/app/views/articles/_sticky_nav.html.erb b/app/views/articles/_sticky_nav.html.erb index 92254b129..ca538ba55 100644 --- a/app/views/articles/_sticky_nav.html.erb +++ b/app/views/articles/_sticky_nav.html.erb @@ -1,6 +1,8 @@ -<% @sticky_collection = StickyArticleCollection.new(@article, @organization || @user) %> -<% @sticky_articles = @sticky_collection.suggested_stickies %> -<% @user_stickies = @sticky_collection.user_stickies %> +<% if user_signed_in? %> + <% @sticky_collection = StickyArticleCollection.new(@article, @organization || @user) %> + <% @sticky_articles = @sticky_collection.suggested_stickies %> + <% @user_stickies = @sticky_collection.user_stickies %> +<% end %> <% @actor = @article.organization || @article.user %> -
+
@@ -38,61 +40,76 @@
<% if @actor.tag_line.present? %> <%= truncate (@actor.tag_line || @actor.summary || "Posts in this tag"), length: 200 %> - <% else %> - Member since <%= @actor.created_at.strftime("%b %e, %Y") %> <% end %>
<%= follow_button(@actor) %>
-
- <% if @actor.class.name == "Organization" && @actor.approved_and_filled_out_cta? %> -
- - + <% if @actor.class.name == "User" %> + <%= render "articles/user_metadata" %> + <% elsif @actor.class.name == "Organization" && @actor.approved_and_filled_out_cta? %>
<%= @actor.cta_processed_html.html_safe %> - + " class="primary-sticky-nav-org-cta-link"> + <%= @actor.cta_button_text.upcase %> + <%= Addressable::URI.parse(@actor.cta_button_url).domain %> +
+ <% end %> +
+ <% if params[:html_variant_id] %> + <% @html_variant = HtmlVariant.where(group: "article_show_sidebar_cta", id: params[:html_variant_id].to_i ).first %> + <% else %> + <% @html_variant = HtmlVariant.find_for_test(@article.cached_tag_list_array) %> + <% end %> + <% if ( params[:html_variant_id] || !user_signed_in? ) && @html_variant %> +
+ <%= @html_variant.html.html_safe %>
<% end %> - <% if @user_stickies && @user_stickies.any? %> -
- <% if @actor.username.size < 18 %> - More from @<%= @actor.username %> - <% else %> - More from
@<%= @actor.username %> - <% end %> -
- <% @user_stickies.each do |article| %> - - - <%= article.title %> -
- <% article.decorate.cached_tag_list_array.each do |tag| %> - #<%= tag %> - <% end %> -
-
+ <% if user_signed_in? && @user_stickies && @user_stickies.any? %> + <% @user_stickies.each_with_index do |article, index| %> +
+ <% if index == 0 %> +
+ <% if @actor.username.size < 18 %> + More from @<%= @actor.username %> + <% else %> + More from
@<%= @actor.username %> + <% end %> +
+ <% end %> + + + <%= article.title %> +
+ <% article.decorate.cached_tag_list_array.each do |tag| %> + #<%= tag %> + <% end %> +
+
+
<% end %> <% end %> - <% if @sticky_articles %> -
- "/> Trending on dev.to -
- <% @sticky_articles.each do |article| %> - - - <%= article.title %> -
- <% article.decorate.cached_tag_list_array.each do |tag| %> - #<%= tag %> - <% end %> -
-
+ <% if user_signed_in? && @sticky_articles %> + <% @sticky_articles.each_with_index do |article, index| %> + <% end %> <% end %>
diff --git a/app/views/articles/edit.html.erb b/app/views/articles/edit.html.erb index 5e026508c..9d735708b 100644 --- a/app/views/articles/edit.html.erb +++ b/app/views/articles/edit.html.erb @@ -1,4 +1,4 @@ -<% title "Edit Article - DEV" %> +<% title "Edit Post" %> <% if @article.video.present? %>
@@ -9,7 +9,7 @@

You'll get an email when this process finishes. -

+

This is an ordinary unpublished post. Add title/tags/body/etc, and then mark as published after the video is done processing.

diff --git a/app/views/articles/show.html.erb b/app/views/articles/show.html.erb index 4f19ce35b..38ff1a1d6 100644 --- a/app/views/articles/show.html.erb +++ b/app/views/articles/show.html.erb @@ -216,7 +216,7 @@
-<% cache("sticky-sidebar-#{@article.id}-#{@article.edited_at}", :expires_in => 8.hours) do %> +<% cache("sticky-sidebar-#{@article.id}-#{@article.edited_at}-#{(@organization || @user).updated_at}", :expires_in => 8.hours) do %> <%= render "articles/sticky_nav" %> <% end %> @@ -238,4 +238,4 @@ <%= GistTag.script.html_safe %> <%= RunkitTag.script.html_safe %> -<% end %> +<% end %> \ No newline at end of file diff --git a/app/views/html_variants/_form.html.erb b/app/views/html_variants/_form.html.erb new file mode 100644 index 000000000..e4d2312ca --- /dev/null +++ b/app/views/html_variants/_form.html.erb @@ -0,0 +1,26 @@ +<% if @html_variant.errors.any? %> +
+

<%= pluralize(@html_variant.errors.count, "error") %> prohibited this block from being saved:

+ + +
+<% end %> + + +<%= form_for(@html_variant) do |f| %> + <% if @html_variant.new_record? %> + <%= f.label :name %> + <%= f.text_field :name, placeholder: 'Unique, descriptive name' %> + <% end %> + <%= f.label :html %> + <%= f.text_area :html, placeholder: 'HTML to be shown. Make sure all CSS is properly scoped, and all HTML tags are closed, etc. Manditory field.' %> + <%= f.label :published %> + <%= f.check_box :published %> + <%= f.label :target_tag %> + <%= f.text_field :target_tag, placeholder: 'One tag, e.g. javascript. Optional targeting.' %> + <%= f.submit %> +<% end %> \ No newline at end of file diff --git a/app/views/html_variants/_page_styles.html.erb b/app/views/html_variants/_page_styles.html.erb new file mode 100644 index 000000000..f3ee523e4 --- /dev/null +++ b/app/views/html_variants/_page_styles.html.erb @@ -0,0 +1,71 @@ + \ No newline at end of file diff --git a/app/views/html_variants/_single_html_variant.html.erb b/app/views/html_variants/_single_html_variant.html.erb new file mode 100644 index 000000000..b040d2c68 --- /dev/null +++ b/app/views/html_variants/_single_html_variant.html.erb @@ -0,0 +1,23 @@ +
+

+ <%= html_variant.name %> +

+ <%= html_variant.html %> +
+ <% if html_variant.published %> + published + fork + <% else %> + edit + <% end %> + preview + <% if html_variant.approved %> + approved + <% elsif admin %> + <%= form_for(html_variant) do |f| %> + <%= f.hidden_field :approved, value: "true" %> + <%= f.submit "APPROVE" %> + <% end %> + <% end %> +
+
diff --git a/app/views/html_variants/edit.html.erb b/app/views/html_variants/edit.html.erb new file mode 100644 index 000000000..8d7f70d63 --- /dev/null +++ b/app/views/html_variants/edit.html.erb @@ -0,0 +1,6 @@ +<%= render "page_styles" %> + +
+

<%= @html_variant.name %>

+ <%= render "form" %> +
diff --git a/app/views/html_variants/index.html.erb b/app/views/html_variants/index.html.erb new file mode 100644 index 000000000..ca7465f2e --- /dev/null +++ b/app/views/html_variants/index.html.erb @@ -0,0 +1,15 @@ +<%= render "page_styles" %> + +
+

+ HTML Variants New +

+

Your HTML Variants

+ <% @user_variants.each do |html_variant| %> + <%= render "single_html_variant", html_variant: html_variant, admin: false %> + <% end %> +

All Published

+ <% @all_published_variants.each do |html_variant| %> + <%= render "single_html_variant", html_variant: html_variant, admin: true %> + <% end %> +
diff --git a/app/views/html_variants/new.html.erb b/app/views/html_variants/new.html.erb new file mode 100644 index 000000000..e12a44994 --- /dev/null +++ b/app/views/html_variants/new.html.erb @@ -0,0 +1,5 @@ +<%= render "page_styles" %> + +
+ <%= render "form" %> +
diff --git a/app/views/users/_org_admin.html.erb b/app/views/users/_org_admin.html.erb index 24d338427..73d10ccda 100644 --- a/app/views/users/_org_admin.html.erb +++ b/app/views/users/_org_admin.html.erb @@ -113,38 +113,19 @@ <%= f.text_area :tech_stack, maxlength: 640, placeholder: "Limit of 640 characters" %>
<% if @organization.approved %> -

Call-to-action box

+

Post Call-to-action box

- <%= f.label "Button Text" %> + <%= f.label "CTA Link Text" %> <%= f.text_field :cta_button_text, maxlength: 12, placeholder: "Limit of 12 characters" %>
- <%= f.label "Button Link" %> + <%= f.label "CTA Link URL" %> <%= f.text_field :cta_button_url %>
- <%= f.label "Body text" %> - <%= f.text_area :cta_body_markdown, maxlength: 140, placeholder: "Limit of 140 characters" %> + <%= f.label "Body text (markdown inline only)" %> + <%= f.text_area :cta_body_markdown, maxlength: 256, placeholder: "Limit of 256 characters" %>
- <% if @organization.approved_and_filled_out_cta? %> -
- Preview of Widget: -
-
-
-
- - <%# @organization.cta_body_markdown %> - <%= @organization.cta_processed_html.html_safe %> - -
-
-
-
-
- <% end %> <% end %>
diff --git a/config/routes.rb b/config/routes.rb index 7bd14f2b4..0d25ac310 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -113,6 +113,9 @@ Rails.application.routes.draw do resources :videos, only: %i[create new] resources :video_states, only: [:create] resources :twilio_tokens, only: [:show] + resources :html_variants + resources :html_variant_trials, only: [:create] + resources :html_variant_successes, only: [:create] resources :push_notification_subscriptions, only: [:create] get "/notifications/:username" => "notifications#index" diff --git a/db/migrate/20181019195746_create_html_variants.rb b/db/migrate/20181019195746_create_html_variants.rb new file mode 100644 index 000000000..1d3adccc6 --- /dev/null +++ b/db/migrate/20181019195746_create_html_variants.rb @@ -0,0 +1,15 @@ +class CreateHtmlVariants < ActiveRecord::Migration[5.1] + def change + create_table :html_variants do |t| + t.integer :user_id + t.string :group + t.string :name + t.text :html + t.string :target_tag + t.float :success_rate, default: 0.0 + t.boolean :published, default: false + t.boolean :approved, default: false + t.timestamps + end + end +end diff --git a/db/migrate/20181020195949_create_html_variant_trials.rb b/db/migrate/20181020195949_create_html_variant_trials.rb new file mode 100644 index 000000000..da5ac8b36 --- /dev/null +++ b/db/migrate/20181020195949_create_html_variant_trials.rb @@ -0,0 +1,10 @@ +class CreateHtmlVariantTrials < ActiveRecord::Migration[5.1] + def change + create_table :html_variant_trials do |t| + t.integer :html_variant_id + t.integer :article_id + t.timestamps + end + add_index :html_variant_trials, [:html_variant_id, :article_id] + end +end diff --git a/db/migrate/20181020195954_create_html_variant_successes.rb b/db/migrate/20181020195954_create_html_variant_successes.rb new file mode 100644 index 000000000..30fde94ce --- /dev/null +++ b/db/migrate/20181020195954_create_html_variant_successes.rb @@ -0,0 +1,10 @@ +class CreateHtmlVariantSuccesses < ActiveRecord::Migration[5.1] + def change + create_table :html_variant_successes do |t| + t.integer :html_variant_id + t.integer :article_id + t.timestamps + end + add_index :html_variant_successes, [:html_variant_id, :article_id] + end +end diff --git a/db/schema.rb b/db/schema.rb index 51495dcd2..44779ac25 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -12,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181016181008) do +ActiveRecord::Schema.define(version: 20181020195954) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -358,6 +356,35 @@ ActiveRecord::Schema.define(version: 20181016181008) do t.integer "watchers_count" end + create_table "html_variant_successes", force: :cascade do |t| + t.integer "article_id" + t.datetime "created_at", null: false + t.integer "html_variant_id" + t.datetime "updated_at", null: false + t.index ["html_variant_id", "article_id"], name: "index_html_variant_successes_on_html_variant_id_and_article_id" + end + + create_table "html_variant_trials", force: :cascade do |t| + t.integer "article_id" + t.datetime "created_at", null: false + t.integer "html_variant_id" + t.datetime "updated_at", null: false + t.index ["html_variant_id", "article_id"], name: "index_html_variant_trials_on_html_variant_id_and_article_id" + end + + create_table "html_variants", force: :cascade do |t| + t.boolean "approved", default: false + t.datetime "created_at", null: false + t.string "group" + t.text "html" + t.string "name" + t.boolean "published", default: false + t.float "success_rate", default: 0.0 + t.string "target_tag" + t.datetime "updated_at", null: false + t.integer "user_id" + end + create_table "identities", id: :serial, force: :cascade do |t| t.text "auth_data_dump" t.datetime "created_at", null: false diff --git a/lib/tasks/fetch.rake b/lib/tasks/fetch.rake index bbcc04f04..282ca6dd6 100644 --- a/lib/tasks/fetch.rake +++ b/lib/tasks/fetch.rake @@ -91,3 +91,12 @@ task :award_contributor_badges, [:arg1] => :environment do |_t, args| BadgeRewarder.award_contributor_badges(usernames) puts "Done!" end + +task remove_old_html_variant_data: :environment do + HtmlVariantTrial.where("created_at < ?", 1.week.ago).destroy_all + HtmlVariantSuccess.where("created_at < ?", 1.week.ago).destroy_all + HtmlVariant.find_each do |html_variant| + return if html_variant.html_variant_successes.size < 20 + html_variant.calculate_success_rate! + end +end diff --git a/spec/factories/html_variants.rb b/spec/factories/html_variants.rb new file mode 100644 index 000000000..9d3215d75 --- /dev/null +++ b/spec/factories/html_variants.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :html_variant do + user + name { Faker::Hipster.paragraph(1) } + html { "
#{rand(10000000000)}

HEllo

" } + success_rate { 0.3 } + group { "article_show_sidebar_cta" } + end +end diff --git a/spec/models/html_variant_spec.rb b/spec/models/html_variant_spec.rb new file mode 100644 index 000000000..448cc5e43 --- /dev/null +++ b/spec/models/html_variant_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe HtmlVariant, type: :model do + it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_presence_of(:html) } + it { is_expected.to belong_to(:user) } + + let(:html_variant) { create(:html_variant, approved: true, published: true) } + + it "calculates success rate" do + HtmlVariantTrial.create!(html_variant_id: html_variant.id) + HtmlVariantTrial.create!(html_variant_id: html_variant.id) + HtmlVariantTrial.create!(html_variant_id: html_variant.id) + HtmlVariantTrial.create!(html_variant_id: html_variant.id) + HtmlVariantSuccess.create!(html_variant_id: html_variant.id) + html_variant.calculate_success_rate! + expect(html_variant.success_rate).to eq(0.025) + end + + it "finds for test without tag" do + html_variant.save! + expect(HtmlVariant.find_for_test.id).to eq(html_variant.id) + end + it "finds for test with tag given" do + html_variant.target_tag = "hello" + html_variant.save! + expect(HtmlVariant.find_for_test(["hello"]).id).to eq(html_variant.id) + end + it "does not find if different tag targeted" do + html_variant.target_tag = "different_tag_yolo" + html_variant.save! + expect(HtmlVariant.find_for_test(["hello"])).to eq(nil) + end + it "finds if no tag targeted and tag given" do + html_variant.save! + expect(HtmlVariant.find_for_test(["hello"]).id).to eq(html_variant.id) + end +end diff --git a/spec/models/html_variant_success_spec.rb b/spec/models/html_variant_success_spec.rb new file mode 100644 index 000000000..b7433582b --- /dev/null +++ b/spec/models/html_variant_success_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe HtmlVariantSuccess, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/html_variant_trial_spec.rb b/spec/models/html_variant_trial_spec.rb new file mode 100644 index 000000000..9b77d92a1 --- /dev/null +++ b/spec/models/html_variant_trial_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe HtmlVariantTrial, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/policies/html_variant_policy_spec.rb b/spec/policies/html_variant_policy_spec.rb new file mode 100644 index 000000000..15be28800 --- /dev/null +++ b/spec/policies/html_variant_policy_spec.rb @@ -0,0 +1,18 @@ +require "rails_helper" + +RSpec.describe HtmlVariantPolicy do + subject { described_class.new(user, html_variant) } + + context "when user is not an admin" do + let(:user) { build(:user) } + let(:html_variant) { build(:html_variant) } + + it { is_expected.to forbid_actions(%i[index show edit update create]) } + end + + context "when user is an admin" do + let(:user) { build(:user, :super_admin) } + let(:html_variant) { build(:html_variant) } + it { is_expected.to permit_actions(%i[index show edit update create]) } + end +end diff --git a/spec/requests/html_variant_successes_spec.rb b/spec/requests/html_variant_successes_spec.rb new file mode 100644 index 000000000..af3e61f32 --- /dev/null +++ b/spec/requests/html_variant_successes_spec.rb @@ -0,0 +1,25 @@ +require "rails_helper" + +RSpec.describe "HtmlVariantSuccesses", type: :request do + let(:user) { create(:user) } + let(:article) { create(:article, user_id: user.id, approved: true) } + let(:html_variant) { create(:html_variant) } + + before do + Delayed::Worker.delay_jobs = false + end + + after do + Delayed::Worker.delay_jobs = true + end + + describe "POST /html_variant_successes" do + it "rejects non-permissioned user" do + post "/html_variant_successes", params: { + article_id: article.id, + html_variant_id: html_variant.id, + } + expect(HtmlVariantSuccess.all.size).to eq(1) + end + end +end diff --git a/spec/requests/html_variant_trials_spec.rb b/spec/requests/html_variant_trials_spec.rb new file mode 100644 index 000000000..6ba711c39 --- /dev/null +++ b/spec/requests/html_variant_trials_spec.rb @@ -0,0 +1,25 @@ +require "rails_helper" + +RSpec.describe "HtmlVariantTrials", type: :request do + let(:user) { create(:user) } + let(:article) { create(:article, user_id: user.id, approved: true) } + let(:html_variant) { create(:html_variant) } + + before do + Delayed::Worker.delay_jobs = false + end + + after do + Delayed::Worker.delay_jobs = true + end + + describe "POST /html_variant_trials" do + it "rejects non-permissioned user" do + post "/html_variant_trials", params: { + article_id: article.id, + html_variant_id: html_variant.id, + } + expect(HtmlVariantTrial.all.size).to eq(1) + end + end +end diff --git a/spec/requests/html_variants_spec.rb b/spec/requests/html_variants_spec.rb new file mode 100644 index 000000000..bd09fb9bc --- /dev/null +++ b/spec/requests/html_variants_spec.rb @@ -0,0 +1,110 @@ +require "rails_helper" + +RSpec.describe "HtmlVariants", type: :request do + let(:user) { create(:user)} + let(:article) { create(:article, user_id: user.id, approved: true) } + + before do + sign_in user + end + + describe "GET /html_variants" do + it "rejects non-permissioned user" do + expect { get "/html_variants" }.to raise_error(Pundit::NotAuthorizedError) + end + + it "accepts permissioned" do + user.add_role(:super_admin) + get "/html_variants" + expect(response.body).to include("HTML Variants") + end + end + + describe "GET /html_variants/new" do + it "rejects non-permissioned user" do + expect { get "/html_variants/new" }.to raise_error(Pundit::NotAuthorizedError) + end + + it "accepts permissioned" do + user.add_role(:super_admin) + get "/html_variants/new" + expect(response.body).to include("