Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7d0aa19
added initial support for insights api in backend
wderocco8 Jan 17, 2024
4ac3395
added more support for InsightsService
wderocco8 Jan 18, 2024
4f02491
updated InsightsService
wderocco8 Jan 24, 2024
2cca314
merged dev/0.2 into insights
wderocco8 Feb 8, 2024
8e8b368
fudged up frontend commit message, this should be: updated InsightsSe…
wderocco8 Feb 8, 2024
2243f84
completed InsightsService functionality
wderocco8 Feb 9, 2024
bf39512
renamed variables
wderocco8 Feb 9, 2024
4720a6e
fixed bug -> handled metric=colleges case with continue statement
wderocco8 Feb 9, 2024
22b30a4
removed case for website/linkedin -> not necessary
wderocco8 Feb 9, 2024
b7ed429
added title case for major/minor and fixed bugs for avg gpa
wderocco8 Feb 9, 2024
6700896
fixed bug -> checking for missing website/linkedin
wderocco8 Feb 10, 2024
1c7fc19
handled N/A gpa case
wderocco8 Feb 10, 2024
54e3003
created unit tests for insights service -> no clue what is breaking
wderocco8 Feb 11, 2024
3576562
found bug --> accidentally commented out InsightsService driver funct…
wderocco8 Feb 11, 2024
02cfc8b
completed test_insights_service
wderocco8 Feb 11, 2024
7f8b441
created JSON files to store sample return values and integrated into …
wderocco8 Feb 15, 2024
a33b5d5
added API pytests for insights
wderocco8 Feb 15, 2024
6223061
slight refactor -> used snake case
wderocco8 Feb 15, 2024
e4835c3
refactor -> switched to most common gradYear (instead of average) and…
wderocco8 Feb 16, 2024
655c2d4
bug fix -> display insights template for 0 applicants
wderocco8 Feb 16, 2024
9839ab2
bug fix -> switched to 'hasURL' instead of 'True' (clears up confusio…
wderocco8 Feb 16, 2024
a29b9db
used Jinyoung's lambda shortcut
wderocco8 Feb 16, 2024
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 app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from chalicelib.api.listings import listings_api
from chalicelib.api.applicants import applicants_api
from chalicelib.api.announcements import announcements_api
from chalicelib.api.insights import insights_api
from chalicelib.api.members import members_api

app = Chalice(app_name="zap")
app.register_blueprint(announcements_api)
app.register_blueprint(listings_api)
app.register_blueprint(applicants_api)
app.register_blueprint(insights_api)
app.register_blueprint(members_api)


Expand Down
13 changes: 13 additions & 0 deletions chalicelib/api/insights.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# TO BE COMPLETED: create api routes for analytics
from chalice import Blueprint
from chalicelib.services.InsightsService import insights_service
from chalicelib.handlers.error_handler import handle_exceptions
from pydantic import ValidationError

insights_api = Blueprint(__name__)


@insights_api.route("/insights/listing/{listing_id}", methods=["GET"], cors=True)
def get_listing_insights(listing_id):
"""Get insights from <listing_id>"""
return insights_service.get_insights_from_listing(listing_id)
172 changes: 172 additions & 0 deletions chalicelib/services/InsightsService.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# TO BE COMPLETED: create service to perform analytics (used in API...)
from chalicelib.db import db

class InsightsService:
def __init__(self):
pass

def get_insights_from_listing(self, id: str):
''' driver function of insights (returns both `dashboard` and `distribution`) '''

# fetch applicants from `get_applicants` endpoint in `db.py`
data = db.get_applicants(table_name="zap-applications", listing_id=id)

# call helper functions
# NOTE: `get_dashboard_insights` updates the data object to ensure all majors/minors are Title() cased
dashboard = InsightsService._get_dashboard_insights(data)
distribution = InsightsService._get_pie_chart_insights(data)

return dashboard, distribution

# private method (kinda)
def _get_dashboard_insights(data):
# initialize metrics
majors = {}
grad_years = {}
num_applicants = len(data)
avg_gpa = 0
count_gpa = 0

dashboard = {
"applicantCount": 0,
"avgGpa": "N/A",
"commonMajor": "N/A",
"commonGradYear": "N/A",
}

if num_applicants < 1:
return dashboard

# iterate over each applicant and perform analytics
for applicant in data:
# convert major/minor to title case
applicant["major"] = applicant["major"].title()
applicant["minor"] = applicant["minor"].title()

gpa, grad_year, major = applicant["gpa"], applicant["gradYear"], applicant["major"]

# attempt conversions (if fail, then skip)
try:
float_gpa = float(gpa)
avg_gpa += float_gpa
count_gpa += 1
except ValueError:
print("skipping gpa: ", gpa)
pass
try:
float_grad = float(grad_year)
if float_grad in grad_years:
grad_years[float_grad] += 1
else:
grad_years[float_grad] = 1
except ValueError:
print("skipping gradYear: ", grad_year)
pass

# parse majors (if non-empty)
if major:
if major in majors:
majors[major] += 1
else:
majors[major] = 1

avg_gpa /= count_gpa

# calculate most common major/gradYear
common_major, count_common_major = max(majors.items(), key=lambda x: x[1])
common_grad_year, count_common_grad_year = max(grad_years.items(), key=lambda x: x[1])


dashboard = {
"applicantCount": num_applicants,
"avgGpa": round(avg_gpa, 1), # round to 1 decimal place (e.g. 3.123 -> 3.1)
"commonMajor": common_major.title(),
# "countCommonMajor": count_common_major, # TO-DO: maybe do something with common major counts
"commonGradYear": int(common_grad_year),
# "avgResponseLength": 0 # TO-DO: maybe implement parsing for response lengths
}

return dashboard


def _get_pie_chart_insights(data):
''' helper function for pie charts (should be function, not method within InsightsService) '''

# initialize return object
# value (list) structure : [ {name: string, value: int, applicants: Applicant[]}, ... , ... ]
distribution = {
"colleges": [],
"gpa": [],
"gradYear": [],
"major": [],
"minor": [],
"linkedin": [],
"website": [],
}

# list of fields we want to consider
fields = ["colleges", "gpa", "gradYear", "major", "minor", "linkedin", "website"]

def findInsightsObject(metric, metric_val):
''' helper to the helper lol -> checks for previously added metric_name '''
# check if college exists in `distribution["colleges"]`
found_object = None

for distribution_object in distribution[metric]:
if distribution_object["name"] == metric_val:
found_object = distribution_object
break

return found_object

for applicant in data:
# iterate over applicant dictionary
for metric, val in applicant.items():

# case 1: ignore irrelevant metrics
if metric not in fields:
continue

# case 2: metric is a url
if metric in ["linkedin", "website"]:
val = 'N/A' if (not val or val == 'N/A') else 'hasURL'

# case 3: handle other metrics with mepty val (attempt to handle some edge cases) # TO-DO: update Form.tsx in frontend to prevent bad inputs
elif metric in ['minor', 'gpa'] and (not val or val.lower() in ['na', 'n/a', 'n a', 'n / a']):
# general case
val = 'N/A'

# case 4: colleges -> iterate over colleges object
elif metric == "colleges":
for college, status in val.items():
# edge case: if status is false, skip (shouldn't contribute to count)
if not status:
continue

# check if college exists in `distribution["colleges"]`
found_college = findInsightsObject(metric, college)

if found_college:
found_college["value"] += 1
found_college["applicants"] += [applicant]
else:
newCollege = {"name": college, "value": 1, "applicants": [applicant]}
distribution[metric] += [newCollege]

# skip to next metric
continue

# handle remaining fields
found_object = findInsightsObject(metric, val)

if found_object:
found_object["value"] += 1
found_object["applicants"] += [applicant]
else:
new_object = {"name": val, "value": 1, "applicants": [applicant]}
distribution[metric] += [new_object]

return distribution


insights_service = InsightsService()
27 changes: 27 additions & 0 deletions tests/api/test_insights.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from chalice.app import Request
from chalice.test import Client
from unittest.mock import MagicMock, patch
import json

from app import app

with open('tests/fixtures/sample_dashboard.json') as f:
SAMPLE_DASHBOARD = json.load(f)

with open('tests/fixtures/sample_distribution.json') as f:
SAMPLE_DISTRIBUTION = json.load(f)


def test_get_insights_from_listing():
# Create a Chalice test client
with Client(app) as client:
# Mock applicant_service's get method
with patch(
"chalicelib.services.InsightsService.insights_service.get_insights_from_listing"
) as mock_get_insights_from_listing:
mock_get_insights_from_listing.return_value = [SAMPLE_DASHBOARD, SAMPLE_DISTRIBUTION]
response = client.http.get(f"/insights/listing/test_listing_id")

# Check the response status code and body
assert response.status_code == 200
assert response.json_body == [SAMPLE_DASHBOARD, SAMPLE_DISTRIBUTION]
Loading