-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Vouchers for user promotion - Part 1: Introduction of Vouchers (#670)
* Initialize voucher model and add some unit tests * Make ensure_no_other_active_voucher validation into a callback * Add throw :abort in order to halt execution * Replace Time.now by Time.zone.now * Set up basic functionality for display of vouchers for tutors * Create separate file for copy and paste button code and decaffeinate * Set up destruction of tutor vouchers * Rename view file as it containes embedded ruby * Add create action for vouchers and corresponding views * Adapt views and controller for adding and removing of vouchers of different type * Put duplicate lines into a separate method * Set up redeeming of vouchers * fix typo * remove obsolete methods * Avoid use of Time.now * Refactor active_vouhcer_of_sort method * remove unused expired? method * Remove duplicate code * remove unused variable * Add controller spec for vouchers * Rewrite controller spec for vouchers * remove obsolete comment * Invalidate vouchers instead of destroying them * Add vouchers for seminar speakers * Rename sort attribute of vouchers to role * Add cypress data attributes * Add first cypress tests * Add more cypress tests * Init future possibility to check clipboard content * Remove unnecessary call of trait * Use NO_SEMINAR_ROLES constant * Refactor JS for copy/paste button * Redesign voucher creation/deletion * Add explanations for what a voucher is * Fix minor UI inconsistencies * Revert downcasing Since in German, we cannot do that... * Improve cypress tests e.g. actually test format of UUID version 4 string * Indent if condition in HTML * Add TODO note to redeem controller method * Remove unnecessary comment * Group role-related stuff together in model * Improve voucher model specs * Update db/migrate/20240728123817_create_vouchers.rb Co-authored-by: Splines <[email protected]> * Add missing speaker trait * Use symbol for implicit order column * Update timestamp for create vouchers migration * Simplify ability handling via automatic resource loading See the docs: https://github.com/CanCanCommunity/cancancan/blob/develop/docs/controller_helpers.md#authorize_resource-load_resource-load_and_authorize_resource * Make whole copy button area clickable & simplify * Init time traveling in Cypress tests & test expired vouchers in frontend * Remove accidental `it.only` flag * Refactor Cypress voucher specs common assertions * Run `bundle install` again due to new version specifier (see also the merge comment before this commit) * Rename spec method * Improve cypress test description --------- Co-authored-by: Splines <[email protected]> Co-authored-by: Splines <[email protected]>
- Loading branch information
1 parent
78467de
commit c00ae31
Showing
35 changed files
with
1,011 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,6 +107,7 @@ | |
"factorybot", | ||
"helpdesk", | ||
"katex", | ||
"Timecop", | ||
"turbolinks" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
class VoucherAbility | ||
include CanCan::Ability | ||
|
||
def initialize(user) | ||
clear_aliased_actions | ||
|
||
can [:create, :invalidate], Voucher do |voucher| | ||
user.can_update_personell?(voucher.lecture) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
$(document).on("turbolinks:load", function () { | ||
// TODO: this is using clipboard.js, which makes use of deprecated browser APIs | ||
// see issue #684 | ||
new Clipboard(".clipboard-btn"); | ||
|
||
$(document).on("click", ".clipboard-button", function () { | ||
$(".token-clipboard-popup").removeClass("show"); | ||
|
||
let dataId = $(this).data("id"); | ||
let popup; | ||
if (dataId) { | ||
popup = `.token-clipboard-popup[data-id="${$(this).data("id")}"]`; | ||
} | ||
else { | ||
// This is a workaround for the transition to the new ClipboardAPI | ||
// as intermediate solution that respects that the whole button should | ||
// be clickable, not just the icon itself. | ||
// See app/views/vouchers/_voucher.html.erb as an example. | ||
popup = $(this).find(".token-clipboard-popup"); | ||
} | ||
|
||
$(popup).addClass("show"); | ||
setTimeout(() => { | ||
$(popup).removeClass("show"); | ||
}, 1700); | ||
}); | ||
}); | ||
|
||
// clean up for turbolinks | ||
$(document).on("turbolinks:before-cache", function () { | ||
$(document).off("click", ".clipboard-button"); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
module Cypress | ||
# Allows to travel to a date in the backend via Cypress tests. | ||
|
||
class TimecopController < CypressController | ||
# Travels to a specific date and time. | ||
# | ||
# Time is passed as local time. If you want to pass a UTC time, set the | ||
# parameter `use_utc` to true. | ||
def travel | ||
new_time = if params[:use_utc] == "true" | ||
Time.utc(params[:year], params[:month], params[:day], | ||
params[:hours], params[:minutes], params[:seconds]) | ||
else | ||
Time.zone.local(params[:year], params[:month], params[:day], | ||
params[:hours], params[:minutes], params[:seconds]) | ||
end | ||
|
||
render json: Timecop.travel(new_time), status: :created | ||
end | ||
|
||
def reset | ||
render json: Timecop.return, status: :created | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
class VouchersController < ApplicationController | ||
load_and_authorize_resource | ||
before_action :find_voucher, only: :invalidate | ||
|
||
def current_ability | ||
@current_ability ||= VoucherAbility.new(current_user) | ||
end | ||
|
||
def create | ||
set_related_data | ||
respond_to do |format| | ||
if @voucher.save | ||
handle_successful_save(format) | ||
else | ||
handle_failed_save(format) | ||
end | ||
end | ||
end | ||
|
||
def invalidate | ||
set_related_data | ||
@voucher.update(invalidated_at: Time.zone.now) | ||
respond_to do |format| | ||
format.html { redirect_to edit_lecture_path(@lecture, anchor: "people") } | ||
format.js | ||
end | ||
end | ||
|
||
def redeem | ||
# TODO: this will be dealt with in the corresponding 2nd PR | ||
render js: "alert('Voucher redeemed!')" | ||
end | ||
|
||
private | ||
|
||
def voucher_params | ||
params.permit(:lecture_id, :role) | ||
end | ||
|
||
def find_voucher | ||
@voucher = Voucher.find_by(id: params[:id]) | ||
return if @voucher | ||
|
||
handle_voucher_not_found | ||
end | ||
|
||
def set_related_data | ||
@lecture = @voucher.lecture | ||
@role = @voucher.role | ||
I18n.locale = @lecture.locale | ||
end | ||
|
||
def handle_successful_save(format) | ||
format.html { redirect_to edit_lecture_path(@lecture, anchor: "people") } | ||
format.js | ||
end | ||
|
||
def handle_failed_save(format) | ||
error_message = @voucher.errors.full_messages.join(", ") | ||
format.html do | ||
redirect_to edit_lecture_path(@lecture, anchor: "people"), | ||
alert: error_message | ||
end | ||
format.js do | ||
render "error", locals: { error_message: error_message } | ||
end | ||
end | ||
|
||
def handle_voucher_not_found | ||
I18n.locale = current_user.locale | ||
error_message = I18n.t("controllers.no_voucher") | ||
respond_to do |format| | ||
format.html do | ||
redirect_back(alert: error_message, | ||
fallback_location: root_path) | ||
end | ||
format.js do | ||
render "error", | ||
locals: { error_message: error_message } | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
class Voucher < ApplicationRecord | ||
SPEAKER_EXPIRATION_DAYS = 30 | ||
TUTOR_EXPIRATION_DAYS = 14 | ||
DEFAULT_EXPIRATION_DAYS = 3 | ||
|
||
ROLE_HASH = { tutor: 0, editor: 1, teacher: 2, speaker: 3 }.freeze | ||
enum role: ROLE_HASH | ||
validates :role, presence: true | ||
|
||
belongs_to :lecture, touch: true | ||
|
||
before_create :generate_secure_hash | ||
before_create :add_expiration_datetime | ||
before_create :ensure_no_other_active_voucher | ||
before_create :ensure_speaker_vouchers_only_for_seminars | ||
|
||
scope :active, lambda { | ||
where("expires_at > ? AND invalidated_at IS NULL", | ||
Time.zone.now) | ||
} | ||
|
||
self.implicit_order_column = :created_at | ||
|
||
def self.roles_for_lecture(lecture) | ||
return ROLE_HASH.keys if lecture.seminar? | ||
|
||
ROLE_HASH.keys - [:speaker] | ||
end | ||
|
||
private | ||
|
||
def generate_secure_hash | ||
self.secure_hash = SecureRandom.hex(16) | ||
end | ||
|
||
def add_expiration_datetime | ||
self.expires_at = created_at + expiration_days.days | ||
end | ||
|
||
def ensure_no_other_active_voucher | ||
return unless lecture | ||
return unless lecture.vouchers.where(role: role).active.any? | ||
|
||
errors.add(:role, | ||
I18n.t("activerecord.errors.models.voucher.attributes.role." \ | ||
"only_one_active")) | ||
throw(:abort) | ||
end | ||
|
||
def ensure_speaker_vouchers_only_for_seminars | ||
return unless speaker? | ||
return if lecture.seminar? | ||
|
||
errors.add(:role, | ||
I18n.t("activerecord.errors.models.voucher.attributes.role." \ | ||
"speaker_vouchers_only_for_seminars")) | ||
throw(:abort) | ||
end | ||
|
||
def expiration_days | ||
return SPEAKER_EXPIRATION_DAYS if speaker? | ||
return TUTOR_EXPIRATION_DAYS if tutor? | ||
|
||
DEFAULT_EXPIRATION_DAYS | ||
end | ||
end |
Oops, something went wrong.