(\{%-? \s* assign \s* v\d+ \s* = \s* fields\[ \s* ['"].*?['"] \s* \] \s* (\| \s* times: \s* \d.*? \s*)? -?%\} \s*)+)
+ \{%-? \s* assign \s* sum \s* = \s* v1 \s* (\| \s* plus: \s* v\d+ \s*)* -?%\} \s*
+ \{\{-? \s* sum \s* \| \s* divided_by: \s* (?.*?) \s* -?\}\}
+ /mx
+
+ ASSIGNMENT_PATTERN_LIQUID = /
+ \{%-? \s* assign \s* v(?\d+) \s* = \s* fields\[ \s* ['"](?.*?)['"] \s* \] \s* (\| \s* times: \s* (?\d.*?) \s*)? -?%\} \s*
+ /mx
+
+ def extract_weight_ruby(sexp, mult)
+ v1 = extract_sum(sexp)
+ case v1
+ in [{field:}]
+ return [{field:, mult:}]
+ else
+ return false
+ end
+ end
+
+ def extract_sum_ruby(sexp)
+ case sexp
+ in [:aref,
+ [:vcall, [:@ident, "fields", _]],
+ [:args_add_block,
+ [[:string_literal, [:string_content, [:@tstring_content, String => field, _]]]],
+ false]
+ ]
+ return [{field:}]
+ in [:binary, v1, :+, v2]
+ return extract_sum_ruby(v1) + extract_sum_ruby(v2)
+ in [:binary, [_, String => mult, _], :*, v1]
+ return extract_weight_ruby(v1, mult)
+ in [:binary, v1, :*, [_, String => mult, _]]
+ return extract_weight_ruby(v1, mult)
+ else
+ false
+ end
+ rescue
+ false
+ end
+
+ def convert_ruby_to_liquid(code)
+ if code =~ MEDIAN_PATTERN_RUBY
+ return "{{- committees | median: '#{$~[:field]}' -}}"
+ elsif code =~ AVG_PATTERN_RUBY
+ return "{{- committees | avg: '#{$~[:field]}' -}}"
+ else
+ # Match (fields['a'] + fields['b'])/2
+ case Ripper.sexp(code)
+ in [:program,
+ [[:binary,
+ [:paren,
+ [val]],
+ :/,
+ [_, div, _]]]]
+ fields = extract_sum_ruby(val)
+ if fields
+ result = []
+ names = []
+ fields.each_with_index do |field, index|
+ names << ["v#{index + 1}"]
+ mult_str = ""
+ mult_str = " | times: #{field[:mult]}" if field.include?(:mult)
+ result << "{%- assign v#{index + 1} = fields['#{field[:field]}']#{mult_str} -%}"
+ end
+ result << "{%- assign sum = #{names.join(" | plus: ")} -%}"
+ result << "{{- sum | divided_by: #{div.to_f} -}}"
+ return result.join("\n")
+ end
+ else
+ return false
+ end
+ end
+ return false
+ end
+
+ def convert_liquid_to_ruby(code)
+ if code =~ MEDIAN_PATTERN_LIQUID
+ return <<~CODE.strip
+ grades = committees.map{ |x| x["#{$~[:field]}"].to_f }
+ sorted = grades.sort
+ length = grades.length
+ (sorted[(length - 1) / 2] + sorted[length / 2]) / 2.0
+ CODE
+ elsif code =~ AVG_PATTERN_LIQUID
+ return <<~CODE.strip
+ grades = committees.map{ |x| x["#{$~[:field]}"].to_f }
+ grades.sum / grades.size
+ CODE
+ elsif code =~ FIELD_AVG_PATTERN_LIQUID
+ div = $~[:div]
+ fields = []
+ $~[:assignments].scan(ASSIGNMENT_PATTERN_LIQUID) do |group|
+ mult = group[2] ? " * #{group[2]}" : ""
+ fields << "fields[\"#{group[1]}\"]#{mult}"
+ end
+ if fields
+ return "(#{fields.join(' + ')}) / #{div}"
+ end
+ end
+ return false
+ end
+
+ def up
+ Admissions::FormField.where(field_type: Admissions::FormField::CODE).each do |form_field|
+ config = JSON.parse(form_field.configuration)
+ next if config["template_type"] != "Ruby"
+ converted = convert_ruby_to_liquid(config["code"])
+ puts "#{form_field.name} (#{form_field.form_template.name})"
+ if converted
+ config["code"] = converted
+ config["template_type"] = "Liquid"
+ form_field.configuration = JSON.dump(config)
+ form_field.save!
+ puts '..Converted'
+ else
+ puts '..Incomplete conversion. Kept Ruby'
+ end
+ end
+ end
+
+ def down
+ Admissions::FormField.disable_erb_validation! do
+ Admissions::FormField.where(field_type: Admissions::FormField::CODE).each do |form_field|
+ config = JSON.parse(form_field.configuration)
+ next if config["template_type"] != "Liquid"
+ converted = convert_liquid_to_ruby(config["code"])
+ puts "#{form_field.name} (#{form_field.form_template.name})"
+ if converted
+ config["code"] = converted
+ config["template_type"] = "Ruby"
+ form_field.configuration = JSON.dump(config)
+ form_field.save!
+ puts '..Converted'
+ else
+ puts '..Incomplete conversion. Kept Liquid'
+ end
+ end
+ end
+ end
+
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0e0e4348..cee1f55d 100644
--- a/db/schema.rb
+++ b/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: 2025_05_04_195245) do
+ActiveRecord::Schema[7.0].define(version: 2025_05_15_173026) do
create_table "accomplishments", force: :cascade do |t|
t.integer "enrollment_id"
t.integer "phase_id"
diff --git a/lib/code_evaluator.rb b/lib/code_evaluator.rb
index c167dc57..865ff620 100644
--- a/lib/code_evaluator.rb
+++ b/lib/code_evaluator.rb
@@ -1,13 +1,47 @@
-# Copyright (c) Universidade Federal Fluminense (UFF).
-# This file is part of SAPOS. Please, consult the license terms in the LICENSE file.
-
-# frozen_string_literal: true
-
-module CodeEvaluator
- def self.evaluate_code(formula, **bindings)
- b = binding
- bindings.each do |var, val| b.local_variable_set(var, val) end
-
- b.eval(formula)
- end
-end
+# Copyright (c) Universidade Federal Fluminense (UFF).
+# This file is part of SAPOS. Please, consult the license terms in the LICENSE file.
+
+# frozen_string_literal: true
+
+module CodeEvaluator
+ def self.create_formatter(bindings, template_type, drops = nil)
+ if template_type == "ERB"
+ cls = ErbFormatter
+ else
+ cls = LiquidFormatter
+ bindings = CodeEvaluator.dropify_bindings(bindings, drops)
+ end
+ cls.new(bindings)
+ end
+
+ def self.evaluate_code(code, bindings, template_type, drops = nil)
+ if template_type == "Ruby"
+ b = binding
+ bindings.each do |var, val|
+ b.local_variable_set(var, val)
+ end
+ b.eval(code)
+ elsif template_type == "ERB"
+ ErbFormatter.new(bindings).format(code)
+ else
+ bindings = CodeEvaluator.dropify_bindings(bindings, drops)
+ LiquidFormatter.new(bindings).format(code)
+ end
+ end
+
+ private
+ def self.dropify_bindings(bindings, drops)
+ drops ||= {}
+ new_bindings = bindings.map { |k, v| CodeEvaluator.load_drop(drops, k, v) }.to_h
+ new_bindings["variables"] = VariablesDrop.new unless new_bindings.key?("variables")
+ new_bindings
+ end
+
+ def self.load_drop(drops, key, value)
+ return [key, value] unless drops.key?(key) # Does not convert if key is not in drops map
+ cls = drops[key]
+ return [key.to_s, value] if cls == :value
+ return [key.to_s, value.map { |v| cls[0].new(v) }] if cls.kind_of?(Array)
+ [key.to_s, cls.new(value)]
+ end
+end
diff --git a/lib/formatter_factory.rb b/lib/formatter_factory.rb
deleted file mode 100644
index eb5c2870..00000000
--- a/lib/formatter_factory.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (c) Universidade Federal Fluminense (UFF).
-# This file is part of SAPOS. Please, consult the license terms in the LICENSE file.
-
-# frozen_string_literal: true
-
-module FormatterFactory
- def self.create_formatter(bindings, template_type)
- if template_type == "ERB"
- cls = ErbFormatter
- else
- cls = LiquidFormatter
- end
- cls.new(bindings)
- end
-end
diff --git a/lib/liquid_formatter.rb b/lib/liquid_formatter.rb
index a405e735..209aaf54 100644
--- a/lib/liquid_formatter.rb
+++ b/lib/liquid_formatter.rb
@@ -10,7 +10,40 @@ def localize(date, format = :default)
I18n.localize(time, format: format.to_sym)
end
+ def avg(input, property = nil)
+ values_for_sum = self.to_number(input, property)
+ result = Liquid::StandardFilters::InputIterator.new(values_for_sum, context).sum do |item|
+ Liquid::Utils.to_number(item)
+ end
+ result.to_f / values_for_sum.size
+ end
+
+ def median(input, property = nil)
+ values = self.to_number(input, property)
+ sorted = values.sort
+ length = values.size
+ (sorted[(length - 1) / 2] + sorted[length / 2]) / 2.0
+ end
+
alias_method :l, :localize
+
+ private
+ def to_number(input, property = nil)
+ ary = Liquid::StandardFilters::InputIterator.new(input, context)
+ return 0 if ary.empty?
+
+ ary.map do |item|
+ if property.nil?
+ Liquid::Utils.to_number(item)
+ elsif item.respond_to?(:[])
+ Liquid::Utils.to_number(item[property])
+ else
+ 0
+ end
+ rescue TypeError
+ raise Liquid::ArgumentError, "cannot select the property '#{Liquid::Utils.to_s(property)}'"
+ end
+ end
end
class RoleEmail < Liquid::Tag
diff --git a/spec/features/dismissals_spec.rb b/spec/features/dismissals_spec.rb
index fd745fb5..a3611ab1 100644
--- a/spec/features/dismissals_spec.rb
+++ b/spec/features/dismissals_spec.rb
@@ -30,7 +30,7 @@
@destroy_all << @record = FactoryBot.create(:dismissal, enrollment: @enrollment1, date: Date.today, dismissal_reason: @dismissal_reason1)
@destroy_all << FactoryBot.create(:dismissal, enrollment: @enrollment2, date: Date.today, dismissal_reason: @dismissal_reason2)
- @destroy_all << FactoryBot.create(:dismissal, enrollment: @enrollment3, date: Date.today, dismissal_reason: @dismissal_reason1)
+ @destroy_all << FactoryBot.create(:dismissal, enrollment: @enrollment3, date: 2.years.ago.at_beginning_of_month.to_date, dismissal_reason: @dismissal_reason1)
end
after(:each) do
@destroy_later.each(&:delete)
@@ -128,5 +128,26 @@
click_button "Buscar"
expect(page.all("tr td.enrollment-column").map(&:text)).to eq ["M03 - Carol"]
end
+
+ it "should be able to search by enrollment level" do
+ expect(page.all("select#search_level option").map(&:text)).to eq ["", "Doutorado"]
+ find(:select, "search_level").find(:option, text: "Doutorado").select_option
+ click_button "Buscar"
+ expect(page.all("tr td.enrollment-column").map(&:text)).to eq ["M02 - Ana", "M01 - Bia", "M03 - Carol"]
+ end
+
+ it "should be able to search by dismissal date" do
+ year = 2.years.ago.year
+ find(:select, "search_date_year").find(:option, text: year).select_option
+ click_button "Buscar"
+ expect(page.all("tr td.enrollment-column").map(&:text)).to eq ["M03 - Carol"]
+ end
+
+ it "should be able to search by dismissal reason" do
+ expect(page.all("select#search_dismissal_reason option").map(&:text)).to eq ["Selecione uma opção", "Reprovado", "Titulação"]
+ find(:select, "search_dismissal_reason").find(:option, text: "Reprovado").select_option
+ click_button "Buscar"
+ expect(page.all("tr td.enrollment-column").map(&:text)).to eq ["M02 - Ana", "M03 - Carol"]
+ end
end
end
diff --git a/spec/models/email_template_spec.rb b/spec/models/email_template_spec.rb
index 44f3ad4f..d2e53249 100644
--- a/spec/models/email_template_spec.rb
+++ b/spec/models/email_template_spec.rb
@@ -146,7 +146,7 @@
:email_template, enabled: false,
to: "t{{ temp }}", subject: "s{{ temp }}", body: "b{{ temp }}"
)
- expect(template.prepare_message(temp: 1)).to eq({
+ expect(template.prepare_message("temp" => 1)).to eq({
to: "t1",
subject: "s1",
body: "b1",