Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ gem 'strip_attributes'
gem 'table_print', require: false
# Email validations
gem 'valid_email2'
# AWS S3
gem 'aws-sdk-s3'

gem 'json', '~> 2.0'

Expand Down
18 changes: 18 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ GEM
ar_outer_joins (0.2.0)
activerecord (>= 3.2)
ast (2.4.1)
aws-eventstream (1.1.0)
aws-partitions (1.412.0)
aws-sdk-core (3.110.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.40.0)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.87.0)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2)
aws-eventstream (~> 1, >= 1.0.2)
backport (1.1.2)
bcrypt (3.1.16)
benchmark (0.1.1)
Expand Down Expand Up @@ -142,6 +158,7 @@ GEM
concurrent-ruby (~> 1.0)
interception (0.5)
jaro_winkler (1.5.4)
jmespath (1.4.0)
json (2.5.1)
json-schema (2.8.1)
addressable (>= 2.4)
Expand Down Expand Up @@ -387,6 +404,7 @@ DEPENDENCIES
active_attr
active_record_extended
annotate
aws-sdk-s3
bcrypt (~> 3.1.7)
better_errors
binding_of_caller
Expand Down
10 changes: 9 additions & 1 deletion app/commands/send_grid_email.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ def pony_payload
@pony_payload
end

def logger
@logger ||= Logger.new(Rails.root.join('log', 'email.log'))
end

def work
Pony.mail(pony_payload)
if Rails.env.development?
logger.info(pony_payload.inspect)
else
Pony.mail(pony_payload)
end
end
end
27 changes: 27 additions & 0 deletions app/commands/upload_to_s3.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_literal_string: true
class UploadToS3 < ApplicationCommand
attribute :content
attribute :path
attribute :filename
attribute :expires_in, default: 3600

validates :content, presence: true
validates :path, presence: true
validates :filename, presence: true

def work
s3 = Aws::S3::Resource.new(
region: ENV['AWS_REGION'],
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
)

bucket = s3.bucket(ENV['AWS_BUCKET'])
obj = bucket.object(self.path)
obj.put(body: self.content, content_disposition: "attachment; filename=#{self.filename}")

obj.presigned_url(:get, expires_in: self.expires_in)
rescue => e
puts e.inspect
end
end
9 changes: 9 additions & 0 deletions app/controllers/locations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ module LocationsController
render json: Reports::Location.new(location).to_h
end

post '/v1/locations/:location_id/people-report' do
location_id = params[:location_id]
location = Location.find_by_id_or_permalink(location_id)
ensure_or_forbidden! { location && current_user.admin_at?(location) }

PeopleReportWorker.perform_async(location.id, current_user.id)
success_response
end

get '/v1/locations/:location_id/stats-overview/:date' do
location = Location.find_by_id_or_permalink!(params[:location_id])
stats = LocationStatsOverview.new(location, params[:date])
Expand Down
1 change: 1 addition & 0 deletions app/models/greenlight_status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class GreenlightStatus < ApplicationRecord

scope :recently_created, -> { where(created_at: 20.days.ago.beginning_of_day..Time.zone.now.end_of_day) }
scope :not_expired, -> { where('expiration_date <= ?', Time.current.to_date) }
scope :chronical, -> { order(submission_date: :asc) }
has_many :medical_events

before_validation :assign_associated_users_to_medical_events
Expand Down
44 changes: 44 additions & 0 deletions app/workers/people_report_worker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

class PeopleReportWorker < ApplicationWorker
def perform(location_id, admin_id)
admin = User.find(admin_id)
location = Location.find(location_id)

people_report = Reports::People.new(location)
s3_upload = UploadToS3.new(
content: people_report.csv,
path: "reports/#{location.permalink}/#{Time.now.strftime('%Y-%m-%d_%H_%M')}.csv",
filename: "report(#{location.permalink}).csv",
)
s3_upload.run

if s3_upload.succeeded?
download_url = s3_upload.result

SendGridEmail.new(
to: admin.name_with_email,
subject: I18n.t('emails.people_report.subject'),
html: eval(html_template),
).run
end
end

private

def html_template
Erubi::Engine.new(<<~HTML).src
<h2><%= I18n.t('emails.people_report.title') %></h2>

<p>
<%= I18n.t('emails.people_report.body', location: location.name) %>
</p>

<p style="font-weight:bold">
<a href="<%= download_url %>">
<%= I18n.t('emails.people_report.action') %>
</a>
</p>
HTML
end
end
4 changes: 4 additions & 0 deletions client/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ export async function updateLocationAccount(
return entity
}

export async function requestPeopleReport(location: Location) {
await v1.post(`/locations/${location.id}/people-report`)
}

