From 0665f8a9b8915c8caea9d8f38b8efc6bcf7dd42c Mon Sep 17 00:00:00 2001 From: "Jeferson S. Brito" Date: Sun, 28 Jan 2024 14:33:58 -0300 Subject: [PATCH 01/10] feat: add whenever gem --- api/Gemfile | 2 ++ api/Gemfile.lock | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/api/Gemfile b/api/Gemfile index 8e4d381..5e6b1d4 100644 --- a/api/Gemfile +++ b/api/Gemfile @@ -61,3 +61,5 @@ gem 'jwt', '~> 2.7' gem 'rmagick', '~> 5.3' gem 'pg', '~> 1.1' + +gem "whenever", "~> 1.0" diff --git a/api/Gemfile.lock b/api/Gemfile.lock index 524badc..f353a67 100644 --- a/api/Gemfile.lock +++ b/api/Gemfile.lock @@ -71,6 +71,7 @@ GEM bootsnap (1.16.0) msgpack (~> 1.2) builder (3.2.4) + chronic (0.10.2) concurrent-ruby (1.2.2) crass (1.0.6) date (3.3.3) @@ -228,6 +229,8 @@ GEM websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + whenever (1.0.0) + chronic (>= 0.6.3) zeitwerk (2.6.7) PLATFORMS @@ -252,6 +255,7 @@ DEPENDENCIES shoulda-matchers (~> 5.1) sqlite3 (~> 1.4) tzinfo-data + whenever (~> 1.0) RUBY VERSION ruby 3.2.0p0 From 83120ac20f39b8274423e392028db658861eac05 Mon Sep 17 00:00:00 2001 From: "Jeferson S. Brito" Date: Sun, 28 Jan 2024 14:34:36 -0300 Subject: [PATCH 02/10] feat: add notification mailer --- api/app/mailers/notification_mailer.rb | 9 +++++++++ .../notification_mailer/review_email.html.erb | 14 ++++++++++++++ .../notification_mailer/review_email.text.erb | 7 +++++++ 3 files changed, 30 insertions(+) create mode 100644 api/app/mailers/notification_mailer.rb create mode 100644 api/app/views/notification_mailer/review_email.html.erb create mode 100644 api/app/views/notification_mailer/review_email.text.erb diff --git a/api/app/mailers/notification_mailer.rb b/api/app/mailers/notification_mailer.rb new file mode 100644 index 0000000..387a537 --- /dev/null +++ b/api/app/mailers/notification_mailer.rb @@ -0,0 +1,9 @@ +class NotificationMailer < ApplicationMailer + default from: 'notifications@flashmemo.com' + + def review_email + @user = params[:user] + @url = params[:url] + mail(to: @user.email, subject: 'Hey, Review time!') + end +end diff --git a/api/app/views/notification_mailer/review_email.html.erb b/api/app/views/notification_mailer/review_email.html.erb new file mode 100644 index 0000000..03e29cd --- /dev/null +++ b/api/app/views/notification_mailer/review_email.html.erb @@ -0,0 +1,14 @@ + + + + + + +

Hey <%= @user.username %>

+

+ We bring you these questions to review to reinforce your learning! + Just follow this link: <%= @url %> +

+

Thanks, have a great day!

