Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Populate user subjects based on documents #634

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ gem 'importmap-rails', '~> 2.1'
gem "omniauth", "~> 2.1"
gem "omniauth-google-oauth2", '~> 1.2'
gem "omniauth-rails_csrf_protection", '~> 1.0'
gem 'pdf-reader'
gem 'pg', '~> 1.5'
gem 'puma', '~> 6.5'
gem 'rollbar', '~> 3.6'
Expand Down
13 changes: 13 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
Ascii85 (2.0.1)
actioncable (8.0.0.1)
actionpack (= 8.0.0.1)
activesupport (= 8.0.0.1)
Expand Down Expand Up @@ -74,6 +75,7 @@ GEM
uri (>= 0.13.1)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
afm (0.2.2)
ast (2.4.2)
base64 (0.2.0)
bcrypt (3.1.20)
Expand Down Expand Up @@ -126,6 +128,7 @@ GEM
ffi (1.15.5)
globalid (1.2.1)
activesupport (>= 6.1)
hashery (2.1.2)
hashie (5.0.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
Expand Down Expand Up @@ -222,6 +225,12 @@ GEM
parser (3.3.7.0)
ast (~> 2.4.1)
racc
pdf-reader (2.13.0)
Ascii85 (>= 1.0, < 3.0, != 2.0.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
ttfunk
pg (1.5.9)
psych (5.2.2)
date
Expand Down Expand Up @@ -320,6 +329,7 @@ GEM
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
rubyzip (2.4.1)
sassc (2.4.0)
ffi (~> 1.9)
Expand Down Expand Up @@ -360,6 +370,8 @@ GEM
thor (1.3.2)
tilt (2.0.10)
timeout (0.4.2)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
Expand Down Expand Up @@ -402,6 +414,7 @@ DEPENDENCIES
omniauth (~> 2.1)
omniauth-google-oauth2 (~> 1.2)
omniauth-rails_csrf_protection (~> 1.0)
pdf-reader
pg (~> 1.5)
puma (~> 6.5)
rails (~> 8.0.0)
Expand Down
29 changes: 29 additions & 0 deletions app/assets/stylesheets/_utility.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@
color: #19a819 !important;
}

.color-red {
color: #ff0000 !important;
}

.d-none {
display: none !important;
}

@mixin generate-margin-classes($direction, $property) {
@for $i from 0 through 6 {
.m#{$direction}-#{$i} {
margin-#{$property}: #{$i * 0.5}rem !important;
}
}
}

@include generate-margin-classes(t, top);
@include generate-margin-classes(r, right);
@include generate-margin-classes(b, bottom);
@include generate-margin-classes(l, left);

@mixin generate-margin-axis-classes($axis, $property1, $property2) {
@for $i from 0 through 6 {
.m#{$axis}-#{$i} {
margin-#{$property1}: #{$i * 0.5}rem !important;
margin-#{$property2}: #{$i * 0.5}rem !important;
}
}
}

@include generate-margin-axis-classes(x, left, right);
@include generate-margin-axis-classes(y, top, bottom);
5 changes: 5 additions & 0 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ body {
padding-left: 0px;
}

.subtle-text {
font-size: 0.7em;
color: #666;
}

.show-password-button {
margin-right: -20px;
}
46 changes: 46 additions & 0 deletions app/controllers/academic_histories_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require 'pdf-reader'

class AcademicHistoriesController < ApplicationController
def index
redirect_to new_academic_history_path
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Por que definis el index si el boton en el menu lleva al new? Se podria sacar?

end

def new
end

def create
file = params[:file]
@failed_entries = []
@successful_entries = []

if file && file.content_type == 'application/pdf'
academic_entries = AcademicHistory::PdfProcessor.process(file)
academic_entries.each do |entry|
save_academic_entry(entry) if entry.approved?
end
else
flash[:error] = 'Please upload a valid PDF file'
end
render :new
end

private

