Commit 3e525b46 authored by Khang Le's avatar Khang Le
Browse files

resolve conflicts

Showing with 732 additions and 132 deletions
+732 -132
......@@ -17,14 +17,18 @@ pickle-email-*.html
# This rule is for MacOS thumbnail files.
**/.DS_Store*
# TODO Comment out this rule if you are OK with secrets being uploaded to the repo
# Put all files in which we use specific config params here
# to replace them into '$\w*.'.example.'.\w^'
config/schedule.rb
# Comment out this rule if you are OK with secrets being uploaded to the repo
config/initializers/secret_token.rb
# Only include if you have production secrets in this file, which is no longer a Rails default
# config/secrets.yml
# dotenv
# TODO Comment out this rule if environment variables can be committed
# Comment out this rule if environment variables can be committed
.env
## Environment normalization:
......
source 'https://rubygems.org'
# gem 'whenever', require: false
gem 'whenever', require: false
gemspec
gem 'dotenv-rails'
gem 'mail'
# gem 'whenever'
gem 'sidekiq'
gem 'sidekiq-scheduler'
\ No newline at end of file
......@@ -145,4 +145,4 @@ DEPENDENCIES
sidekiq-scheduler
BUNDLED WITH
1.17.2
2.0.1
OpenProject Service Packs Plugin
================================
## A R&D project
================================
## A R&D project
A plugin tracks its units through logging time work packages basing on definable rates of Time Entry Activities.
\ No newline at end of file
service_pack_entry(1,1)-----(0,1)time_entry
(1,1)
|
|
|
(1,n)
service_pack(1,n)-----(1,1)mapping_rate(1,1)-----(1,n)time_entry_activity
(1,n)
|
|
|
(1,1)
assign
(1,1)
|
|
|
(1,n)
project
Delete a Service Pack
- Condition: currently, this SP must not be assigned to any project
- Effect:
"assign" records associated with this SP will be deleted
"mapping rate" records associated with this SP will be deleted
"service pack entry" records associated with this SP will be deleted
Delete an Enumeration with Time Entry Activity type
- If the Time Entry Activity has been logged for the time entry, reassigning this activity to another activity
- Effect:
"mapping rate" records associated with this Enum will be deleted
Create a new Enumeration with TimeEntryActivity type
- Automatically create this activity type with the default rates (0 units/hour) for each SP
When user log time on a WP of a project
- If this project was assigned a SP, calculate the consumed units for that log time based on the mapping rates of the SP
- Update the remained units
When user edit a time entry on a WP of a project
- Remained units of a SP will be updated if at the time user logged time on a project, there was a SP (currently exist) was assigned to that project.
When user delete a time entry on a WP of a project
- Remained units of a SP will be backed if at the time user logged time on a project, there was a SP (currently exist) was assigned to that project.
When assigning a SP to the Project, only time entries of that project (the hierachy of WP doesn't matter) will trigger the "Update remained units" function, not the time entries of the parent project or children projects.
Every day (at 0h:00 am) cronjobs run to check whether a Service Pack is expired.
Report
date
user
activity
project
WP
type
subject
units
comments
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
//use <% javascript_include_tag %>
function loadServicePack() {
var co = document.querySelector("#select-sp");
if (co.selectedIndex == 0) {
document.querySelector("#sp-content").innerHTML = "Please select a Service Pack";
document.querySelector("#sp-assign-button").disabled = true;
return;
}
var comp = co.options[co.selectedIndex];
var str = "Activation Date: " + comp.dataset.start + "<br/>";
str += "Expiration Date: " + comp.dataset.end + "<br/>";
str += "Capacity: " + comp.dataset.cap + "<br/>";
str += "Remained: " + comp.dataset.rem + "<br/>";
// click for more
document.querySelector("#sp-content").innerHTML = str;
document.querySelector("#sp-assign-button").disabled = false;
}
document.addEventListener("DOMContentLoaded", function(event) {
document.querySelector("#select-sp").addEventListener("change", loadServicePack);
// unintrusive
document.querySelector("#sp-assign-button").disabled = true;
})
\ No newline at end of file
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
document.addEventListener("DOMContentLoaded", function(event) {
document.getElementById("view-stat").addEventListener("click", function(){
var xhr = new XMLHttpRequest();
// replace the link with statistics link
xhr.open('GET', 'http://localhost:3000/service_packs/1.json');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status === 200) {
console.log(JSON.parse(xhr.responseText))
}
else {
alert('Request failed. Returned status of ' + xhr.status);
}
};
xhr.send();
});
})
\ No newline at end of file
/*
Place all the styles related to the matching controller here.
They will automatically be included in application.css.
*/
*/
\ No newline at end of file
/*
Place all the styles related to the matching controller here.
They will automatically be included in application.css.
*/
label, input {
margin-bottom: 0 !important;
max-width: 500px !important;
}
input.rates {
max-width: 465px !important;
}
table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 100%;
}
td, th {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}
.grid-container {
display: grid;
grid-template-columns: auto auto;
/* background-color: #2196F3;*/
padding: 10px;
}
.grid-container > div {
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid black;
text-align: left;
font-size: 20px;
}
.destroy {
background-color: #fc0000 !important;
color: #ffeeee !important;
}
a.blocked {
opacity: 0.3;
margin-right: 8px;
cursor: default !important;
margin-left: -5px;
}
#warnbox {
font-size: 0.75em;
text-align: center;
position: relative;
bottom: 7px;
right: 2px;
}
hr {
margin-top: 12px;
}
#assignments-box {
line-height: 1.25;
margin-bottom: 0.9rem;
}
.button.-highlight.-edit {
margin-bottom: 0;
}
.button.-alt-highlight.-new {
margin-bottom: 0;
}
.button-container {
margin-bottom: 10px;
}
#rates-input {
padding-left: 30px;
padding-bottom: 20px;
overflow-x: auto;
}
#table-rates-input {
width: 40%;
}
table#table-rates-input td {
border: 0px;
}
table#table-rates-input th {
border: 0px;
}
#error-explanation {
background-color: rgba(255,255,0,0.41);
}
#error-explanation h2 {
color: #fc0000;
}
#detailed-sp {
overflow: auto;
}
.sp {
border: 1.5px solid;
border-color: #EAEAEA;
height: 300px;
margin: 20px;
padding: 20px;
}
.sp.description {
float: left;
width: 27%;
}
.sp.rates {
float: left;
width: 27%;
}
.sp.assigned {
float: left;
width: 27%;
}
class AssignsController < ApplicationController
#layout 'admin'
before_action :find_project_by_project_id
include SPAssignmentManager
def assign
if !(@project.assigns.where(assigned: true).empty?)
flash[:alert] = "You must unassign first!"
# binding.pry
return head 403 unless @can_assign = User.current.allowed_to?(:assign_service_packs, @project)
if assigned?(@project)
flash.now[:alert] = "You must unassign first!"
render_400 and return
end
@service_pack = ServicePack.find(params[:assign][:service_pack_id])
#binding.pry
@service_pack = ServicePack.find_by(id: params[:assign][:service_pack_id])
if @service_pack.nil?
flash.now[:alert] = "Service Pack not found"
render_404 and return
end
if @service_pack.available?
if assignable = @service_pack.assigns.where(assigned: true).empty?
ActiveRecord::Base.transaction do
# one query only
@project.assigns.update_all(assigned: false)
@assignment = @service_pack.assigns.find_by(project_id: @project.id) || @project.assigns.new
@assignment.assigned = true
@assignment.assign_date = Date.today
@assignment.service_pack_id = @service_pack.id
@assignment.save!
end
#binding.pry
flash[:notice] = "Service Pack #{@service_pack.name} successfully assigned to project #{@project.name}"
redirect_to action: "show" and return
else
# already assigned for another project
flash[:alert] = "Service Pack #{@service_pack.name} has been already assigned"
render_400 and return
end
# binding.pry
assign_to(@service_pack, @project)
flash.now[:notice] = "Service Pack '#{@service_pack.name}' successfully assigned to project '#{@project.name}'"
render 'already_assigned' and return
else
# already assigned for another project
# constraint need
flash.now[:alert] = "Service Pack '#{@service_pack.name}' has been already assigned"
render_400 and return
end
flash[:alert] = "Service Pack not found"
render_404 and return
flash.now[:alert] = 'Service Pack cannot be assigned'
redirect_to action: :show
end
def unassign
#
# if !@project.module_enabled?(:openproject_service_packs)
# render_400 and return
# end
@assignment = @project.assigns.find_by(assigned: true)
if @assignment.nil?
flash[:alert] = "No Service Pack is assigned to this project"
return head 403 unless @can_unassign = User.current.allowed_to?(:unassign_service_packs, @project)
if unassigned?(@project)
flash[:alert] = 'No Service Pack is assigned to this project'
render_404 and return
end
@assignment.assigned = false
@assignment.save!
flash[:notice] = "Unassigned a Service Pack from this project"
redirect_to action: "show"
_unassign(@project)
flash[:notice] = 'Unassigned a Service Pack from this project'
redirect_to action: :show and return
end
def show
# assigned now
@assignment = @project.assigns.find_by(assigned: true)
if @assignment && @assignment.service_pack.unavailable?
@assignment.assigned = false
@assignment.save!
@assignment = nil # overdue
# This will lock even admins out if the module is not activated.
return head 403 unless
User.current.allowed_to?(:see_assigned_service_packs, @project) ||
(@can_assign = User.current.allowed_to?(:assign_service_packs, @project)) ||
(@can_unassign = User.current.allowed_to?(:unassign_service_packs, @project))
# binding.pry
if @assignment = @project.assigns.find_by(assigned: true)
if @assignment.service_pack.unavailable?
@assignment.terminate
@assignment = nil # signifying no assignments are in effect
# as the single one is terminated.
end
end
#binding.pry
# binding.pry
if @assignment.nil?
# testing only
t = ServicePack.where("expired_date >= ?", Date.today) if Rails.env.development?
@assignment = Assign.new
#binding.pry
@assignables = []
t.each do |assignable|
if assignable.assigns.where(assigned: true).empty?
@assignables << assignable
if @can_assign ||= User.current.allowed_to?(:assign_service_packs, @project)
@assignables = ServicePack.availables
if @assignables.exists?
@assignment = Assign.new
render -'not_assigned_yet' and return
end
end
#binding.pry
render -'unassignable'
# binding.pry
else
@service_pack = @assignment.service_pack
render -'already_assigned'
end
end
def select_to_transfer
return head 403 unless @can_assign = User.current.allowed_to?(:assign_service_packs, @project)
end
# =======================================================
# :Docs
# * Limit: Serving JSON only. Intended to restrict to :show.
# * Purpose:
# Return a table with consumed units by a Project grouped by service
# pack, then activities and sorted from large to small.
# * Expected Inputs:
# [project_id]: Sharing the same route with the resourceful default.
# Put in the link. Mandatory.
# [start_period]: Beginning of the counting period. As a date. Optional.
# [end_period]: Ending of the counting period. As a date. Optional.
# start_period MUST NOT be later than end_period.
# Both or none of [start_period, end_period] can be present.
# * Expected Outputs
# Top class: None
# Content: Array of object having [name, act_name, consumed]
# - Name: Name of Service Pack
# - act_name: Name of activity
# - consumed: How many units are consumed (in given period)
# Status: 200
# * When raising error
# HTTP 400: Malformed request.
# =======================================================
def statistics
return head 403 unless
User.current.allowed_to?(:see_assigned_service_packs, @project) ||
User.current.allowed_to?(:assign_service_packs, @project) ||
User.current.allowed_to?(:unassign_service_packs, @project)
start_day = params[:start_period]&.to_date # ruby >= 2.3.0
end_day = params[:end_period]&.to_date
if start_day.nil? ^ end_day.nil?
render json: { error: 'GET OUT!'}, status: 400 and return
end
# Notice: Change max(t4.name) to ANY_VALUE(t4.name) on production builds.
# MySQL specific >= 5.7.5
# https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html
get_parent_id = <<-SQL
SELECT id, name, COALESCE(parent_id, id) AS pid
FROM #{TimeEntryActivity.table_name}
WHERE type = 'TimeEntryActivity'
SQL
body_query = <<-SQL
SELECT t1.service_pack_id AS spid, max(t4.name) AS name, t3.pid AS pid,
max(t3.name) AS act_name, sum(t1.units) AS consumed
FROM #{ServicePackEntry.table_name} t1
INNER JOIN #{TimeEntry.table_name} t2
ON t1.time_entry_id = t2.id
INNER JOIN (#{get_parent_id}) t3
ON t2.activity_id = t3.id
INNER JOIN #{ServicePack.table_name} t4
ON t1.service_pack_id = t4.id
SQL
group_clause = <<-SQL
GROUP BY t1.service_pack_id, t3.pid
ORDER BY consumed DESC
SQL
where_clause = "WHERE t2.project_id = ?"
where_clause << (start_day.nil? ? '' : ' AND t1.created_at BETWEEN ? AND ?')
query = body_query + where_clause + group_clause
par = start_day.nil? ? [query, params[@project.id]] : [query, params[@project.id], start_day, end_day]
sql = ActiveRecord::Base.send(:sanitize_sql_array, par)
render json: ActiveRecord::Base.connection.exec_query(sql).to_hash, status: 200
end
end
class ServicePacksController < ApplicationController
include CsvExtractionHelper
# only allow admin
before_action :require_admin
......@@ -6,62 +9,234 @@ class ServicePacksController < ApplicationController
layout 'admin'
def index
# @service_packs = ServicePack.all
# render plain: ServicePack.expired.count
@service_packs = ServicePack.all
end
def new
@service_pack = ServicePack.new
TimeEntryActivity.shared.count.times {@service_pack.mapping_rates.build}
# TimeEntryActivity.shared.count.times {@service_pack.mapping_rates.build}
@sh = TimeEntryActivity.shared
@c = TimeEntryActivity.shared.count
end
def show
@service_pack = ServicePack.find(params[:id])
# add some json between this
# controller chooses not to get the thresholds.
# assume the service pack exists.
# TODO: make a separate action JSON only.
respond_to do |format|
format.json {render json: @service_pack.as_json(except: [:threshold1, :threshold2, :updated_on])}
# and this
format.html
@rates = @service_pack.mapping_rates
format.json {
# the function already converted this to json
render plain: ServicePackPresenter.new(@service_pack).json_export(:rate), status: 200
}
format.html {
# http://www.chrisrolle.com/en/blog/benchmark-preload-vs-eager_load
@rates = @service_pack.mapping_rates.preload(:activity)
@assignments = @service_pack.assignments.preload(:project)
}
format.csv {
# projection order is not significant
# Work packages can be deleted.
sql = <<-SQL
SELECT t2.spent_on, concat(t4.firstname, ' ', t4.lastname) AS user_name, t3.name AS activity_name,
t6.name AS project_name, t5.id AS work_package_id, t7.name AS type_name, t5.subject AS subject,
t2.comments AS comment, t1.units
FROM service_pack_entries t1
INNER JOIN #{TimeEntry.table_name} t2
ON t1.time_entry_id = t2.id
INNER JOIN #{TimeEntryActivity.table_name} t3
ON t2.activity_id = t3.id
INNER JOIN users t4
ON t2.user_id = t4.id
INNER JOIN projects t6
ON t2.project_id = t6.id
LEFT JOIN #{WorkPackage.table_name} t5
ON t2.work_package_id = t5.id
LEFT JOIN types t7
ON t5.type_id = t7.id
WHERE service_pack_id = #{@service_pack.id}
ORDER BY spent_on DESC
SQL
entries = ActiveRecord::Base.connection.exec_query(sql).to_hash
render csv: csv_extractor(entries), filename: "service_pack_#{@service_pack.name}.csv"
}
end
end
# The string with the minus sign in front is a shorthand for <string>.freeze
# reducing server processing time (and testing time) by 30%!
# Freezing a string literal will stop it from being created anew over and over.
# All literal strings will be frozen in Ruby 3 by default, which is a good idea.
def create
# @service_pack = ServicePack.new(service_pack_params)
# if @service_pack.save
# redirect_to @service_pack
# else
# render 'new'
# end
mapping_rate_attribute = params['service_pack']['mapping_rates_attributes']
mapping_rate_attribute = params[:service_pack][:mapping_rates_attributes]
# binding.pry
activity_id = []
mapping_rate_attribute.each {|_index, hash_value| activity_id.push(hash_value['activity_id'])}
mapping_rate_attribute.each {|_index, hash_value| activity_id.push(hash_value[:activity_id])}
if activity_id.uniq.length == activity_id.length
render plain: 'not duplicated'
@service_pack = ServicePack.new(service_pack_params)
# render plain: 'not duplicated'
if @service_pack.save
flash[:notice] = -'Service Pack creation successful.'
redirect_to action: :show, id: @service_pack.id and return
else
flash.now[:error] = -'Service Pack creation failed.'
end
else
render plain: 'duplicated'
# render plain: 'duplicated'
flash.now[:error] = -'Only one rate can be defined to one activity.'
end
# the only successful path has returned 10 lines ago.
@sh = TimeEntryActivity.shared
@c = TimeEntryActivity.shared.count
render 'new'
end
def edit
@sp = ServicePack.find(params[:id])
@activity = @sp.time_entry_activities.build
@sp = ServicePack.find_by(id: params[:id])
if @sp.nil?
flash[:error] = -"Service Pack not found"
redirect_to action: :index and return
end
# @activity = @sp.time_entry_activities.build
end
def update
@sp = ServicePack.find_by(id: params[:id])
if @sp.nil?
flash[:error] = -"Service Pack not found"
redirect_to action: :index and return
end
mapping_rate_attribute = params[:service_pack][:mapping_rates_attributes]
activity_id = []
mapping_rate_attribute.each {|_index, hash_value| activity_id.push(hash_value[:activity_id])}
if activity_id.uniq.length == activity_id.length
# No duplication
add_units
@sp.assign_attributes(service_pack_edit_params)
# binding.pry
if @sp.save
flash[:notice] = -'Service Pack update successful.'
redirect_to @sp
else
flash.now[:error] = -'Service Pack update failed.'
render 'edit'
end
else
# Duplication
flash.now[:error] = -'Only one rate can be defined to one activity.'
render 'edit'
end
end
def destroy
@sp = ServicePack.find(params[:id])
@sp.destroy
@sp = ServicePack.find_by(id: params[:id])
if @sp.nil?
flash[:error] = -"Service Pack not found"
redirect_to action: :index and return
end
if @sp.assigned?
flash.now[:error] = "Please unassign this SP from all projects before proceeding!"
redirect_to @sp
end
@sp.destroy!
redirect_to service_packs_path
end
# for breadcrumb code
def show_local_breadcrumb
true
end
def default_breadcrumb
action_name == 'index'? -'Service Packs' : ActionController::Base.helpers.link_to(-'Service Packs', service_packs_path)
end
# =======================================================
# :Docs
# * Limit: Serving JSON only. Must be Admin to access.
# * Purpose:
# Return a table with consumed units for a Service Pack grouped by activities and sorted
# from large to small.
# * Expected Inputs:
# [service_pack_id]: Sharing the same route with the resourceful default.
# Put in the link. Mandatory.
# [start_period]: Beginning of the counting period. As a date. Optional.
# [end_period]: Ending of the counting period. As a date. Optional.
# start_period MUST NOT be later than end_period.
# Both or none of [start_period, end_period] can be present.
# * Expected Outputs
# Top class: None
# Content: Array of object having [name, consumed]
# - consumed: How many units are consumed (in given period)
# - act_name: Name of activity
# Status: 200
# * When raising error
# HTTP 404: SP not found
# HTTP 400: Malformed request.
# =======================================================
def statistics
start_day = params[:start_period]&.to_date # ruby >= 2.3.0
end_day = params[:end_period]&.to_date
if start_day.nil? ^ end_day.nil?
render json: {error: 'GET OUT!'}, status: 400 and return
end
if !ServicePack.find_by(id: params[:service_pack_id])
render json: {error: 'NOT FOUND'}, status: 404 and return
end
# Notice: Change max(t3.name) to ANY_VALUE(t3.name) on production builds.
# MySQL specific >= 5.7.5
# https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html
get_parent_id = <<-SQL
SELECT id, name, COALESCE(parent_id, id) AS pid
FROM #{TimeEntryActivity.table_name}
WHERE type = 'TimeEntryActivity'
SQL
body_query = <<-SQL
SELECT t3.pid AS act_id, max(t3.name) AS act_name, sum(t1.units) AS consumed
FROM #{ServicePackEntry.table_name} t1
INNER JOIN #{TimeEntry.table_name} t2
ON t1.time_entry_id = t2.id
INNER JOIN (#{get_parent_id}) t3
ON t2.activity_id = t3.id
SQL
group_clause = <<-SQL
GROUP BY t3.pid
ORDER BY consumed DESC
SQL
where_clause = "WHERE t1.service_pack_id = ?"
where_clause << (start_day.nil? ? '' : ' AND t1.created_at BETWEEN ? AND ?')
query = body_query + where_clause + group_clause
# binding.pry
par = start_day.nil? ? [query, params[:service_pack_id]] : [query, params[:service_pack_id], start_day, end_day]
sql = ActiveRecord::Base.send(:sanitize_sql_array, par)
render json: ActiveRecord::Base.connection.exec_query(sql).to_hash, status: 200
end
private
def service_pack_params
params.require(:service_pack).permit(:name, :total_units, :started_date, :expired_date, :threshold1, :threshold2, mapping_rates_attributes: [:id, :activity_id, :service_pack_id, :units_per_hour, :_destroy])
params.require(:service_pack).permit(:name, :total_units, :started_date, :expired_date, :threshold1, :threshold2,
mapping_rates_attributes: [:id, :activity_id, :service_pack_id, :units_per_hour, :_destroy])
end
def service_pack_edit_params
params.require(:service_pack).permit(:threshold1, :threshold2,
mapping_rates_attributes: [:id, :activity_id, :service_pack_id, :units_per_hour, :_destroy])
end
def add_units
return unless params[:service_pack][:total_units]
if (t = params[:service_pack][:total_units].to_f) <= 0.0
@sp.errors.add(:total_units, 'is invalid') and return
end
@sp.grant(t - @sp.total_units) unless t == @sp.total_units
end
end
# frozen_string_literal: true
module CsvExtractionHelper
def csv_extractor(entries)
# from timelog_helper.rb
# decimal_separator = l(:general_csv_decimal_separator) # not needed
# custom_fields = TimeEntryCustomField.all # not supported
# fix broken display on Excel
export = CSV.generate(col_sep: ';') { |csv|
headers = ['Date', 'User', 'Activity', 'Project', 'Work Package', 'Type', 'Subject', 'Units', 'Comments']
# headers += custom_fields.map(&:name) # not supported
csv << headers
entries.each do |entry|
fields = [format_date(entry['spent_on']),
entry['user_name'],
entry['activity_name'],
entry['project_name'],
entry['work_package_id'],
entry['type_name'],
entry['subject'],
entry['units'],
entry['comment']
]
# fields += custom_fields.map { |f| show_value(entry.custom_value_for(f)) }
# binding.pry
csv << fields
end
}
export
end
end
\ No newline at end of file
class ServicePackPresenter
attr_reader :service_pack
def initialize(service_pack)
# we don't take NIL as an option
if service_pack && service_pack.id
@service_pack = service_pack
else
raise "This is NIL cannot print"
end
end
def json_full_header
# not recommended in production
@service_pack.to_json
end
def hash_lite_header
@service_pack.as_json(except: [:id, :threshold1, :threshold2, :updated_at, :created_at])
end
def json_lite_header
hash_lite_header.to_json
end
def hash_rate_only
# ActiveRecord join returns an array!
q = <<-SQL
SELECT name, units_per_hour AS upt
FROM mapping_rates t1
INNER JOIN #{TimeEntryActivity.table_name} t2
ON t1.activity_id = t2.id
WHERE t1.service_pack_id = #{@service_pack.id}
SQL
ActiveRecord::Base.connection.exec_query(q).to_hash
end
def json_rate_only
hash_rate_only.to_json
end
def json_export(sym=:header)
err = { :error => 422, :name => "Unsupported format"}
sym == :header ? json_lite_header : (sym == :rate ? json_rate_only : err.to_json)
end
end
\ No newline at end of file
module ServicePacksHelper
end
module ServicePacksNotification
def self.notify_under_threshold1
# https://blog.arkency.com/2013/12/rails4-preloading/
service_packs = ServicePack.where('remained_units <= total_units / 100.0 * threshold1').preload(:consuming_projects)
ServicePack.find_each do |sp|
sp.consuming_projects.find_each do |project|
users = User.allowed(:see_assigned_service_packs, project)
users.each do |user| ServicePacksMailer.notify_under_threshold1(user, sp).deliver_later end
end
end
end
end
\ No newline at end of file
class SPAssignmentManager
module SPAssignmentManager
# Implementation is subject to change.
def assign_to(service_pack, project)
if service_pack.available?
if assignable = service_pack.assigns.where(assigned: true).empty?
ActiveRecord::Base.transaction do
# one query only
# project.assigns.update_all!(assigned: false)
@assign_record = service_pack.assigns.find_by(project_id: project.id) || project.assigns.new
@assign_record.assigned = true
@assign_record.service_pack_id = service_pack.id
@assign_record.save!
end
# rescue ActiveRecord::RecordInvalid
# return :failed
# end
return :successful
else
return :owned
end
# binding.pry
ActiveRecord::Base.transaction do
# one query only
project.assigns.update_all(assigned: false)
@assignment = service_pack.assigns.find_by(project_id: project.id) || project.assigns.new
@assignment.assigned = true
@assignment.assign_date = Date.today
@assignment.unassign_date = service_pack.expired_date
@assignment.service_pack_id = service_pack.id if @assignment.new_record?
@assignment.save!
end
return :unassignable
end
def unassign(project)
return nil unless @assignment = project.assigns.find_by(assigned: true)
@assignment.assigned = false
@assignment.save!
true
def _unassign(project)
project.assigns.find_by(assigned: true)&.terminate # ruby >= 2.3.0 "safe navigation operator"
end
def assigned?(project)
project.assigns.where(assigned: true).empty?
end
def assignment_terminate(assignment)
assignment.assigned = false
assignment.save!
def unassigned?(project)
!assigned?(project)
end
def assignment_overdue?(assignment)
assignment.service_pack.unavailable?
def assigned?(project)
project.assigns.find_by(assigned: true)
end
end
class ExpiredSpMailer < ApplicationMailer
def expired_email(user, service_pack)
@user = user
@sp = service_pack
# binding.pry
mail to: @user.mail, subject: "The service pack #{@sp.name} has expired" do |format|
format.text
format.html
end
end
end
class ServicePackMailer < ApplicationMailer
def expired(expired_service_packs)
return if (expired_service_packs.count = 0)
@sps = expired_service_packs
user_mails = User.pluck(:mail)
mail(to: user_mails, subject: 'Service ' + pluralize(@sps.count, 'pack') + 'expired')
end
end
\ No newline at end of file
class ServicePacksMailer < ApplicationMailer
default from: 'op@example.com'
def expired_email(user, service_pack)
@user = user
@sp = service_pack
# binding.pry
mail to: @user.mail, subject: "The service pack #{@sp.name} has expired" do |format|
format.text
format.html
end
end
def notify_under_threshold1(user, service_pack)
@user = user
@sp = service_pack
# binding.pry
mail to: @user.mail, subject: "The service pack #{@sp.name} is running out" do |format|
format.text
format.html
end
end
end
class Assign < ApplicationRecord
belongs_to :service_pack
belongs_to :project
scope :active, -> {where(assigned: true)}
belongs_to :service_pack
belongs_to :project
scope :active, ->{where("assigned = ? and unassign_date > ?", true, Date.today)}
def terminate
self.assigned = false
self.unassign_date = Date.today
self.save!
end
def overdue?
service_pack.unavailable?
end
end
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment