Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ group :test do
gem "capybara"
gem "selenium-webdriver"
end

gem "faraday-retry", "~> 2.3"
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ GEM
logger
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
faraday-retry (2.3.1)
faraday (~> 2.0)
ffi (1.17.1-x86_64-linux-gnu)
globalid (1.2.1)
activesupport (>= 6.1)
Expand Down Expand Up @@ -369,6 +371,7 @@ DEPENDENCIES
devise (~> 4.9)
dotenv-rails
faker (~> 3.5, >= 3.5.1)
faraday-retry (~> 2.3)
importmap-rails
jbuilder
jquery-rails
Expand Down
7 changes: 4 additions & 3 deletions app/assets/config/manifest.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//= link_tree ../images
//= link_tree ../builds
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js
//= link_directory ../stylesheets .scss
//= link application.css
// = link jquery.min.js
//= link jquery.min.js
//= link application.scss
//= link badge/style.scss
//= link nav.scss
5 changes: 3 additions & 2 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "bootstrap";

@import "../node_modules/bootstrap/scss/bootstrap";
@import "typography/icons";
@import "global";
@import "badge/style";
@import "badge/nav";
44 changes: 44 additions & 0 deletions app/assets/stylesheets/badge/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.earned-badge {
position: relative;
border: 2px solid #FFD700;
transition: all 0.3s ease;
}

.earned-badge:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(255, 215, 0, 0.2);
}

.earned-ribbon {
position: absolute;
top: 10px;
right: -10px;
background: #FFD700;
color: #000;
padding: 3px 15px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
z-index: 1;
}

.earned-ribbon:before {
content: '';
position: absolute;
bottom: -10px;
right: 0;
border-left: 10px solid transparent;
border-right: 10px solid #c90;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
}

.badge-icon {
max-height: 100px;
filter: drop-shadow(0 2px 5px rgba(0,0,0,0.2));
}

.grayscale {
filter: grayscale(100%) brightness(0.7);
}
30 changes: 30 additions & 0 deletions app/assets/stylesheets/nav.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.navbar {
padding: 0.75rem 1rem;
transition: all 0.3s ease;
}

.navbar-brand {
font-size: 1.25rem;
transition: all 0.3s ease;
}

.nav-link {
position: relative;
padding: 0.5rem 1rem;
}

.nav-link:after {
content: '';
position: absolute;
bottom: 0;
left: 1rem;
right: 1rem;
height: 2px;
background: rgba(255, 255, 255, 0.5);
transform: scaleX(0);
transition: transform 0.3s ease;
}

.nav-link:hover:after {
transform: scaleX(1);
}
53 changes: 53 additions & 0 deletions app/controllers/admin/badges_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
class Admin::BadgesController < ApplicationController
before_action :set_badge, only: %i[show edit update destroy]

def index
@badges = Badge.all
end

def show; end

def new
@badge = Badge.new
@rules = Badge.rules_with_descriptions
end

def edit
@rules = Badge.rules_with_descriptions
end

def create
@badge = Badge.new(badge_params)

if @badge.save
redirect_to admin_badges_path, notice: t("admin.badge.create.success")
else
@rules = Badge.rules_with_descriptions
render :new
end
end

def update
if @badge.update(badge_params)
redirect_to admin_badges_path, notice: t("admin.badge.update.success")
else
@rules = Badge.rules_with_descriptions
render :edit
end
end

def destroy
@badge.destroy
redirect_to admin_badges_path, notice: t("admin.badge.delete.success")
end

private

def set_badge
@badge = Badge.find(params[:id])
end

def badge_params
params.require(:badge).permit(:title, :text, :rule, :image_url)
end
end
8 changes: 8 additions & 0 deletions app/controllers/badges_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class BadgesController < ApplicationController
before_action :authenticate_user!

def index
@all_badges = Badge.all
@my_badge_ids = current_user.badges.pluck(:id)
end
end
25 changes: 25 additions & 0 deletions app/controllers/feedbacks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class FeedbacksController < ApplicationController
before_action :authenticate_user!
def new
@feedback = Feedback.new
end

def create
@feedback = Feedback.new(feedback_params)
@feedback.email = current_user.email
@feedback.author = current_user

if @feedback.save
FeedbackMailer.feedback_email(@feedback).deliver_now
redirect_to root_path, notice: t(".success")
else
render :new
end
end

private

def feedback_params
params.require(:feedback).permit(:name, :message)
end
end
11 changes: 8 additions & 3 deletions app/controllers/test_passages_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
class TestPassagesController < ApplicationController

before_action :authenticate_user!
before_action :set_test_passage, only: %i[ show result update ]

Expand All @@ -8,7 +7,7 @@ def show; end
def result; end

def update
if @test_passage.question_any?(params)
if @test_passage.question_any?(params)
@test_passage.accept!(params[:answer_ids])

completed_test
Expand All @@ -25,9 +24,15 @@ def set_test_passage

def completed_test
if @test_passage.completed?

TestMailer.completed_test(@test_passage).deliver_now