def save_academic_entry(entry)
subject_match = Subject.where("lower(unaccent(name)) = lower(unaccent(?))", entry.name)
subject_match = subject_match.select { |subject| subject.credits == entry.credits.to_i }
active_subjects = subject_match.select { |subject| !subject.inactive? }
if subject_match.length == 1
save_subject(subject_match.first)
elsif active_subjects.length == 1
save_subject(active_subjects.first)
else
@failed_entries << entry
end
end

def save_subject(subject)
current_student.force_add_subject(subject)
@successful_entries << subject
end
end
6 changes: 6 additions & 0 deletions app/models/base_student.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ def add(approvable)
end
end

def force_add_subject(subject)
ids << subject.exam.id if subject.exam
ids << subject.course.id if subject.course
save!
end

def remove(approvable)
ids.delete(approvable.id)
if !approvable.is_exam? && approvable.subject.exam.present? && ids.include?(approvable.subject.exam.id)
Expand Down
35 changes: 35 additions & 0 deletions app/views/academic_histories/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<h3 class="mdc-deprecated-list-group__subheader mdc-typography--headline5 sign-in mb-3">Resumen por Grupo</h3>
<p class="mb-3">Carga tu escolaridad para marcar automaticamente las materias que ya cursaste.</p>
<p>1) Entrar a <a href="https://bedelias.udelar.edu.uy/" target="_blank">Bedelias</a> e iniciar session</p>
<p>2) Entrar a "Estudiante" -> "Escolaridad" -> "Solicitar Escolaridad"</p>
<p>3) Seleccionar tu carrera y "No" en "Resultados Intermedios"</p>
<%= form_with url: academic_histories_path, method: :post, local: true, html: { multipart: true } do |form| %>
<div class="sign-in my-6">
<%= form.label :file, "Upload PDF" %>
<%= form.file_field :file, accept: "application/pdf" %>

<%= form.button class: "mdc-button mdc-button--raised" do %>
<span class="mdc-button__ripple"></span>
<span class='mdc-button__label'>
Subir
</span>
<% end %>
</div>
<% end %>
<div class="sign-in">
<% if @successful_entries&.present? %>
<p class="color-green mb-2"><%= @successful_entries.length %> Materias marcadas correctamente</p>
<% @successful_entries.each do |entry| %>
<p><%= entry.code %> - <%= entry.name %></p>
<% end %>
<% end %>
<% if @failed_entries&.present? %>
<p class="color-red my-2"><%= @failed_entries.length %> Materias no encontradas</p>
<% @failed_entries.each do |entry| %>
<p><%= entry.name %></p>
<% end %>
<% end %>
</div>
<p class="subtle-text sign-in mt-3">
La escolaridad no incluye codigos de materias, por lo que en caso de multiples candidatos con el mismo nombre, se marca la materia mas reciente y actualmente activa
</p>
6 changes: 6 additions & 0 deletions app/views/shared/_user_menu.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
Editar Perfil
<span>
<% end %>
<%= link_to new_academic_history_path, class: "mdc-deprecated-list-item", tabindex: 0 do %>
<span class="mdc-deprecated-list-item__ripple"></span>
<span class="mdc-deprecated-list-item__text">
Importar Materias
<span>
<% end %>
<%= link_to destroy_user_session_path, method: :delete, class: "mdc-deprecated-list-item", tabindex: 0 do %>
<span class="mdc-deprecated-list-item__ripple"></span>
<span class="mdc-deprecated-list-item__text">
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@

resources :current_optional_subjects, only: :index

resources :academic_histories, only: [:new, :create, :index]

resources :planned_subjects, only: [:index, :create, :destroy], param: :subject_id
end
41 changes: 41 additions & 0 deletions lib/academic_history/pdf_processor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'pdf-reader'

module AcademicHistory
module PdfProcessor
AcademicEntry = Struct.new(:name, :credits, :number_of_failures, :date_of_completion, :grade) do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creo que deberias mover esto a una clase AcademicHistory::AcademicEntry luego en esa clase podes definir un metodo como save_academic_entry_to_user(user) y moves el codigo que dejaste en el controller para ahi.