+ + diff --git a/api/app/views/notification_mailer/review_email.text.erb b/api/app/views/notification_mailer/review_email.text.erb new file mode 100644 index 0000000..4a0bbfc --- /dev/null +++ b/api/app/views/notification_mailer/review_email.text.erb @@ -0,0 +1,7 @@ +Hey <%= @user.username %> +========================== + +We bring you these questions to review to improve your learning! +Just follow this link: <%= @url %> + +Thanks, have a great day! \ No newline at end of file From afc41c64bcf23279d731da95161179a7e044c332 Mon Sep 17 00:00:00 2001 From: "Jeferson S. Brito" Date: Fri, 2 Feb 2024 10:00:27 -0300 Subject: [PATCH 03/10] feat: add rake task and whenever schedule --- api/app/mailers/notification_mailer.rb | 2 +- api/app/models/revision.rb | 8 + .../notification_mailer/review_email.html.erb | 2 +- api/config/schedule.rb | 7 + api/db/schema.rb | 185 +++++++++--------- api/lib/tasks/send_review_email.rake | 8 + .../fixtures/notification_mailer/review_email | 7 + api/spec/mailers/notification_spec.rb | 21 ++ .../mailers/previews/notification_preview.rb | 5 + 9 files changed, 149 insertions(+), 96 deletions(-) create mode 100644 api/config/schedule.rb create mode 100644 api/lib/tasks/send_review_email.rake create mode 100644 api/spec/fixtures/notification_mailer/review_email create mode 100644 api/spec/mailers/notification_spec.rb create mode 100644 api/spec/mailers/previews/notification_preview.rb diff --git a/api/app/mailers/notification_mailer.rb b/api/app/mailers/notification_mailer.rb index 387a537..62ba974 100644 --- a/api/app/mailers/notification_mailer.rb +++ b/api/app/mailers/notification_mailer.rb @@ -1,5 +1,5 @@ class NotificationMailer < ApplicationMailer - default from: 'notifications@flashmemo.com' + default from: 'noreply@flashmemo.com' def review_email @user = params[:user] diff --git a/api/app/models/revision.rb b/api/app/models/revision.rb index 57c4295..4b614a2 100644 --- a/api/app/models/revision.rb +++ b/api/app/models/revision.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Revision < ApplicationRecord + after_create :schedule_review belongs_to :exam belongs_to :user @@ -8,4 +9,11 @@ class Revision < ApplicationRecord validates :user, presence: true validates :exam, presence: true + + def schedule_review + NotificationMailer + .with(user: User.find(self.user_id), url: "/api/revisions/#{self.id}") + .review_email + .deliver_later(wait_until: 3.day.from_now) + end end diff --git a/api/app/views/notification_mailer/review_email.html.erb b/api/app/views/notification_mailer/review_email.html.erb index 03e29cd..77d5052 100644 --- a/api/app/views/notification_mailer/review_email.html.erb +++ b/api/app/views/notification_mailer/review_email.html.erb @@ -7,7 +7,7 @@

Hey <%= @user.username %>

We bring you these questions to review to reinforce your learning! - Just follow this link: <%= @url %> + Just follow target="_blank" rel="noopener">this link

Thanks, have a great day!

