From af2d5b854c226272577c90739c7bd7979687f6a7 Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 3 Apr 2024 16:52:54 +0300 Subject: [PATCH] Reorganized project controller and modified getFinalists to be an async function Removed commented code Added new winner votes controller function getFinalistProjectsWithAllVotes to return finalist projects with the votes received Removed sorting from getVotesForEvent function in the winner votes controller Modified structure of winner votes routes file to be more readable Deleted unused files Modified projects/winners page to show results sorted from highest to lowest vote count and improved performance by making only one call to the backend Modified participant/finalist-voting page to be readable and reduced the amount of calls done to the backend --- backend/modules/project/controller.js | 10 +- backend/modules/project/model.js | 7 +- backend/modules/winner-votes/controller.js | 51 ++++- backend/modules/winner-votes/routes.js | 106 ++++++--- .../inputs/ImageUpload copy/index.js | 211 ------------------ .../organiser/projects/winners/index.js | 88 +++----- .../participant/finalist-voting/index.js | 114 ++++------ 7 files changed, 203 insertions(+), 384 deletions(-) delete mode 100644 frontend/src/components/inputs/ImageUpload copy/index.js diff --git a/backend/modules/project/controller.js b/backend/modules/project/controller.js index 4ac023687..47e860a4b 100644 --- a/backend/modules/project/controller.js +++ b/backend/modules/project/controller.js @@ -213,10 +213,12 @@ controller.exportProjects = async projectIds => { return exportData } -controller.getFinalists = event => { - return Project.find({ _id: { $in: event.finalists } }) +controller.getFinalists = async event => { + const finalistProjects = await Project.find({ + _id: { $in: event.finalists }, + }) + return finalistProjects } -module.exports = controller controller.getDataForPartnerReviewing = async (event, user) => { const data = {} @@ -261,3 +263,5 @@ controller.getDataForPartnerReviewing = async (event, user) => { } return data } + +module.exports = controller diff --git a/backend/modules/project/model.js b/backend/modules/project/model.js index 53754c608..4b45d2ca8 100644 --- a/backend/modules/project/model.js +++ b/backend/modules/project/model.js @@ -6,7 +6,7 @@ const AchievementSchema = require('../../common/schemas/Achievement') const GavelController = require('../reviewing/gavel/controller') const WebhookService = require('../../common/services/webhook') const CustomAnswer = require('@hackjunction/shared/schemas/CustomAnswer') -const ProjectDefaultFields = require('@hackjunction/shared/constants/project-default-fields') +// const ProjectDefaultFields = require('@hackjunction/shared/constants/project-default-fields') // const AnswersSchema = require('@hackjunction/shared/schemas/Answers') const ProjectSchema = new mongoose.Schema({ @@ -70,11 +70,6 @@ const ProjectSchema = new mongoose.Schema({ submissionFormAnswers: { type: [CustomAnswer.mongoose], }, - // TODO default fields - // enabledFields: { - // type: [String], - // default: ProjectDefaultFields, - // }, }) ProjectSchema.set('timestamps', true) diff --git a/backend/modules/winner-votes/controller.js b/backend/modules/winner-votes/controller.js index d5cbc2283..3ab3712f3 100644 --- a/backend/modules/winner-votes/controller.js +++ b/backend/modules/winner-votes/controller.js @@ -1,8 +1,56 @@ const _ = require('lodash') const WinnerVote = require('./model') +const projectController = require('../project/controller') +const tokenVotingController = require('../voting-token/controller') const controller = {} +controller.getFinalistProjectsWithAllVotes = async event => { + const finalistProjects = await projectController.getFinalists(event) + const finalistProjectsWithVotes = finalistProjects.map(project => { + const projectObject = project.toObject() + projectObject.votingData = { + totalVotes: 0, + userVotes: 0, + tokenVotes: 0, + } + return projectObject + }) + const userVotes = await controller.getVotesForEvent(event) + const tokenVotes = await tokenVotingController.getVotesByProject(event._id) + if (userVotes) { + userVotes.map(v => { + const projectWithVotes = _.find( + finalistProjectsWithVotes, + project => project._id.toString() === v.project, + ) + if (projectWithVotes) { + projectWithVotes.votingData.userVotes = v.votes + } + }) + } + if (tokenVotes) { + tokenVotes.map(v => { + const projectWithVotes = _.find( + finalistProjectsWithVotes, + project => project._id.toString() === v.project, + ) + if (projectWithVotes) { + projectWithVotes.votingData.tokenVotes = v.votes + } + }) + } + finalistProjectsWithVotes.forEach(project => { + project.votingData.totalVotes = + project.votingData.userVotes + project.votingData.tokenVotes + }) + const sortedProjects = _.sortBy( + finalistProjectsWithVotes, + n => -1 * n.votingData.totalVotes, + ) + return sortedProjects +} + controller.getVotesForEvent = async event => { const votes = await WinnerVote.find({ event: event._id }).lean() const grouped = _.groupBy(votes, 'project') @@ -14,8 +62,7 @@ controller.getVotesForEvent = async event => { }) return result }, []) - const sorted = _.sortBy(results, n => -1 * n.votes) - return sorted + return results } module.exports = controller diff --git a/backend/modules/winner-votes/routes.js b/backend/modules/winner-votes/routes.js index 7581b8fe8..f7483e10a 100644 --- a/backend/modules/winner-votes/routes.js +++ b/backend/modules/winner-votes/routes.js @@ -12,55 +12,97 @@ const { isEventOrganiser, } = require('../../common/middleware/events') +const votingController = require('./controller') + +const getProjectsWithVotesForEvent = asyncHandler(async (req, res) => { + const finalistProjectsWithAllVotes = + await votingController.getFinalistProjectsWithAllVotes(req.event) + return res.status(200).json(finalistProjectsWithAllVotes) +}) + +const getVote = asyncHandler(async (req, res) => { + const vote = await WinnerVote.findOne({ + event: req.event._id, + user: req.user.sub, + }) + + return res.status(200).json(vote) +}) + +const submitVote = asyncHandler(async (req, res) => { + const vote = await WinnerVote.findOne({ + event: req.event._id, + user: req.user.sub, + }) + if (vote) { + vote.project = req.body.projectId + const result = await vote.save() + return res.status(200).json(result) + } + const newVote = new WinnerVote({ + event: req.event._id, + user: req.user.sub, + project: req.body.projectId, + }) + const result = await newVote.save() + return res.status(200).json(result) +}) + router .route('/:slug') .get( hasToken, hasRegisteredToEvent, - asyncHandler(async (req, res) => { - const vote = await WinnerVote.findOne({ - event: req.event._id, - user: req.user.sub, - }) + getVote, + // asyncHandler(async (req, res) => { + // const vote = await WinnerVote.findOne({ + // event: req.event._id, + // user: req.user.sub, + // }) - return res.status(200).json(vote) - }), + // return res.status(200).json(vote) + // }), ) .post( hasToken, hasRegisteredToEvent, - asyncHandler(async (req, res) => { - const vote = await WinnerVote.findOne({ - event: req.event._id, - user: req.user.sub, - }) - if (vote) { - vote.project = req.body.projectId - const result = await vote.save() - return res.status(200).json(result) - } - const newVote = new WinnerVote({ - event: req.event._id, - user: req.user.sub, - project: req.body.projectId, - }) - const result = await newVote.save() - return res.status(200).json(result) - }), + submitVote, + // asyncHandler(async (req, res) => { + // const vote = await WinnerVote.findOne({ + // event: req.event._id, + // user: req.user.sub, + // }) + // if (vote) { + // vote.project = req.body.projectId + // const result = await vote.save() + // return res.status(200).json(result) + // } + // const newVote = new WinnerVote({ + // event: req.event._id, + // user: req.user.sub, + // project: req.body.projectId, + // }) + // const result = await newVote.save() + // return res.status(200).json(result) + // }), ) router.route('/:slug/results').get( hasToken, isEventOrganiser, - asyncHandler(async (req, res) => { - const votes = await WinnerVote.find({ - event: req.event._id, - }) + getProjectsWithVotesForEvent, + // asyncHandler(async (req, res) => { + // const votes = await WinnerVote.find({ + // event: req.event._id, + // }) - const grouped = _.groupBy(votes, 'project') + // const grouped = _.groupBy(votes, 'project') + // console.log('Grouped votes:', grouped) + // getProjectsWithVotesForEvent(req, res) - return res.status(200).json(grouped) - }), + // return res.status(200).json(grouped) + // } ) +// ) module.exports = router diff --git a/frontend/src/components/inputs/ImageUpload copy/index.js b/frontend/src/components/inputs/ImageUpload copy/index.js deleted file mode 100644 index 2517ba948..000000000 --- a/frontend/src/components/inputs/ImageUpload copy/index.js +++ /dev/null @@ -1,211 +0,0 @@ -import React, { useState, useCallback } from 'react' - -import Upload from 'antd/es/upload' -import { useDispatch, useSelector } from 'react-redux' -import { makeStyles } from '@material-ui/core/styles' -import { Box, Typography, CircularProgress } from '@material-ui/core' -import DeleteIcon from '@material-ui/icons/Delete' -import VisibilityIcon from '@material-ui/icons/Visibility' - -import * as AuthSelectors from 'redux/auth/selectors' -import * as SnackbarActions from 'redux/snackbar/actions' - -const useStyles = makeStyles(theme => ({ - wrapper: { - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: '100%', - background: '#f7fafc', - }, - emptyWrapper: { - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: '100%', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - cursor: 'pointer', - }, - emptyWrapperText: { - textAlign: 'center', - color: 'black', - userSelect: 'none', - }, - image: ({ resizeMode }) => ({ - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: '100%', - objectFit: resizeMode || 'contain', - }), - imageOverlay: { - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: '100%', - background: 'rgba(0,0,0,0.6)', - opacity: 0, - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - transition: 'opacity 0.2s ease', - '&:hover': { - opacity: 1, - }, - cursor: 'pointer', - }, - imageOverlayButton: { - padding: theme.spacing(1), - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - '&:hover': { - background: 'rgba(255,255,255,0.2)', - }, - }, -})) - -export default ({ value, onChange, uploadUrl, resizeMode = 'contain' }) => { - const dispatch = useDispatch() - const idToken = useSelector(AuthSelectors.getIdToken) - const classes = useStyles({ resizeMode }) - const [loading, setLoading] = useState(false) - - if (!uploadUrl) { - throw new Error('ImageUpload component must be supplied an upload url') - } - - const beforeUpload = useCallback( - file => { - const isJpgOrPng = - file.type === 'image/jpeg' || file.type === 'image/png' - if (!isJpgOrPng) { - dispatch( - SnackbarActions.error('Please upload a .jpg or .png file'), - ) - } - const isLt2M = file.size / 1024 / 1024 < 2 - if (!isLt2M) { - dispatch( - SnackbarActions.error( - 'Upload size cannot be more than 2MB', - ), - ) - } - return isJpgOrPng && isLt2M - }, - [dispatch], - ) - - const handleChange = useCallback( - info => { - if (info.file.status === 'uploading') { - setLoading(true) - return - } - if (info.file.status === 'done') { - onChange(info.file.response) - setLoading(false) - } - - if (info.file.status === 'error') { - const message = - info?.file?.response?.message ?? - 'Something went wrong... Please try again' - dispatch(SnackbarActions.error(message)) - setLoading(false) - } - }, - [dispatch, onChange], - ) - - const handleRemove = useCallback( - e => { - e.stopPropagation() - onChange() - }, - [onChange], - ) - - const renderLoading = () => { - return ( - - - - ) - } - const renderImage = () => { - return ( - - upload - - - - - - Remove image - - - window.open(value.url, '_blank')} - > - - - - View original - - - - - ) - } - - const renderEmpty = () => { - return ( - - - Click or drag a file to upload - - - ) - } - - return ( - - - {loading && renderLoading()} - {value && renderImage()} - {!loading && !value && renderEmpty()} - - - ) -} diff --git a/frontend/src/pages/_dashboard/renderDashboard/organiser/projects/winners/index.js b/frontend/src/pages/_dashboard/renderDashboard/organiser/projects/winners/index.js index d8af9b851..c8eb2d7d2 100644 --- a/frontend/src/pages/_dashboard/renderDashboard/organiser/projects/winners/index.js +++ b/frontend/src/pages/_dashboard/renderDashboard/organiser/projects/winners/index.js @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react' -import { Grid, Box, Dialog, Typography } from '@material-ui/core' +import { Grid, Box, Dialog } from '@material-ui/core' import { useSelector } from 'react-redux' import PageHeader from 'components/generic/PageHeader' @@ -11,12 +11,8 @@ import ProjectDetail from 'components/projects/ProjectDetail' import * as OrganiserSelectors from 'redux/organiser/selectors' import * as AuthSelectors from 'redux/auth/selectors' -import EventsService from 'services/events' -//import ProjectsService from 'services/projects' - import WinnerVoteService from 'services/winnerVote' -import VotingTokenService from 'services/votingToken' -import { debugGroup } from 'utils/debuggingTools' +import _ from 'lodash' export default () => { const event = useSelector(OrganiserSelectors.event) @@ -26,65 +22,35 @@ export default () => { const [selected, setSelected] = useState(false) const [projects, setProjects] = useState([]) - const [results, setResults] = useState({}) - const [tokenVoterResults, setTokenVoterResults] = useState([]) - - const updateVote = useCallback(() => { - return WinnerVoteService.getResults(idToken, event.slug) - }, [idToken, event]) - - const updateVotesWithToken = useCallback(() => { - return VotingTokenService.getVotingTokenResults( - idToken, - event.slug, - ).catch(error => { - console.error(error?.response) - }) - }, [idToken, event]) - - // const updateProjects = useCallback(() => { - // // TODO use EventsService - // return ProjectsService.getProjectsByEvent(event.slug) - // // return EventsService.getWinnerProjects(idToken, event.slug) - // }, [event]) const update = useCallback(async () => { - setLoading(true) - if (event.overallReviewMethod === 'finalsManualSelection') { - const vote = await updateVote() - const partnerVotes = await updateVotesWithToken() - const topProjects = await EventsService.getFinalists( - idToken, - event.slug, - ) - setProjects(topProjects) - if (vote) { - setResults(vote) - } - if (partnerVotes) { - setTokenVoterResults(partnerVotes) + try { + setLoading(true) + const finalistProjectsWithAllVotes = + await WinnerVoteService.getResults(idToken, event.slug) + if ( + Array.isArray(finalistProjectsWithAllVotes) && + finalistProjectsWithAllVotes.length > 0 + ) { + setProjects(finalistProjectsWithAllVotes) } + } catch (error) { + console.error(error) + } finally { + setLoading(false) } - setLoading(false) - }, [ - event.overallReviewMethod, - event.slug, - updateVote, - updateVotesWithToken, - idToken, - ]) + }, [event, idToken]) useEffect(() => { - update() - }, [update]) - - const getScoreText = projectId => { - const scoreFromUsers = results[projectId]?.length ?? 0 - const scoreFromTokenVoters = - tokenVoterResults.find(res => res.project === projectId)?.votes ?? 0 - - const total = scoreFromUsers + scoreFromTokenVoters + if (event?.slug) { + update() + } + }, [event]) + const getScoreText = project => { + const scoreFromUsers = project?.votingData?.userVotes ?? 0 + const scoreFromTokenVoters = project?.votingData?.tokenVotes ?? 0 + const total = project?.votingData?.totalVotes ?? 0 return ( <> Total votes received: {total}
@@ -93,16 +59,16 @@ export default () => { ) } - debugGroup('Results', results) return ( - {projects.map(project => ( + {projects.map((project, index) => ( setSelected(project)} showScore={true} /> diff --git a/frontend/src/pages/_dashboard/renderDashboard/participant/finalist-voting/index.js b/frontend/src/pages/_dashboard/renderDashboard/participant/finalist-voting/index.js index 4272a9e3d..107bfde2d 100644 --- a/frontend/src/pages/_dashboard/renderDashboard/participant/finalist-voting/index.js +++ b/frontend/src/pages/_dashboard/renderDashboard/participant/finalist-voting/index.js @@ -13,10 +13,7 @@ import * as DashboardSelectors from 'redux/dashboard/selectors' import * as AuthSelectors from 'redux/auth/selectors' import * as SnackbarActions from 'redux/snackbar/actions' -import * as OrganiserSelectors from 'redux/organiser/selectors' - import EventsService from 'services/events' -import ProjectsService from 'services/projects' import WinnerVoteService from 'services/winnerVote' @@ -32,97 +29,75 @@ export default () => { const [vote, setVote] = useState(null) const [hasVoted, setVoted] = useState(false) - const updateVote = useCallback(() => { + const getCurrentVote = useCallback(() => { return WinnerVoteService.getVote(idToken, event.slug) }, [idToken, event]) - const updateProjects = useCallback(() => { - // TODO use EventsService - return ProjectsService.getProjectsByEvent(event.slug) - // return EventsService.getWinnerProjects(idToken, event.slug) - }, [event]) - - const update = useCallback(async () => { + const getFinalists = useCallback(async () => { setLoading(true) - if (event.overallReviewMethod === 'finalsManualSelection') { - const vote = await updateVote() - - const topProjects = await EventsService.getFinalists( - idToken, - event.slug, - ) - setProjects(topProjects) - if (vote) { - setVote(vote.project) - setVoted(true) - } - } else { - //TODO holy shit redo this - const topProjects = [] - const rankingsByTrack = useSelector( - OrganiserSelectors.rankingsByTrack, - ) - try { - const [vote, projects] = await Promise.all([ - updateVote(), - updateProjects(), - ]) - if (rankingsByTrack) { - Object.keys(rankingsByTrack).forEach(name => - topProjects?.push( - projects?.find( - x => x._id === rankingsByTrack[name][0], - ), - ), - ) - } - setProjects(topProjects) - if (vote) { - setVote(vote.project) - } - } catch (err) { + EventsService.getFinalists(idToken, event.slug) + .then(finalistProjects => { + setProjects(finalistProjects) + }) + .catch(err => { dispatch( SnackbarActions.error( - 'Oops, something went wrong... Please reload the page.', + 'Something went wrong... Please try again', ), ) - } - } - setLoading(false) - }, [ - event.overallReviewMethod, - event.slug, - updateVote, - idToken, - updateProjects, - dispatch, - ]) + console.error(err) + }) + .finally(() => { + setLoading(false) + }) + }, [idToken, event]) useEffect(() => { + getFinalists() update() - }, [update]) + }, []) + + const update = useCallback(async () => { + try { + setLoading(true) + const vote = await getCurrentVote() + if (vote) { + setVote(vote.project) + setVoted(true) + } + } catch (err) { + dispatch( + SnackbarActions.error( + 'Something went wrong... Please try again', + ), + ) + console.error(err) + } finally { + setLoading(false) + } + }, [event, idToken]) const handleSubmit = useCallback(async () => { - setLoading(true) try { + setLoading(true) const result = await WinnerVoteService.submitVote( idToken, event.slug, vote, ) - setVote(result.project) - setVoted(true) - dispatch(SnackbarActions.success('Vote submitted!')) + if (result) { + dispatch(SnackbarActions.success('Vote submitted!')) + } } catch (err) { dispatch( SnackbarActions.error( 'Something went wrong... Please try again', ), ) + } finally { + setLoading(false) } - setLoading(false) - }, [idToken, event.slug, vote, dispatch]) - + }, [idToken, event, vote]) return ( {
- {projects.map(project => ( + {projects.map((project, index) => ( setSelected(project)}