Además, asi podemos hacer test sobre esa clase.

def approved?
grade != '***'
end
end
extend self

def process(file)
Enumerator.new do |yielder|
reader = PDF::Reader.new(file.path)

reader.pages.each do |page|
page.text.split("\n").each do |line|
line.match(subject_regex) do |match|
yielder << AcademicEntry.new(match[1], match[2], match[3], match[4], match[5])
end
end
end
end
end

# SubjectName Credits NumberOfFailures DateOfCompletion Concept
def subject_regex
/\s*(.*?)\s+(\d+)\s+(\d+)\s+(#{date_regex.source})\s+(#{concept_regex.source})/
end

# The date can be either a date in the format DD/MM/YYYY or **********
def date_regex
/\*{10}|\d\d\/\d\d\/\d\d\d\d/
end

# The concept can be either a number from 0 to 12, S/N or ***
def concept_regex
/Aceptable|Bueno|Muy Bueno|Excelente|S\/C|\*{3}/
end
end
end
79 changes: 79 additions & 0 deletions spec/lib/academic_history/pdf_processor_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require 'rails_helper'

RSpec.describe AcademicHistory::PdfProcessor, type: :lib do
# rubocop:disable Layout/LineLength
let(:pdf_mock) {
[
double('page', text: " Test Subject 1 10 0 20/02/2024 Aceptable"),
double('page', text: " Test Subject 2 9 0 20/02/2024 Bueno"),
double('page', text: " Test Subject 3 8 0 20/02/2024 Muy Bueno"),
double('page', text: " Test Subject 4 7 0 20/02/2024 Excelente"),
double('page', text: " Test Subject 5 6 0 20/02/2024 S/C"),
double('page', text: " Failed Subject 1 8 1 ********** ***")
]
}
# rubocop:enable Layout/LineLength

let(:academic_entries) { described_class::AcademicEntry }

describe '.process' do
let(:file) { double('file', path: 'path') }
let(:reader) { double('reader') }
let(:academic_entries_list) {
[
academic_entries.new('Test Subject 1', '10', '0', '20/02/2024', 'Aceptable'),
academic_entries.new('Test Subject 2', '9', '0', '20/02/2024', 'Bueno'),
academic_entries.new('Test Subject 3', '8', '0', '20/02/2024', 'Muy Bueno'),
academic_entries.new('Test Subject 4', '7', '0', '20/02/2024', 'Excelente'),
academic_entries.new('Test Subject 5', '6', '0', '20/02/2024', 'S/C'),
academic_entries.new('Failed Subject 1', '8', '1', '**********', '***')
]
}

before do
allow(PDF::Reader).to receive(:new).with('path').and_return(reader)
allow(reader).to receive(:pages).and_return(pdf_mock)
end

it 'returns a list of academic entries' do
expect(described_class.process(file).to_a).to all(be_an(academic_entries))
end

it 'parses the text from the pdf file' do
expect(described_class.process(file).to_a).to eq(academic_entries_list)
end
end

describe 'academic entry' do
let(:entry) { academic_entries.new('Test Subject', '10', '0', '20/02/2024', 'Aceptable') }
let(:failed_entry) { academic_entries.new('Failed Subject', '10', '1', '**********', '***') }

it 'has a name' do
expect(entry.name).to eq('Test Subject')
end

it 'has a number of credits' do
expect(entry.credits).to eq('10')
end

it 'has a number of failures' do
expect(entry.number_of_failures).to eq('0')
end

it 'has a date of completion' do
expect(entry.date_of_completion).to eq('20/02/2024')
end

it 'has a grade' do
expect(entry.grade).to eq('Aceptable')
end

it 'is approved if the grade is not ***' do
expect(entry).to be_approved
end

it 'is not approved if the grade is ***' do
expect(failed_entry).not_to be_approved
end
end
end