Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"cors": "^2.8.4",
"cron": "^1.8.2",
"date-fns": "^2.30.0",
"dayjs": "^1.11.13",
"dotenv": "^5.0.1",
"dropbox": "^10.34.0",
"express": "^4.17.1",
Expand Down
29 changes: 29 additions & 0 deletions src/controllers/githubAnalyticsController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const HgnFormResponses = require('../models/hgnFormResponse');
const UserProfile = require('../models/userProfile');
const fetchGitHubReviews = require('../services/analytics/fetchGithubReviews')(HgnFormResponses, UserProfile);

const getGitHubReviews = async (req, res) => {
const org = 'OneCommunityGlobal';
const repos = ['HighestGoodNetworkApp', 'HGNRest'];

const { duration = 'allTime', sort = 'desc', team = null } = req.query;

try {
let combinedResults = [];

const allData = await Promise.all(
repos.map((repo) => fetchGitHubReviews(org, repo, duration, sort, team))
);

combinedResults = allData.flat();

res.status(200).json(combinedResults);
} catch (err) {
console.error('Error in controller:', err);
res.status(500).json({ error: 'Failed to fetch GitHub review data' });
}
};

module.exports = {
getGitHubReviews,
};
14 changes: 7 additions & 7 deletions src/controllers/lbdashboard/biddingController.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ const userProfile = require('../../models/userProfile');
const biddingController = (Bidding) => {
const getBidListings = async (req, res) => {
try {
const page = req.headers['page'] || 1;
const size = req.headers['size'] || 10;
const village = req.headers['village'];
const page = req.headers.page || 1;
const size = req.headers.size || 10;
const {village} = req.headers;

const pageNum = parseInt(page, 10);
const sizeNum = parseInt(size, 10);
Expand Down Expand Up @@ -92,7 +92,7 @@ const biddingController = (Bidding) => {

const getBidListingById = async (req, res) => {
try {
const id = req.headers['id'];
const {id} = req.headers;
if (!id) return res.status(400).json({ error: 'Missing listing id in header' });
const listing = await Bidding.findById(id)
.populate([
Expand Down Expand Up @@ -138,7 +138,7 @@ const biddingController = (Bidding) => {
return res.status(400).json({ error: 'Invalid user or village ID' });
}

let listingData = {
const listingData = {
title,
description,
initialPrice: parseFloat(initialPrice),
Expand Down Expand Up @@ -172,7 +172,7 @@ const biddingController = (Bidding) => {

const updateBidListing = async (req, res) => {
try {
const id = req.headers['id'];
const {id} = req.headers;
if (!id) return res.status(400).json({ error: 'Missing listing id in header' });
const updateData = req.body;

Expand All @@ -193,7 +193,7 @@ const biddingController = (Bidding) => {

const deleteBidListing = async (req, res) => {
try {
const id = req.headers['id'];
const {id} = req.headers;
if (!id) return res.status(400).json({ error: 'Missing listing id in header' });
const deleted = await Bidding.findByIdAndDelete(id);
if (!deleted) {
Expand Down
6 changes: 3 additions & 3 deletions src/controllers/lbdashboard/listingAvailablityController.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const Listing = require('../../models/lbdashboard/listings');
const listingAvailablityController = (Availability) => {
const getListingAvailablity = async (req, res) => {
try {
const listingId = req.headers['listingid'];
const listingId = req.headers.listingid;
if (!listingId || !mongoose.Types.ObjectId.isValid(listingId)) {
return res.status(400).json({ error: 'Valid listingId is required in header or body' });
}
Expand All @@ -21,7 +21,7 @@ const listingAvailablityController = (Availability) => {
const createListingAvailability = async (req, res) => {
try {
const data = req.body;
const listingId = data.listingId;
const {listingId} = data;
if (!listingId || !mongoose.Types.ObjectId.isValid(listingId)) {
return res.status(400).json({ error: 'Valid listingId is required in header or body' });
}
Expand Down Expand Up @@ -155,7 +155,7 @@ const listingAvailablityController = (Availability) => {
if (!listing) {
return res.status(404).json({ error: 'Listing not found' });
}
const allowedUser = userId || req.headers['userid'];
const allowedUser = userId || req.headers.userid;
if (
!allowedUser ||
(
Expand Down
20 changes: 10 additions & 10 deletions src/controllers/lbdashboard/listingsController.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
const mongoose = require('mongoose');
const { fetchImagesFromAzureBlobStorage, saveImagestoAzureBlobStorage } = require('../../utilities/AzureBlobImages');
const userProfile = require('../../models/userProfile');
const jwt = require('jsonwebtoken');
const moment = require('moment');
const { fetchImagesFromAzureBlobStorage, saveImagestoAzureBlobStorage } = require('../../utilities/AzureBlobImages');
const userProfile = require('../../models/userProfile');
const config = require('../../config');
const Role = require('../../models/role');
const Village = require('../../models/lbdashboard/villages');

const verifyToken = async (req) => {
const token = req.headers['authorization'];
const token = req.headers.authorization;
if (!token) throw new Error('No token provided');
try {
const decoded = jwt.verify(token, config.JWT_SECRET);
Expand Down Expand Up @@ -36,11 +36,11 @@ const verifyToken = async (req) => {
const listingsController = (ListingHome) => {
const getListings = async (req, res) => {
try {
const page = req.headers['page'] || 1;
const size = req.headers['size'] || 10;
const village = req.headers['village'];
const availableFrom = req.headers['availablefrom'];
const availableTo = req.headers['availableto'];
const page = req.headers.page || 1;
const size = req.headers.size || 10;
const {village} = req.headers;
const availableFrom = req.headers.availablefrom;
const availableTo = req.headers.availableto;

const pageNum = parseInt(page, 10);
const sizeNum = parseInt(size, 10);
Expand Down Expand Up @@ -362,7 +362,7 @@ const listingsController = (ListingHome) => {

const updateListing = async (req, res) => {
try {
const id = req.headers['id'];
const {id} = req.headers;
if (!id) return res.status(400).json({ error: 'Missing listing id in header' });
const updateData = req.body;
if (req.files && req.files.length) {
Expand All @@ -381,7 +381,7 @@ const listingsController = (ListingHome) => {

const deleteListing = async (req, res) => {
try {
const id = req.headers['id'];
const {id} = req.headers;
if (!id) return res.status(400).json({ error: 'Missing listing id in header' });
const deleted = await ListingHome.findByIdAndDelete(id);
if (!deleted) {
Expand Down
7 changes: 7 additions & 0 deletions src/routes/githubAnalyticsRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const express = require('express');
const router = express.Router();
const { getGitHubReviews } = require('../controllers/githubAnalyticsController');

router.get('/github-reviews', getGitHubReviews);

module.exports = router;
171 changes: 171 additions & 0 deletions src/services/analytics/fetchGithubReviews.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
const axios = require('axios');
const dayjs = require('dayjs');
const NodeCache = require('node-cache');

/**
* Controller for user skills profile operations
*
* @param {Object} HgnFormResponses - The HgnFormResponses model
* @param {Object} UserProfile - The UserProfile model
* @returns {Object} Controller methods
*/

const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const BASE_URL = 'https://api.github.com';
const cache = new NodeCache({ stdTTL: 3600 }); // Cache for 1 hour



const fetchGitHubReviews = (HgnFormResponses, UserProfile) => async (org, repo, duration = 'allTime', sort = 'desc') => {
const cacheKey = `${org}_${repo}_${duration}_${sort}`;
const cachedData = cache.get(cacheKey);
if (cachedData) {
return cachedData;
}

const headers = {
Authorization: `token ${GITHUB_TOKEN}`,
Accept: 'application/vnd.github+json',
};

const now = dayjs();
const durationMap = {
lastWeek: now.subtract(7, 'day'),
last2weeks: now.subtract(14, 'day'),
lastMonth: now.subtract(30, 'day'),
allTime: dayjs('2000-01-01'), // default fallback to include everything
};
const startDate = durationMap[duration] || durationMap.allTime;

try {
let allPRs = [];
let page = 1;
const maxPRs = 200; // To avoid overloading the API and UI
let hasMore = true;

// Fetch PR's paginated to handle more than 100 results
while (hasMore && allPRs.length < maxPRs) {
const prsResponse = await axios.get(
`${BASE_URL}/repos/${org}/${repo}/pulls?state=all&per_page=100&page=${page}`,
{ headers }
);
const prData = prsResponse.data;
allPRs = allPRs.concat(prData);
hasMore = prData.length === 100;
page++;
}
allPRs = allPRs.slice(0, maxPRs);

// Use Promise.all to fetch reviews in parallel
const reviewPromises = allPRs.map(async (pr) => {
try {
const reviewsResponse = await axios.get(
`${BASE_URL}/repos/${org}/${repo}/pulls/${pr.number}/reviews`,
{ headers }
);
const reviews = reviewsResponse.data;
return reviews.map((review) => {
const reviewer = review.user?.login || 'Unknown';
const state = review.state;
const submittedAt = review.submitted_at;

if (!reviewer || !submittedAt || !state) return null;

const reviewDate = dayjs(submittedAt);
if (reviewDate.isBefore(startDate)) return null;

return { reviewer, state };
}).filter(Boolean);
} catch (err) {
console.error(`Failed to fetch reviews for PR #${pr.number}:`, err.message);
return [];
}
});

const reviewArrays = await Promise.all(reviewPromises);
const allReviewData = reviewArrays.flat();

const reviewerSummary = {};

const fetchMentorAndTeamInfo = async (githubUsername) => {
try {
const formResponse = await HgnFormResponses.findOne({
'userInfo.github': githubUsername,
}).lean();



if (!formResponse) return null;

const userId = formResponse.user_id;

const userProfile = await UserProfile.findById(userId)
.populate({
path: 'teams',
select: '_id teamName',
})
.lean();

const isMentor =
typeof userProfile.role === 'string' &&
userProfile.role.toLowerCase() === 'mentor';

return {
isMentor,
team: userProfile.teamCode || null,
};
} catch (err) {
console.error(`Failed to fetch mentor/team info for ${githubUsername}:`, err.message);
return null;
}
};



for (const { reviewer, state } of allReviewData) {
if (!reviewer) continue;

if (!reviewerSummary[reviewer]) {

// const responses = await HgnFormResponses.find({}).lean();
// console.log(JSON.stringify(responses, null, 2));
const extraInfo = await fetchMentorAndTeamInfo(reviewer);

reviewerSummary[reviewer] = {
reviewer,
isMentor: extraInfo?.isMentor ?? null,
team: extraInfo?.team ?? null,
counts: {
Exceptional: 0,
Sufficient: 0,
'Needs Changes': 0,
'Did Not Review': 0,
},
};
}

const mappedState =
state === 'APPROVED' ? 'Sufficient'
: state === 'CHANGES_REQUESTED' ? 'Needs Changes'
: state === 'COMMENTED' ? 'Exceptional'
: 'Did Not Review';

reviewerSummary[reviewer].counts[mappedState]++;
}


const result = Object.values(reviewerSummary).sort((a, b) => {
const aTotal = Object.values(a.counts).reduce((acc, val) => acc + val, 0);
const bTotal = Object.values(b.counts).reduce((acc, val) => acc + val, 0);
return sort === 'asc' ? aTotal - bTotal : bTotal - aTotal;
});

cache.set(cacheKey, result);
return result;
} catch (err) {
console.error('Error fetching data from GitHub:', err.response?.data || err.message);
return [];
}
};

module.exports = fetchGitHubReviews;
4 changes: 4 additions & 0 deletions src/startup/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ const blueSquareEmailAssignmentRouter = require('../routes/BlueSquareEmailAssign
// PR Analytics
const prInsightsRouter = require('../routes/prAnalytics/prInsightsRouter')(PRReviewInsights);


const githubAnalyticsRouter = require('../routes/githubAnalyticsRouter');
const eventRouter = require('../routes/eventRouter');
const weeklySummaryEmailAssignmentRouter = require('../routes/WeeklySummaryEmailAssignmentRoute')(
weeklySummaryEmailAssignment,
Expand Down Expand Up @@ -446,6 +448,8 @@ module.exports = function (app) {
app.use('/api', projectMaterialRouter);
app.use('/api/bm', bmRentalChart);
app.use('/api/lb', lbWishlistsRouter);

app.use('/api/analytics', githubAnalyticsRouter);
app.use('/api/lb', listingAvailablityRouter);
// lb dashboard
app.use('/api/lb', bidTermsRouter);
Expand Down
12 changes: 12 additions & 0 deletions testGithub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const axios = require('axios');

const testGitHub = async () => {
try {
const response = await axios.get('http://localhost:4500/api/analytics/github-reviews');
console.log(8response.data);
} catch (error) {
console.error(error.response?.data || error.message);
}
};

testGitHub();