export async function checkLocationRegistrationCode(locationId: string, registrationCode: string): Promise<any> {
const result = await v1.post(`/locations/${locationId}/check-registration-code`, {
registrationCode,
Expand Down
28 changes: 25 additions & 3 deletions client/src/pages/admin/AdminDashboardPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
AccordionContent,
Block, Icon, List, ListItem, Navbar, Page,
AccordionContent, f7,
Block, Icon, List, ListItem, Navbar, Page, Button,
} from 'framework7-react'
import React from 'react'
import { useEffect, useGlobal, useState } from 'reactn'
Expand All @@ -13,11 +13,13 @@ import { dynamicPaths, paths } from 'src/config/routes'
import { UsersFilter } from 'src/components/UsersFilter'
import { assertNotNull, assertNotUndefined } from 'src/helpers/util'
import { F7Props } from 'src/types'
import { getLocation, store, v1 } from 'src/api'
import { getLocation, store, v1, requestPeopleReport } from 'src/api'
import { GreenlightStatus, Location } from 'src/models'
import { GreenlightStatusTypes } from 'src/models/GreenlightStatus'
import logger from 'src/helpers/logger'
import SubmitHandler from 'src/helpers/SubmitHandler'
import FakeF7ListItem from 'src/components/FakeF7ListItem'
import Tr, { tr } from 'src/components/Tr'

interface StatsSquareProps {
title: string
Expand Down Expand Up @@ -153,6 +155,20 @@ export default function AdminDashboardPage(props: F7Props): JSX.Element {
],
},
}

const reportHandler = new SubmitHandler(f7, {
onSubmit: async () => {
assertNotNull(state.location)
await requestPeopleReport(state.location)
},
errorTitle: tr({ es: 'Solicitud fallida', en: 'Request failed.' }),
submittingMessage: tr({ es: 'Solicitando Informe ...', en: 'Requesting Report...' }),
successMessage: tr({
es: 'El informe generado se enviará a su correo electrónico.',
en: 'The generated report will be sent to your email.',
}),
})

content = (
<>
<Navbar title={`${state.location.name} Overview`}>
Expand Down Expand Up @@ -254,6 +270,12 @@ export default function AdminDashboardPage(props: F7Props): JSX.Element {
</FakeF7ListItem>
)
}

<p>
<Button fill onClick={() => reportHandler.submit()}>
<Tr en="Request Report" es="Solicitar informe" />
</Button>
</p>
</List>
</Block>
</>
Expand Down
7 changes: 7 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ en:
You requested to reset your password and here's the link!
Note that this link expires in 1 hour and can only be used once.
action: Click Here to Reset Password
people_report:
subject: ✨ Greenlight Report
title: Download the Report
body: |
"You requested a report for %{location} and here's the download link!"
Note that this link expires in 1 hour and can only be used once.
action: Click Here to Download Report
reminder:
subject: ✨ Daily Greenlight Reminder
title: Check In with Greenlight
Expand Down
80 changes: 80 additions & 0 deletions lib/reports/people.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_literal_string: true
require 'csv'

module Reports
class People
SCHOOL_COLUMNS = "First Name, Last Name, Parent First Name, Parent Last Name, \
Parent Email, Parent Phone, Submissions, Date Registered, \
Last Date Yellow, Last Date Red, Last Test Date, \
Last Test Result".split(', ').map(&:strip).freeze
OTHER_COLUMNS = "First Name, Last Name, Submissions, Date Registered, \
Last Date Yellow, Last Date Red, Last Test Date, \
Last Test Result".split(', ').map(&:strip).freeze

def initialize(location)
@location = location
@users = @location.users.includes(:parents)
@is_school = @location.category == 'school'
end

def csv
CSV.generate(headers: true) do |csv|
csv << (@is_school ? SCHOOL_COLUMNS : OTHER_COLUMNS)

@users.each do |user|
csv << csv_row(user)
end
end
end

private

def csv_row(user)
parent = user.parents.first
submission_stats = submission_stats(user)

user_row = [ user.first_name, user.last_name ]

if @is_school
user_row << parent&.first_name
user_row << parent&.last_name
user_row << parent&.email
user_row << parent&.mobile_number
end

user_row << submission_stats[:submissions].join(' ')
user_row << user.created_at.strftime('%Y-%m-%d')
user_row << submission_stats[:last_yellow_date]
user_row << submission_stats[:last_red_date]
user_row << submission_stats[:last_test_date]
user_row << submission_stats[:last_test_result]
end

def submission_stats(user)
statuses_chronical = user.greenlight_statuses.chronical

submissions = []
last_yellow_date = nil
last_red_date = nil
last_greenlight_status = statuses_chronical.last

statuses_chronical.each do |greenlight_status|
submissions << greenlight_status.status

if greenlight_status.status == 'pending'
last_yellow_date = greenlight_status.submission_date
elsif greenlight_status.status == 'recovery'
last_red_date = greenlight_status.submission_date
end
end

{
submissions: submissions,
last_yellow_date: last_yellow_date,
last_red_date: last_red_date,
last_test_date: last_greenlight_status&.submission_date,
last_test_result: last_greenlight_status&.status,
}
end
end
end
36 changes: 36 additions & 0 deletions spec/requests/locations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,42 @@
end
end

describe 'POST :location_id/people-report' do
let(:admin) { Fabricate(:user) }

before do
# skip actual uploading to S3 bucket, stub the operation
allow_any_instance_of(UploadToS3)
.to receive(:work).and_return(Faker::Internet.url)
end

subject { post_json("/v1/locations/#{location.id}/people-report", user: admin) }

it "returns 403 if a user isn't the admin of the location" do
subject

expect(response.status).to eq(403)
end

context 'when requested by the location admin' do
before do
Fabricate(
:location_account,
user: admin,
location: location,
permission_level: 'admin',
)
end

it "generates report and sends download link" do
subject

expect_success_response
expect_work(PeopleReportWorker)
end
end
end

describe 'swagger specs' do
path '/v1/locations/:location_id' do
get 'return the specified location' do
Expand Down
Loading