diff --git a/api/config/schedule.rb b/api/config/schedule.rb new file mode 100644 index 0000000..ffb83dc --- /dev/null +++ b/api/config/schedule.rb @@ -0,0 +1,7 @@ +every 1.week do + rake 'send_review_email:all' +end + +every :month do + rake 'send_review_email:all' +end \ No newline at end of file diff --git a/api/db/schema.rb b/api/db/schema.rb index 3ae5dfc..eb58734 100644 --- a/api/db/schema.rb +++ b/api/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,120 +10,119 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 20_240_106_180_026) do +ActiveRecord::Schema[7.0].define(version: 2024_01_06_180026) do # These are extensions that must be enabled in order to support this database - enable_extension 'plpgsql' + enable_extension "plpgsql" - create_table 'active_storage_attachments', force: :cascade do |t| - t.string 'name', null: false - t.string 'record_type', null: false - t.bigint 'record_id', null: false - t.bigint 'blob_id', null: false - t.datetime 'created_at', null: false - t.index ['blob_id'], name: 'index_active_storage_attachments_on_blob_id' - t.index %w[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', - unique: true + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end - create_table 'active_storage_blobs', force: :cascade do |t| - t.string 'key', null: false - t.string 'filename', null: false - t.string 'content_type' - t.text 'metadata' - t.string 'service_name', null: false - t.bigint 'byte_size', null: false - t.string 'checksum' - t.datetime 'created_at', null: false - t.index ['key'], name: 'index_active_storage_blobs_on_key', unique: true + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end - create_table 'active_storage_variant_records', force: :cascade do |t| - t.bigint 'blob_id', null: false - t.string 'variation_digest', null: false - t.index %w[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end - create_table 'answers', force: :cascade do |t| - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.integer 'score' - t.bigint 'user_id' - t.bigint 'exam_id' - t.index ['exam_id'], name: 'index_answers_on_exam_id' - t.index ['user_id'], name: 'index_answers_on_user_id' + create_table "answers", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "score" + t.bigint "user_id" + t.bigint "exam_id" + t.index ["exam_id"], name: "index_answers_on_exam_id" + t.index ["user_id"], name: "index_answers_on_user_id" end - create_table 'categories', force: :cascade do |t| - t.string 'title' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false + create_table "categories", force: :cascade do |t| + t.string "title" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table 'exams', force: :cascade do |t| - t.string 'title' - t.integer 'difficulty', default: 0 - t.integer 'version' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.bigint 'category_id' - t.index ['category_id'], name: 'index_exams_on_category_id' + create_table "exams", force: :cascade do |t| + t.string "title" + t.integer "difficulty", default: 0 + t.integer "version" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "category_id" + t.index ["category_id"], name: "index_exams_on_category_id" end - create_table 'exams_questions', id: false, force: :cascade do |t| - t.bigint 'exam_id' - t.bigint 'question_id' - t.index %w[exam_id question_id], name: 'index_exams_questions_on_exam_id_and_question_id', unique: true - t.index ['exam_id'], name: 'index_exams_questions_on_exam_id' - t.index ['question_id'], name: 'index_exams_questions_on_question_id' + create_table "exams_questions", id: false, force: :cascade do |t| + t.bigint "exam_id" + t.bigint "question_id" + t.index ["exam_id", "question_id"], name: "index_exams_questions_on_exam_id_and_question_id", unique: true + t.index ["exam_id"], name: "index_exams_questions_on_exam_id" + t.index ["question_id"], name: "index_exams_questions_on_question_id" end - create_table 'options', force: :cascade do |t| - t.string 'text' - t.bigint 'question_id', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.boolean 'correct', default: false - t.index ['question_id'], name: 'index_options_on_question_id' + create_table "options", force: :cascade do |t| + t.string "text" + t.bigint "question_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "correct", default: false + t.index ["question_id"], name: "index_options_on_question_id" end - create_table 'questions', force: :cascade do |t| - t.string 'title' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.bigint 'exam_id' - t.bigint 'revision_id' - t.boolean 'has_duo', default: false - t.index ['exam_id'], name: 'index_questions_on_exam_id' - t.index ['revision_id'], name: 'index_questions_on_revision_id' + create_table "questions", force: :cascade do |t| + t.string "title" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "exam_id" + t.bigint "revision_id" + t.boolean "has_duo", default: false + t.index ["exam_id"], name: "index_questions_on_exam_id" + t.index ["revision_id"], name: "index_questions_on_revision_id" end - create_table 'revisions', force: :cascade do |t| - t.bigint 'exam_id', null: false - t.bigint 'user_id', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['exam_id'], name: 'index_revisions_on_exam_id' - t.index ['user_id'], name: 'index_revisions_on_user_id' + create_table "revisions", force: :cascade do |t| + t.bigint "exam_id", null: false + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["exam_id"], name: "index_revisions_on_exam_id" + t.index ["user_id"], name: "index_revisions_on_user_id" end - create_table 'users', force: :cascade do |t| - t.string 'username' - t.string 'email' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'password_digest' + create_table "users", force: :cascade do |t| + t.string "username" + t.string "email" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "password_digest" end - add_foreign_key 'active_storage_attachments', 'active_storage_blobs', column: 'blob_id' - add_foreign_key 'active_storage_variant_records', 'active_storage_blobs', column: 'blob_id' - add_foreign_key 'answers', 'exams' - add_foreign_key 'answers', 'users' - add_foreign_key 'exams', 'categories' - add_foreign_key 'exams_questions', 'exams' - add_foreign_key 'exams_questions', 'questions' - add_foreign_key 'options', 'questions' - add_foreign_key 'questions', 'exams' - add_foreign_key 'questions', 'revisions' - add_foreign_key 'revisions', 'exams' - add_foreign_key 'revisions', 'users' + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "answers", "exams" + add_foreign_key "answers", "users" + add_foreign_key "exams", "categories" + add_foreign_key "exams_questions", "exams" + add_foreign_key "exams_questions", "questions" + add_foreign_key "options", "questions" + add_foreign_key "questions", "exams" + add_foreign_key "questions", "revisions" + add_foreign_key "revisions", "exams" + add_foreign_key "revisions", "users" end diff --git a/api/lib/tasks/send_review_email.rake b/api/lib/tasks/send_review_email.rake new file mode 100644 index 0000000..92d9e56 --- /dev/null +++ b/api/lib/tasks/send_review_email.rake @@ -0,0 +1,8 @@ +namespace :send_review_email do + desc 'Send review email to answer revision' + task :all => :environment do + Revision.find_each do |rev| + NotificationMailer.with(user: rev.user_id, url: "/api/revisions/#{rev.last.id}").review_email.deliver_now + end + end +end diff --git a/api/spec/fixtures/notification_mailer/review_email b/api/spec/fixtures/notification_mailer/review_email new file mode 100644 index 0000000..b736fae --- /dev/null +++ b/api/spec/fixtures/notification_mailer/review_email @@ -0,0 +1,7 @@ +Hey Jeferson +========================== + +We bring you these questions to review to improve your learning! +Just follow this link: http://localhost/api/revisions/10 + +Thanks, have a great day! \ No newline at end of file diff --git a/api/spec/mailers/notification_spec.rb b/api/spec/mailers/notification_spec.rb new file mode 100644 index 0000000..637d430 --- /dev/null +++ b/api/spec/mailers/notification_spec.rb @@ -0,0 +1,21 @@ +require "rails_helper" + +RSpec.describe NotificationMailer, type: :mailer do + describe 'review' do + let(:user) { create(:user) } + let(:mail) { NotificationMailer.with(user: user, url: '/api/revisions/1').review_email } + + it 'renders the subject' do + expect(mail.subject).to eq('Hey, Review time!') + end + + it 'renders the receiver email' do + puts mail.body.encoded + expect(mail.to).to eq([user.email]) + end + + it 'renders the sender email' do + expect(mail.from).to eq(['noreply@flashmemo.com']) + end + end +end diff --git a/api/spec/mailers/previews/notification_preview.rb b/api/spec/mailers/previews/notification_preview.rb new file mode 100644 index 0000000..b00e935 --- /dev/null +++ b/api/spec/mailers/previews/notification_preview.rb @@ -0,0 +1,5 @@ +class NotificationPreview < ActionMailer::Preview + def review_email + NotificationMailer.with(user: User.first, url: 'http://localhost:3000/api/revisions/1.json').review_email + end +end From 8228a10b70f0fce2ae4d62a2b82b5f182829d43d Mon Sep 17 00:00:00 2001 From: "Jeferson S. Brito" Date: Sat, 3 Feb 2024 16:30:05 -0300 Subject: [PATCH 04/10] fix: whenever schedule --- api/Gemfile | 3 +-- api/app/mailers/notification_mailer.rb | 2 ++ api/app/models/revision.rb | 2 +- api/config/schedule.rb | 12 +++++++++++- api/lib/tasks/send_review_email.rake | 7 ++++++- api/spec/mailers/notification_spec.rb | 6 ++++-- api/spec/mailers/previews/notification_preview.rb | 2 ++ 7 files changed, 27 insertions(+), 7 deletions(-) diff --git a/api/Gemfile b/api/Gemfile index 5e6b1d4..99a055a 100644 --- a/api/Gemfile +++ b/api/Gemfile @@ -47,6 +47,7 @@ group :development, :test do gem 'rubocop-rails', '~> 2.19', require: false gem 'rubocop-rspec', '~> 2.22', require: false gem 'shoulda-matchers', '~> 5.1' + gem 'whenever', '~> 1.0', require: false end group :development do @@ -61,5 +62,3 @@ gem 'jwt', '~> 2.7' gem 'rmagick', '~> 5.3' gem 'pg', '~> 1.1' - -gem "whenever", "~> 1.0" diff --git a/api/app/mailers/notification_mailer.rb b/api/app/mailers/notification_mailer.rb index 62ba974..13f4934 100644 --- a/api/app/mailers/notification_mailer.rb +++ b/api/app/mailers/notification_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NotificationMailer < ApplicationMailer default from: 'noreply@flashmemo.com' diff --git a/api/app/models/revision.rb b/api/app/models/revision.rb index 4b614a2..88e20fa 100644 --- a/api/app/models/revision.rb +++ b/api/app/models/revision.rb @@ -12,7 +12,7 @@ class Revision < ApplicationRecord def schedule_review NotificationMailer - .with(user: User.find(self.user_id), url: "/api/revisions/#{self.id}") + .with(user: User.find(user_id), url: "/api/revisions/#{id}") .review_email .deliver_later(wait_until: 3.day.from_now) end diff --git a/api/config/schedule.rb b/api/config/schedule.rb index ffb83dc..4bbf79d 100644 --- a/api/config/schedule.rb +++ b/api/config/schedule.rb @@ -1,7 +1,17 @@ +# frozen_string_literal: true + +env :PATH, ENV['PATH'] +set :environment, :development +set :output, "#{path}/log/cron.log" + +every 3.day do + rake 'send_review_email:all' +end + every 1.week do rake 'send_review_email:all' end every :month do rake 'send_review_email:all' -end \ No newline at end of file +end diff --git a/api/lib/tasks/send_review_email.rake b/api/lib/tasks/send_review_email.rake index 92d9e56..5964595 100644 --- a/api/lib/tasks/send_review_email.rake +++ b/api/lib/tasks/send_review_email.rake @@ -2,7 +2,12 @@ namespace :send_review_email do desc 'Send review email to answer revision' task :all => :environment do Revision.find_each do |rev| - NotificationMailer.with(user: rev.user_id, url: "/api/revisions/#{rev.last.id}").review_email.deliver_now + last_ans = rev.exam.answer + if rev.questions.length > 1 && !last_ans + puts "[LOG] #{Time.now}: sending e-mail revision #{rev.id}" + + NotificationMailer.with(user: rev.user_id, url: "/api/revisions/#{rev.last.id}").review_email.deliver_now + end end end end diff --git a/api/spec/mailers/notification_spec.rb b/api/spec/mailers/notification_spec.rb index 637d430..936fc34 100644 --- a/api/spec/mailers/notification_spec.rb +++ b/api/spec/mailers/notification_spec.rb @@ -1,9 +1,11 @@ -require "rails_helper" +# frozen_string_literal: true + +require 'rails_helper' RSpec.describe NotificationMailer, type: :mailer do describe 'review' do let(:user) { create(:user) } - let(:mail) { NotificationMailer.with(user: user, url: '/api/revisions/1').review_email } + let(:mail) { NotificationMailer.with(user:, url: '/api/revisions/1').review_email } it 'renders the subject' do expect(mail.subject).to eq('Hey, Review time!') diff --git a/api/spec/mailers/previews/notification_preview.rb b/api/spec/mailers/previews/notification_preview.rb index b00e935..5045b58 100644 --- a/api/spec/mailers/previews/notification_preview.rb +++ b/api/spec/mailers/previews/notification_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NotificationPreview < ActionMailer::Preview def review_email NotificationMailer.with(user: User.first, url: 'http://localhost:3000/api/revisions/1.json').review_email From b7f00755b9684763e07801b7327265cfa0bd7d3a Mon Sep 17 00:00:00 2001 From: "Jeferson S. Brito" Date: Mon, 12 Feb 2024 17:43:39 -0300 Subject: [PATCH 05/10] feat: add interval level and last attempt to Answers --- api/app/models/answer.rb | 16 ++++++++++++++++ ...240212194401_add_last_attempted_to_answers.rb | 5 +++++ ...240212200940_add_interval_level_to_answers.rb | 7 +++++++ api/db/schema.rb | 4 +++- 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 api/db/migrate/20240212194401_add_last_attempted_to_answers.rb create mode 100644 api/db/migrate/20240212200940_add_interval_level_to_answers.rb diff --git a/api/app/models/answer.rb b/api/app/models/answer.rb index 4fb85cf..02140c0 100644 --- a/api/app/models/answer.rb +++ b/api/app/models/answer.rb @@ -4,7 +4,23 @@ class Answer < ApplicationRecord belongs_to :user belongs_to :exam + INTERVALS = { + 1 => 3.minutes, + 2 => 10.minutes, + 3 => 20.minutes + }.freeze + validates :user, presence: true validates :exam, presence: true validates :score, presence: true + + def attempt + self.last_attempted_at = Time.now + self.interval_level += 1 if score < 100 && interval_level < INTERVALS.keys.max + save + end + + def valid_interval? + last_attempted_at + INTERVALS[self.interval_level] < Time.now + end end diff --git a/api/db/migrate/20240212194401_add_last_attempted_to_answers.rb b/api/db/migrate/20240212194401_add_last_attempted_to_answers.rb new file mode 100644 index 0000000..3afee9c --- /dev/null +++ b/api/db/migrate/20240212194401_add_last_attempted_to_answers.rb @@ -0,0 +1,5 @@ +class AddLastAttemptedToAnswers < ActiveRecord::Migration[7.0] + def change + add_column :answers, :last_attempted_at, :datetime + end +end diff --git a/api/db/migrate/20240212200940_add_interval_level_to_answers.rb b/api/db/migrate/20240212200940_add_interval_level_to_answers.rb new file mode 100644 index 0000000..36fa75e --- /dev/null +++ b/api/db/migrate/20240212200940_add_interval_level_to_answers.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddIntervalLevelToAnswers < ActiveRecord::Migration[7.0] + def change + add_column :answers, :interval_level, :integer, default: 0 + end +end diff --git a/api/db/schema.rb b/api/db/schema.rb index eb58734..7e6b91a 100644 --- a/api/db/schema.rb +++ b/api/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_01_06_180026) do +ActiveRecord::Schema[7.0].define(version: 2024_02_12_200940) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -48,6 +48,8 @@ t.integer "score" t.bigint "user_id" t.bigint "exam_id" + t.datetime "last_attempted_at" + t.integer "interval_level", default: 0 t.index ["exam_id"], name: "index_answers_on_exam_id" t.index ["user_id"], name: "index_answers_on_user_id" end From a0a12b8198fcc731ee33aec6b73478480604cf56 Mon Sep 17 00:00:00 2001 From: "Jeferson S. Brito" Date: Mon, 12 Feb 2024 17:45:39 -0300 Subject: [PATCH 06/10] fix: change to daily schedule and store answer when performing /evaluate --- api/app/controllers/exams_controller.rb | 11 ++++++++++- api/app/models/revision.rb | 8 -------- api/config/schedule.rb | 10 +--------- api/lib/tasks/send_review_email.rake | 6 +++--- api/spec/mailers/notification_spec.rb | 1 - 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/api/app/controllers/exams_controller.rb b/api/app/controllers/exams_controller.rb index e3b8e82..9ec951b 100644 --- a/api/app/controllers/exams_controller.rb +++ b/api/app/controllers/exams_controller.rb @@ -12,12 +12,21 @@ def index render json: @exams end + # Evaluates an exam + # FindOrCreate revision (if missed questions) + # Store the final answer for the user def evaluate @exam = Exam.find(params[:exam_id]) questions = params[:questions] score, questions_answered_incorrectly = Exams::Evaluate.perform(questions, @exam.questions.length) Revisions::Create.perform(params[:exam_id], @user.id, questions_answered_incorrectly) + answer = Answer.find_or_create_by( + exam_id: params[:exam_id], + user_id: @user.id, + score: + ) + answer.attempt render json: { score: }, status: :created end @@ -56,7 +65,7 @@ def evaluate_duos end private - + def create_params params.permit(:title, :difficulty, :version, :category_id, :question_ids) end diff --git a/api/app/models/revision.rb b/api/app/models/revision.rb index 88e20fa..57c4295 100644 --- a/api/app/models/revision.rb +++ b/api/app/models/revision.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Revision < ApplicationRecord - after_create :schedule_review belongs_to :exam belongs_to :user @@ -9,11 +8,4 @@ class Revision < ApplicationRecord validates :user, presence: true validates :exam, presence: true - - def schedule_review - NotificationMailer - .with(user: User.find(user_id), url: "/api/revisions/#{id}") - .review_email - .deliver_later(wait_until: 3.day.from_now) - end end diff --git a/api/config/schedule.rb b/api/config/schedule.rb index 4bbf79d..355cd11 100644 --- a/api/config/schedule.rb +++ b/api/config/schedule.rb @@ -4,14 +4,6 @@ set :environment, :development set :output, "#{path}/log/cron.log" -every 3.day do - rake 'send_review_email:all' -end - -every 1.week do - rake 'send_review_email:all' -end - -every :month do +every :day do rake 'send_review_email:all' end diff --git a/api/lib/tasks/send_review_email.rake b/api/lib/tasks/send_review_email.rake index 5964595..1f8f144 100644 --- a/api/lib/tasks/send_review_email.rake +++ b/api/lib/tasks/send_review_email.rake @@ -2,10 +2,10 @@ namespace :send_review_email do desc 'Send review email to answer revision' task :all => :environment do Revision.find_each do |rev| - last_ans = rev.exam.answer - if rev.questions.length > 1 && !last_ans + answer = rev.exam.answer.last + is_enough_questions = rev.questions.length > 1 + if is_enough_questions && answer.valid_interval? puts "[LOG] #{Time.now}: sending e-mail revision #{rev.id}" - NotificationMailer.with(user: rev.user_id, url: "/api/revisions/#{rev.last.id}").review_email.deliver_now end end diff --git a/api/spec/mailers/notification_spec.rb b/api/spec/mailers/notification_spec.rb index 936fc34..ec25fdd 100644 --- a/api/spec/mailers/notification_spec.rb +++ b/api/spec/mailers/notification_spec.rb @@ -12,7 +12,6 @@ end it 'renders the receiver email' do - puts mail.body.encoded expect(mail.to).to eq([user.email]) end From 77e38cf7932c6d825348c4cbfac0b625f48ea778 Mon Sep 17 00:00:00 2001 From: "Jeferson S. Brito" Date: Mon, 12 Feb 2024 17:49:57 -0300 Subject: [PATCH 07/10] chore: update interval times --- api/app/models/answer.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/app/models/answer.rb b/api/app/models/answer.rb index 02140c0..a014e01 100644 --- a/api/app/models/answer.rb +++ b/api/app/models/answer.rb @@ -5,9 +5,9 @@ class Answer < ApplicationRecord belongs_to :exam INTERVALS = { - 1 => 3.minutes, - 2 => 10.minutes, - 3 => 20.minutes + 1 => 3.days, + 2 => 1.week, + 4 => 1.month }.freeze validates :user, presence: true From b812195aba2b0d7ac4da70f1a5de87f070b9cdfe Mon Sep 17 00:00:00 2001 From: "Jeferson S. Brito" Date: Sat, 17 Feb 2024 17:27:39 -0300 Subject: [PATCH 08/10] refact: add batch_size to query --- api/lib/tasks/send_review_email.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/lib/tasks/send_review_email.rake b/api/lib/tasks/send_review_email.rake index 1f8f144..67e9755 100644 --- a/api/lib/tasks/send_review_email.rake +++ b/api/lib/tasks/send_review_email.rake @@ -1,7 +1,7 @@ namespace :send_review_email do desc 'Send review email to answer revision' - task :all => :environment do - Revision.find_each do |rev| + task all: :environment do + Revision.find_each(batch_size: 100) do |rev| answer = rev.exam.answer.last is_enough_questions = rev.questions.length > 1 if is_enough_questions && answer.valid_interval? From 7681e9097a98ade6f00d08cdccd81f2173d424b9 Mon Sep 17 00:00:00 2001 From: "Jeferson S. Brito" Date: Sat, 17 Feb 2024 18:19:41 -0300 Subject: [PATCH 09/10] feat: add Answer tests for attempt method --- api/app/models/answer.rb | 2 +- api/spec/models/answer_spec.rb | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/api/app/models/answer.rb b/api/app/models/answer.rb index a014e01..dff9b31 100644 --- a/api/app/models/answer.rb +++ b/api/app/models/answer.rb @@ -7,7 +7,7 @@ class Answer < ApplicationRecord INTERVALS = { 1 => 3.days, 2 => 1.week, - 4 => 1.month + 3 => 1.month }.freeze validates :user, presence: true diff --git a/api/spec/models/answer_spec.rb b/api/spec/models/answer_spec.rb index 246e193..b0156c2 100644 --- a/api/spec/models/answer_spec.rb +++ b/api/spec/models/answer_spec.rb @@ -11,4 +11,67 @@ it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:exam) } end + + describe 'attempt' do + describe 'when interval level is already at maximum' do + let(:answer) { create(:answer) } + + it 'does not increment interval level' do + answer.interval_level = 3 + answer.attempt + expect(answer.interval_level).to be <= 3 + end + end + + describe 'when score is 100 or greater' do + let(:answer) { create(:answer) } + + it 'does not increment interval level' do + answer.score = 100 + answer.attempt + expect(answer.interval_level).to be 0 + end + end + + describe 'when score is less than 100 and interval level is less than maximum' do + let(:answer) { create(:answer) } + + it 'increments interval level' do + answer.attempt + expect(answer.interval_level).to be 1 + end + + it 'saves answer attempt' do + answer.attempt + expect(answer.last_attempted_at).to_not be nil + expect(answer.interval_level).to be 1 + end + end + end + + describe 'valid_interval' do + describe 'when last_attempted_at is within interval' do + let(:answer) { create(:answer) } + + it 'returns true' do + answer.interval_level = 1 + answer.last_attempted_at = 3.days.ago + + expect(answer.last_attempted_at).to_not be nil + expect(answer.valid_interval?).to be true + end + end + + describe 'when last_attempted_at is NOT within interval' do + let(:answer) { create(:answer) } + + it 'returns false' do + answer.interval_level = 1 + answer.last_attempted_at = 2.days.ago + + expect(answer.last_attempted_at).to_not be nil + expect(answer.valid_interval?).to be false + end + end + end end From c3ae3ecbda797074ed6913faaf87fd83a080f2a9 Mon Sep 17 00:00:00 2001 From: "Jeferson S. Brito" Date: Tue, 20 Feb 2024 20:26:31 -0300 Subject: [PATCH 10/10] tests: add rake spec for send_review_email --- api/app/views/revisions/show.json.jbuilder | 2 +- api/lib/tasks/send_review_email.rake | 4 ++-- api/spec/factories/revisions.rb | 8 +++++-- api/spec/tasks/send_review_email_spec.rb | 28 ++++++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 api/spec/tasks/send_review_email_spec.rb diff --git a/api/app/views/revisions/show.json.jbuilder b/api/app/views/revisions/show.json.jbuilder index f574308..b59a685 100644 --- a/api/app/views/revisions/show.json.jbuilder +++ b/api/app/views/revisions/show.json.jbuilder @@ -6,4 +6,4 @@ json.user_id @revision.exam_id json.questions @revision.questions json.created_at @revision.created_at -json.updated_at @revision.updated_at \ No newline at end of file +json.updated_at @revision.updated_at diff --git a/api/lib/tasks/send_review_email.rake b/api/lib/tasks/send_review_email.rake index 67e9755..cdebc55 100644 --- a/api/lib/tasks/send_review_email.rake +++ b/api/lib/tasks/send_review_email.rake @@ -4,9 +4,9 @@ namespace :send_review_email do Revision.find_each(batch_size: 100) do |rev| answer = rev.exam.answer.last is_enough_questions = rev.questions.length > 1 - if is_enough_questions && answer.valid_interval? + if answer && (is_enough_questions && answer.valid_interval?) puts "[LOG] #{Time.now}: sending e-mail revision #{rev.id}" - NotificationMailer.with(user: rev.user_id, url: "/api/revisions/#{rev.last.id}").review_email.deliver_now + NotificationMailer.with(user: rev.user_id, url: "/api/revisions/#{rev.id}").review_email.deliver_now end end end diff --git a/api/spec/factories/revisions.rb b/api/spec/factories/revisions.rb index b752675..a4f180e 100644 --- a/api/spec/factories/revisions.rb +++ b/api/spec/factories/revisions.rb @@ -5,9 +5,13 @@ association :user association :exam + transient do + questions_count { 3 } + end + trait :with_questions do - after(:create) do |revision| - revision.questions = create_list(:question, 3, :with_options) + after(:create) do |revision, evaluator| + revision.questions = create_list(:question, evaluator.questions_count, :with_options) end end end diff --git a/api/spec/tasks/send_review_email_spec.rb b/api/spec/tasks/send_review_email_spec.rb new file mode 100644 index 0000000..7c3d121 --- /dev/null +++ b/api/spec/tasks/send_review_email_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'send_review_email:all' do + before :all do + Api::Application.load_tasks + Rake::Task.define_task(:environment) + end + + describe 'when having enough questions and answer is in valid interval' do + let!(:rev_with_questions) { create(:revision, :with_questions, id: 2) } + let!(:answer) { + create(:answer, + exam_id: rev_with_questions.exam.id, + interval_level: 1, + last_attempted_at: 3.days.ago + ) + } + + it 'sends review email for revision' do + allow(NotificationMailer).to receive_message_chain(:with, :review_email, :deliver_now) + Rake::Task['send_review_email:all'].invoke + expect(NotificationMailer).to have_received(:with).with(hash_including(user: rev_with_questions.user.id, + url: "/api/revisions/#{rev_with_questions.id}")) + end + end +end