new_badges = BadgeAwardService.new(@test_passage).call

if new_badges.any?
flash[:notice] = t("test_passages.badge", name_badge: new_badges.map(&:title).join(", "))
end

redirect_to result_test_passage_path(@test_passage)
else
redirect_to test_passage_path(@test_passage)
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/admin/badges_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module Admin::BadgesHelper
end
2 changes: 2 additions & 0 deletions app/helpers/badges_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module BadgesHelper
end
2 changes: 2 additions & 0 deletions app/helpers/feedbacks_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module FeedbacksHelper
end
6 changes: 4 additions & 2 deletions app/javascript/utilities/progress_bar.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
document.addEventListener('turbo:load', function() {
if (document.URL.includes('/test_passages/')) {
const testPassageUrlPattern = /\/test_passages\/\d+$/;

if (testPassageUrlPattern.test(document.URL)) {
const progress_bars = document.querySelector('.progress-bar');

const totalQuestion = progress_bars.dataset.totalQuestion
let currentQuestion = progress_bars.dataset.currentQuestion

let progress_test = (100 / totalQuestion) * (currentQuestion - 1)
progress_bars.style = "width: " + progress_test + "%"
progress_bars.ariaValuenow = progress_test
progress_bars.setAttribute('aria-valuenow', progress_test)
}
})
4 changes: 2 additions & 2 deletions app/mailers/application_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: %{"TestGuru" <mail@testguru.com>}
layout 'mailer'
default from: "TestGuru <#{ENV.fetch('SMTP_USERNAME', nil)}>"
layout "mailer"
end
11 changes: 11 additions & 0 deletions app/mailers/feedback_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class FeedbackMailer < ApplicationMailer
default from: -> { ENV.fetch("SMTP_USERNAME") || "feedback@testguru.com" }

def feedback_email(feedback)
@feedback = feedback
mail(
to: ENV.fetch("EMAIL_TO_ADMIN", "admin@testguru.com"),
subject: "New feedback from #{feedback.name}"
)
end
end
19 changes: 19 additions & 0 deletions app/models/badge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class Badge < ApplicationRecord
has_many :badge_users, dependent: :destroy
has_many :users, through: :badge_users

validates :title, :text, :rule, :image_url, presence: true

def self.available_rules
BadgeAwardService::RULE_CLASSES.keys.map(&:to_s)
end

def self.rules_with_descriptions
{
all_backend_tests: I18n.t("activerecord.badge.all_backend_tests"),
first_attempt: I18n.t("activerecord.badge.first_attempt"),
all_frontend_tests: I18n.t("activerecord.badge.all_frontend_tests"),
all_tests_of_1_level: I18n.t("activerecord.badge.all_tests_of_1_level")
}
end
end
4 changes: 4 additions & 0 deletions app/models/badge_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class BadgeUser < ApplicationRecord
belongs_to :user
belongs_to :badge
end
7 changes: 7 additions & 0 deletions app/models/feedback.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Feedback < ApplicationRecord
validates :name, presence: true, length: { minimum: 2 }
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :message, presence: true

belongs_to :author, class_name: "User", foreign_key: "user_id"
end
10 changes: 6 additions & 4 deletions app/models/test_passage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ class TestPassage < ApplicationRecord

before_validation :set_current_question

def passed
completed? && test_successful?
end
alias_method :passed?, :passed

def accept!(answer_ids)
if correct_answer?(answer_ids)
self.correct_question += 1
Expand All @@ -16,10 +21,7 @@ def accept!(answer_ids)
end

def completed?
if current_question.nil?
self.current_question = nil
true
end
current_question.nil?
end

def current_question_number
Expand Down
3 changes: 3 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ class User < ApplicationRecord
has_many :test_passages, dependent: :delete_all
has_many :tests, through: :test_passages
has_many :created_tests, class_name: "Test", foreign_key: "author_id"
has_many :feedbacks, dependent: :delete_all

validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false }

has_many :badge_users, dependent: :destroy
has_many :badges, through: :badge_users

def tests_by_level(level)
tests.by_level(level)
Expand Down
39 changes: 39 additions & 0 deletions app/services/badge_award_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class BadgeAwardService
RULE_CLASSES = {
all_backend_tests: "Badges::AllBackendTests",
first_attempt: "Badges::FirstAttempt",
all_frontend_tests: "Badges::AllFrontendTests",
all_tests_of_1_level: "Badges::AllTestsOf1Level"
}.freeze

def initialize(test_passage)
@test_passage = test_passage
@user = test_passage.user
@new_badges = []
end

def call
Badge.where.not(id: @user.badge_ids).each do |badge|
rule_class = self.class.rule_class_for(badge.rule)
next unless rule_class

if rule_class.reward?(@user, @test_passage)
award_badge(badge)
end
end
@new_badges
end

def self.rule_class_for(rule_name)
RULE_CLASSES[rule_name.to_sym]&.constantize
end

private

def award_badge(badge)
unless @user.badges.include?(badge)
@user.badges << badge
@new_badges << badge
end
end
end
Loading
Loading