├── .dockerignore ├── .eslintrc.json ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── client └── modules │ └── coach │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ └── src │ ├── App.js │ ├── Header.js │ ├── Main.js │ ├── actions │ ├── actionTypes.js │ ├── ojnameActions.js │ └── userActions.js │ ├── components │ ├── Template.js │ ├── classroom │ │ ├── AddClassroom.js │ │ ├── Classroom.js │ │ ├── ClassroomContainer.js │ │ ├── DeleteClass.js │ │ ├── Leaderboard.js │ │ └── ProblemListClassroom.js │ ├── contestPortal │ │ ├── AddContest.js │ │ ├── AddStandings.js │ │ ├── ContestPortal.js │ │ ├── ContestPortalContainer.js │ │ ├── EditContest.js │ │ ├── SingleContest.js │ │ ├── SingleContestContainer.js │ │ └── StandingsPreview.js │ ├── dashboard │ │ ├── ClassroomList.js │ │ ├── ClassroomListContainer.js │ │ ├── Dashboard.js │ │ ├── ProblemList.js │ │ └── ProblemListContainer.js │ ├── problemList │ │ ├── ProblemList.js │ │ ├── SettingsList.js │ │ ├── ViewAddProblem.js │ │ ├── ViewClassroom.js │ │ └── WhoSolvedIt.js │ ├── studentPortal │ │ ├── AddStudent.js │ │ ├── RemoveStudent.js │ │ └── StudentPortal.js │ ├── userProfile │ │ ├── ChangePassword.js │ │ ├── OJSolve.js │ │ ├── Profile.js │ │ └── UserProfileContainer.js │ ├── utility │ │ └── index.js │ └── whoSolvedIt │ │ └── WhoSolvedIt.js │ ├── css │ ├── my.css │ └── progressive-button.css │ ├── index.js │ └── reducers │ ├── index.js │ ├── ojnames.js │ └── user.js ├── deploy.sh ├── doc ├── db_migrations.md └── doc.md ├── docker-compose.yml ├── gulp ├── config.js ├── copy.js ├── script.js ├── server.js ├── style.js ├── util.js └── watch.js ├── gulpfile.js ├── package.json ├── server ├── api │ └── v1 │ │ ├── classrooms.js │ │ ├── contests.js │ │ ├── ojnames.js │ │ ├── problemBank.js │ │ ├── problemList.js │ │ ├── ratings.js │ │ ├── standings.js │ │ └── users.js ├── configuration │ ├── database.js │ ├── init.js │ └── session.js ├── controllers │ ├── admin │ │ ├── dashboard.js │ │ └── root.js │ ├── gateway │ │ ├── crudController.js │ │ ├── doneStat.js │ │ ├── getChildrenController.js │ │ ├── ojscraper.js │ │ └── otherController.js │ ├── index │ │ └── indexController.js │ ├── notebook │ │ ├── noteController.js │ │ └── otherController.js │ └── user │ │ ├── loginController.js │ │ ├── profileController.js │ │ └── verificationController.js ├── index.js ├── models │ ├── classroomModel.js │ ├── contestModel.js │ ├── gateModel.js │ ├── notebookModel.js │ ├── ojnames.js │ ├── problemBankModel.js │ ├── problemListModel.js │ ├── ratingModel.js │ ├── settingModel.js │ ├── standingModel.js │ └── userModel.js └── node_modules │ ├── escapeLatex │ └── index.js │ ├── logger │ └── index.js │ ├── mailer │ └── index.js │ ├── middlewares │ ├── allowSignUp.js │ ├── flash.js │ ├── login.js │ ├── passSession.js │ ├── privateSite.js │ ├── userGroup.js │ ├── username.js │ └── verification.js │ ├── queue │ ├── index.js │ └── worker.js │ ├── settings │ └── index.js │ └── world │ └── index.js ├── src ├── css │ ├── markdown.css │ └── style.css ├── js │ ├── common │ │ └── simplemde.js │ ├── gateway │ │ └── getChildren │ │ │ └── index.js │ ├── layout │ │ ├── fill-view.js │ │ ├── flash.js │ │ ├── formLogic.js │ │ └── index.js │ └── user │ │ ├── changePassword.js │ │ ├── profile.js │ │ └── register.js └── scss │ └── main.scss ├── test └── nodemailer.test.js └── views ├── admin ├── dashboard.pug ├── invite.pug └── userList.pug ├── gateway ├── doneList.pug ├── editItem.pug ├── form │ └── inline-addItem.pug ├── getChildren.pug ├── leaderboard.pug ├── random.pug ├── readItem.pug ├── recent.pug └── search.pug ├── index ├── bugsandhugs.pug ├── faq.pug └── index.pug ├── layouts └── layout.pug ├── notebook ├── addNote.pug ├── editNote.pug ├── recent.pug └── viewNote.pug ├── partials ├── footer.pug └── header.pug ├── root └── settings.pug └── user ├── changePassword.pug ├── login.pug ├── profile.pug ├── register.pug ├── setUsername.pug └── verify.pug /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | client_module 3 | css_build 4 | public 5 | .eslintrc 6 | server/secret.js 7 | client/build/ 8 | client/modules/coach/node_modules 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["google", "plugin:react/recommended"], 3 | "parserOptions": { 4 | "ecmaVersion": 8, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true, 8 | "experimentalObjectRestSpread": true 9 | } 10 | }, 11 | "plugins": [ 12 | "react" 13 | ], 14 | "env": { 15 | "node": true, 16 | "es6": true 17 | }, 18 | "rules": { 19 | "new-cap": ["error", { 20 | "capIsNewExceptions": ["Router", "ObjectId"] 21 | }], 22 | "require-jsdoc": "off", 23 | "max-len": "off", 24 | "no-undef": "error" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | client_module 3 | css_build 4 | /public 5 | .eslintrc 6 | server/secret.js 7 | client/build/ 8 | logs/ 9 | .env 10 | backup/ 11 | package-lock.json 12 | yarn.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Version 1.5.0 4 | - Added WYSIWYG editor, with latex support. 5 | - Added support for username. 6 | - Add mongo command to deploy.sh 7 | - Improve codebase by removing myRender function. 8 | 9 | ## Version 1.4.0 (2017-10-18) 10 | - Updated deploy.sh script to automate dev server 11 | - Added google analytics 12 | 13 | ## Version 1.3.1 14 | - Added preview of notes during add/edit (Issue 24). 15 | - Disable Submit button on click in gateway/add 16 | 17 | ## Version 1.2.2 18 | - Added "createdBy" and "lastUpdateBy" properties to model. #35 19 | - Fixed folder relocation bug #37 20 | 21 | ## Version 1.1.0 22 | - Added "admin" type. They can do almost everything, except for deleting. 23 | - Added logic to turn off sign-up using environmental variable NO_SIGN_UP. 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8 2 | LABEL maintainer="forthright48@gmail.com" 3 | 4 | WORKDIR /home/src 5 | 6 | RUN apt update 7 | 8 | RUN apt install -y gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget 9 | 10 | RUN curl -sS http://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 11 | RUN echo "deb http://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 12 | RUN apt-get update && apt-get -y install yarn 13 | 14 | RUN yarn global add forever 15 | 16 | RUN npm install -g gulp-cli 17 | 18 | COPY package.json . 19 | RUN yarn install 20 | 21 | COPY client client 22 | RUN cd client/modules/coach && yarn install && yarn build && cd - && cd client/ && mkdir -p build && cd build && cp ../modules/coach/build -r coach 23 | 24 | RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.1/dumb-init_1.2.1_amd64.deb 25 | RUN dpkg -i dumb-init_*.deb 26 | 27 | ENTRYPOINT ["/usr/bin/dumb-init", "--"] 28 | 29 | ADD . . 30 | 31 | EXPOSE 8002 32 | EXPOSE 3000 33 | EXPOSE 3050 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CPPS 2 | 3 | A portal all about competitive programming and problem solving. 4 | 5 | You can visit the site [cpps.bacsbd.org](http://cpps.bacsbd.org) to view the project. 6 | 7 | # Features 8 | 9 | The site has two parts: notebook and gateway. 10 | 11 | In notebook, you will find theoretical resources about CPPS. In gateway, you will find categorized problems with hints for practice. 12 | 13 | # Getting Started for Developers 14 | 15 | 1. Fork the project. 16 | 2. Clone the project into your workstation. 17 | 3. Install docker and docker-compose. 18 | 4. Enter the following command to start docker containers: `./deploy.sh -t prod` 19 | 5. In order to run the project, you need project specific secret values. In the project root, create a file named `server/secret.js` and enter the following infos: 20 | ``` 21 | module.exports = { 22 | secret: "Your-secret-key", //Used to encrypt passwords and session 23 | dburl: "mongodb://cpps_db_1:27017/cpps", 24 | mailApi: "secret-mail-api-key", //Needs to be mailgun api 25 | recaptcha: { 26 | //Get your own recaptcha site & secret key. Use localhost as domain to run in locally. 27 | site: "Your-own-recaptcha-site-key", 28 | secret: "Your-own-recaptcha-secret-key" 29 | } 30 | } 31 | ``` 32 | 6. Next simply run the command `./deploy.sh -t dev` to start dev server. If required, please give the deploy script run permission. 33 | 7. View the site in `localhost:3000`. 34 | 35 | 36 | # Deploy Script 37 | 38 | A script called `deploy` is available for use. Using this script, we can control the site in following manners: 39 | 40 | 1. **Starting Production Server**: `./deploy.sh -t prod` for starting production server. 41 | 1. **Starting Dev Server**: `./deploy.sh -t dev`, which runs on port 3000. 42 | 1. **Changing port**: `./deploy -t dev -p 8000`. By default port 80 is used. 43 | 1. **MongoDB shell**: `./deploy -t mongo` 44 | 1. **MongoDB GUI**: `./deploy -t mongo-express` 45 | -------------------------------------------------------------------------------- /client/modules/coach/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | build/ 61 | -------------------------------------------------------------------------------- /client/modules/coach/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "bootstrap": "4.0.0-beta.3", 7 | "codeforces-rating-system": "^1.5.0", 8 | "font-awesome": "^4.7.0", 9 | "grunt": "^1.0.4", 10 | "grunt-contrib-less": "^1.4.1", 11 | "grunt-contrib-watch": "^1.1.0", 12 | "less-plugin-autoprefix": "^1.5.1", 13 | "less-plugin-clean-css": "^1.5.1", 14 | "markdown": "^0.5.0", 15 | "markdown-react-js": "^0.3.0", 16 | "qs": "^6.7.0", 17 | "react": "^16.8.6", 18 | "react-dom": "^16.8.6", 19 | "react-loading-overlay": "^0.2.8", 20 | "react-notification-system": "^0.2.17", 21 | "react-notification-system-redux": "^1.2.0", 22 | "react-progress-button": "^5.1.0", 23 | "react-redux": "^5.1.1", 24 | "react-router-bootstrap": "^0.24.4", 25 | "react-router-dom": "^4.3.1", 26 | "react-spinkit": "^3.0.0", 27 | "react-transition-group": "^1.2.1", 28 | "reactstrap": "^5.0.0", 29 | "recharts": "^1.6.2", 30 | "redux": "^3.7.2", 31 | "redux-thunk": "^2.3.0" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^4.19.1", 35 | "eslint-config-google": "^0.9.1", 36 | "eslint-plugin-react": "^7.14.2", 37 | "react-scripts": "1.0.12" 38 | }, 39 | "scripts": { 40 | "start": "NODE_PATH=src/ react-scripts start", 41 | "build": "NODE_PATH=src/ react-scripts build", 42 | "test": "react-scripts test --env=jsdom", 43 | "eject": "react-scripts eject" 44 | }, 45 | "proxy": "http://localhost:3000" 46 | } 47 | -------------------------------------------------------------------------------- /client/modules/coach/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bacsbd/cpps/ac26cb340bc7898cc000dfc336e967e90197001f/client/modules/coach/public/favicon.ico -------------------------------------------------------------------------------- /client/modules/coach/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | CPPS 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /client/modules/coach/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CPPS_Client", 3 | "name": "CPPS", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/modules/coach/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Container} from 'reactstrap'; 3 | import Header from './Header.js'; 4 | import Main from './Main.js'; 5 | 6 | export default class App extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 |
13 | 14 |
15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/modules/coach/src/Header.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {withRouter} from 'react-router-dom'; 4 | import {LinkContainer} from 'react-router-bootstrap'; 5 | import { 6 | Nav, NavItem, Dropdown, DropdownItem, DropdownToggle, DropdownMenu, NavLink, 7 | } from 'reactstrap'; 8 | import PropTypes from 'prop-types'; 9 | 10 | function mapStateToProps(state) { 11 | return { 12 | user: state.user, 13 | }; 14 | } 15 | 16 | function mapDispatchToProps(dispatch) { 17 | return {}; 18 | } 19 | 20 | class Header extends Component { 21 | constructor(props) { 22 | super(props); 23 | 24 | this.toggle = this.toggle.bind(this); 25 | this.state = { 26 | dropdownOpen: false, 27 | }; 28 | } 29 | 30 | toggle() { 31 | this.setState({ 32 | dropdownOpen: !this.state.dropdownOpen, 33 | }); 34 | } 35 | 36 | render() { 37 | const path = this.props.location.pathname; 38 | const isClassroomRegex = /^\/classroom/; 39 | const isClassroom = isClassroomRegex.exec(path)?true:false; 40 | const classId = path.split('/')[2]; 41 | const {username} = this.props.user; 42 | 43 | const tools = ( 44 | 45 | 46 | Tools 47 | 48 | 49 | 50 | Who Solved It? 51 | 52 | 53 | 54 | ); 55 | 56 | return ( 57 |
58 |

CPPS

59 | 109 |
110 | ); 111 | } 112 | } 113 | 114 | Header.propTypes = { 115 | location: PropTypes.shape({ 116 | pathname: PropTypes.string.isRequired, 117 | }), 118 | user: PropTypes.shape({ 119 | username: PropTypes.string.isRequired, 120 | }), 121 | }; 122 | 123 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)); 124 | -------------------------------------------------------------------------------- /client/modules/coach/src/Main.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Switch, Route} from 'react-router-dom'; 3 | 4 | import Dashboard from 'components/dashboard/Dashboard'; 5 | 6 | import ClassroomContainer from 'components/classroom/ClassroomContainer.js'; 7 | import AddClassroom from 'components/classroom/AddClassroom.js'; 8 | import DeleteClass from 'components/classroom/DeleteClass.js'; 9 | import {WhoSolvedIt} from 'components/whoSolvedIt/WhoSolvedIt.js'; 10 | import Leaderboard from 'components/classroom/Leaderboard'; 11 | 12 | import AddStudent from 'components/studentPortal/AddStudent.js'; 13 | import RemoveStudent from 'components/studentPortal/RemoveStudent.js'; 14 | 15 | import AddContest from 'components/contestPortal/AddContest.js'; 16 | import EditContest from 'components/contestPortal/EditContest.js'; 17 | import SingleContestContainer from 'components/contestPortal/SingleContestContainer.js'; 18 | import {AddStandings} from 'components/contestPortal/AddStandings.js'; 19 | 20 | import UserProfileContainer from 'components/userProfile/UserProfileContainer.js'; 21 | 22 | import ProblemList from 'components/problemList/ProblemList.js'; 23 | 24 | export default class Main extends Component { 25 | render() { 26 | return ( 27 |
28 | 29 | 30 | 31 | 32 | 37 | 42 | 47 | 52 | 57 | 62 | 63 | {/* Contest Portal */} 64 | 69 | 74 | 79 | 84 | 85 | {/* User Portal */} 86 | 91 | 92 | {/* Problem List Portal*/} 93 | 98 | 99 |
100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /client/modules/coach/src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const FETCH_USER = 'FETCH_USER'; 2 | export const SET_USER = 'SET_USER'; 3 | export const SET_OJNAME = 'SET_OJNAME'; 4 | -------------------------------------------------------------------------------- /client/modules/coach/src/actions/ojnameActions.js: -------------------------------------------------------------------------------- 1 | import * as types from 'actions/actionTypes'; 2 | 3 | function setOjnames(ojnames) { 4 | return { 5 | type: types.SET_OJNAME, 6 | ojnames, 7 | }; 8 | } 9 | 10 | export function fetchOJnames() { 11 | return async (dispatch) => { 12 | try { 13 | let resp = await fetch('/api/v1/ojnames', { 14 | credentials: 'same-origin', 15 | }); 16 | resp = await resp.json(); 17 | if (resp.status !== 200) throw resp; 18 | dispatch(setOjnames(resp.data)); 19 | } catch (err) { 20 | console.error(`Failed to fetch: ${err}`); 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /client/modules/coach/src/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import * as types from 'actions/actionTypes'; 2 | 3 | function setUser(user) { 4 | return { 5 | type: types.SET_USER, 6 | user, 7 | }; 8 | } 9 | 10 | export function fetchUser() { 11 | return async (dispatch) => { 12 | try { 13 | let resp = await fetch('/api/v1/users/session', { 14 | credentials: 'same-origin', 15 | }); 16 | resp = await resp.json(); 17 | if (resp.status !== 200) throw resp; 18 | resp.data.login = true; 19 | dispatch(setUser(resp.data)); 20 | } catch (err) { 21 | console.error(`Failed to fetch: ${err}`); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/Template.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import Notifications, {error, success} from 'react-notification-system-redux'; 4 | import Loadable from 'react-loading-overlay'; 5 | import {LinkContainer} from 'react-router-bootstrap'; 6 | import PropTypes from 'prop-types'; 7 | 8 | class Template extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | loadingState: true, 14 | loadingMessage: '', 15 | }; 16 | 17 | this.handleError = this.handleError.bind(this); 18 | this.notifySuccess = this.notifySuccess.bind(this); 19 | this.propagateToChild = this.propagateToChild.bind(this); 20 | this.handleInputChange = this.handleInputChange.bind(this); 21 | this.setLoadingStateComponent = this.setLoadingStateComponent.bind(this); 22 | } 23 | 24 | setLoadingStateComponent(state, message) { 25 | this.setState({ 26 | loadingState: state, 27 | loadingMessage: message, 28 | }); 29 | } 30 | 31 | propagateToChild() { 32 | return { 33 | ...this.props, 34 | changeView: this.changeView, 35 | handleError: this.handleError, 36 | setLoadingStateComponent: this.setLoadingStateComponent, 37 | }; 38 | } 39 | 40 | handleInputChange(event) { 41 | const target = event.target; 42 | const value = target.type === 'checkbox' ? target.checked : target.value; 43 | const name = target.name; 44 | 45 | this.setState({ 46 | [name]: value, 47 | }); 48 | } 49 | 50 | async handleSubmit(event) { 51 | event.preventDefault(); 52 | ... 53 | } 54 | 55 | handleError(err) { 56 | this.props.showNotification(error({ 57 | title: 'Error', 58 | message: err.message, 59 | autoDismiss: 500, 60 | })); 61 | console.error(err); 62 | } 63 | 64 | notifySuccess(msg) { 65 | this.props.showNotification(success({ 66 | title: 'Success', 67 | message: msg, 68 | autoDismiss: 10, 69 | })); 70 | } 71 | 72 | async componentWillMount() { 73 | const {problemListId} = this.props.match.params; 74 | const {user} = this.props; 75 | try { 76 | let resp = await fetch(`/api/v1/problemlists/${problemListId}`, { 77 | method: 'POST', 78 | headers: {'Content-Type': 'application/json'}, 79 | body: JSON.stringify({}), 80 | credentials: 'same-origin', 81 | }); 82 | resp = await resp.json(); 83 | if (resp.status !== 200) throw resp; 84 | } catch (err) { 85 | this.handleError(err); 86 | } finally { 87 | this.setState({ 88 | loadingState: false, 89 | }); 90 | } 91 | } 92 | 93 | render() { 94 | return ( 95 | 96 | 97 | 98 | ); 99 | } 100 | } 101 | 102 | Template.propTypes = { 103 | showNotification: PropTypes.func.isRequired, 104 | }; 105 | 106 | function mapStateToProps(state) { 107 | return { 108 | notifications: state.notifications, 109 | user: state.user, 110 | ojnames: state.ojnames, 111 | }; 112 | } 113 | 114 | function mapDispatchToProps(dispatch) { 115 | return { 116 | showNotification(msg) { 117 | dispatch(msg); 118 | }, 119 | }; 120 | } 121 | 122 | export default connect(mapStateToProps, mapDispatchToProps)(Template); 123 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/classroom/AddClassroom.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Redirect} from 'react-router-dom'; 3 | import {LinkContainer} from 'react-router-bootstrap'; 4 | import { 5 | Form, 6 | Button, 7 | Input, 8 | Label, 9 | FormGroup, 10 | } from 'reactstrap'; 11 | 12 | import {asyncUsernameToUserId} from 'components/utility'; 13 | 14 | class AddClassroom extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | name: '', 19 | students: '', 20 | fireRedirect: false, 21 | }; 22 | this.handleInputChange = this.handleInputChange.bind(this); 23 | this.handleSubmit = this.handleSubmit.bind(this); 24 | } 25 | 26 | handleInputChange(event) { 27 | const target = event.target; 28 | const value = target.type === 'checkbox' ? target.checked : target.value; 29 | const name = target.name; 30 | 31 | this.setState({ 32 | [name]: value, 33 | }); 34 | } 35 | 36 | async handleSubmit(e) { 37 | e.preventDefault(); 38 | 39 | if (!this.state.name) { 40 | return alert('Name cannot be empty'); 41 | } 42 | 43 | let students = this.state.students; 44 | 45 | students = students.split(',').map((x)=> x.trim()); 46 | students = await Promise.all(students.map(async (username) => { 47 | try { 48 | return await asyncUsernameToUserId(username); 49 | } catch (err) { 50 | console.log(`Some problem occured due to ${username}`); 51 | return ''; 52 | } 53 | })); 54 | students = students.filter((x)=>x); 55 | 56 | let data = { 57 | name: this.state.name, 58 | students, 59 | }; 60 | try { 61 | let resp = await fetch('/api/v1/classrooms', { 62 | method: 'POST', 63 | body: JSON.stringify(data), 64 | headers: new Headers({ 65 | 'Content-Type': 'application/json', 66 | }), 67 | credentials: 'same-origin', 68 | }); 69 | resp = await resp.json(); 70 | if ( resp.status !== 201 ) { 71 | throw resp; 72 | } 73 | this.setState({fireRedirect: true}); 74 | return; 75 | } catch (err) { 76 | console.log(err); 77 | if (err.status) alert(err.message); 78 | } 79 | } 80 | 81 | render() { 82 | return ( 83 |
84 |

Add Classroom

85 |
86 | 87 | 88 | 93 | 94 | 99 | 100 | 101 | 102 | 103 | 104 |
105 | 106 | {this.state.fireRedirect && ()} 107 |
108 | ); 109 | } 110 | } 111 | 112 | export default AddClassroom; 113 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/classroom/Classroom.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {LinkContainer} from 'react-router-bootstrap'; 3 | import { 4 | Row, Col, Button, UncontrolledDropdown, DropdownToggle, DropdownMenu, 5 | DropdownItem, 6 | } from 'reactstrap'; 7 | import PropTypes from 'prop-types'; 8 | import StudentPortal from 'components/studentPortal/StudentPortal'; 9 | import ContestPortalContainer from 10 | 'components/contestPortal/ContestPortalContainer'; 11 | import Notifications, {error} from 'react-notification-system-redux'; 12 | import {ProblemListClassroom} from './ProblemListClassroom'; 13 | 14 | /** Setting List */ 15 | 16 | function SettingsList({classId, name}) { 17 | return ( 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | SettingsList.propTypes = { 37 | classId: PropTypes.string.isRequired, 38 | name: PropTypes.string.isRequired, 39 | }; 40 | 41 | class Classroom extends Component { 42 | 43 | constructor(props) { 44 | super(props); 45 | 46 | this.handleError = this.handleError.bind(this); 47 | } 48 | 49 | handleError(err) { 50 | this.props.showNotification(error({ 51 | title: 'Error', 52 | message: err.message, 53 | autoDismiss: 500, 54 | })); 55 | } 56 | 57 | render() { 58 | const {classId, name, owner} = this.props; 59 | const {notifications} = this.props; 60 | return ( 61 |
62 | 65 | 66 | 67 | 68 |
69 | 70 |
71 |
72 | 73 | 74 |

{this.props.name}

75 | 76 | 77 | { 78 | owner? 79 | : 80 | 81 | } 82 | 83 |
84 | 85 | 86 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 | ); 101 | } 102 | } 103 | 104 | /** PropTypes */ 105 | Classroom.propTypes = { 106 | classId: PropTypes.string.isRequired, 107 | name: PropTypes.string.isRequired, 108 | students: PropTypes.arrayOf(PropTypes.shape({ 109 | _id: PropTypes.string.isRequired, 110 | username: PropTypes.string.isRequired, 111 | })).isRequired, 112 | owner: PropTypes.bool.isRequired, 113 | }; 114 | 115 | export default Classroom; 116 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/classroom/DeleteClass.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Redirect} from 'react-router-dom'; 3 | import {LinkContainer} from 'react-router-bootstrap'; 4 | import PropTypes from 'prop-types'; 5 | import {Form, Button, Input, Label, FormGroup} from 'reactstrap'; 6 | 7 | class DeleteClass extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | name: '', 12 | fireRedirect: false, 13 | fireSuccess: false, 14 | }; 15 | this.handleInputChange = this.handleInputChange.bind(this); 16 | this.handleSubmit = this.handleSubmit.bind(this); 17 | } 18 | 19 | componentDidMount() { 20 | if (this.props.location.state === undefined) { 21 | this.setState({fireRedirect: true}); 22 | } 23 | } 24 | 25 | handleInputChange(event) { 26 | const target = event.target; 27 | const value = target.type === 'checkbox' ? target.checked : target.value; 28 | const name = target.name; 29 | 30 | this.setState({ 31 | [name]: value, 32 | }); 33 | } 34 | 35 | async handleSubmit(e) { 36 | e.preventDefault(); 37 | 38 | if (!this.state.name) { 39 | return alert('Name cannot be empty'); 40 | } 41 | 42 | if (this.state.name !== this.props.location.state.name) { 43 | alert('Class name did not match'); 44 | return; 45 | } 46 | const api = `/api/v1/classrooms/${this.props.match.params.classId}`; 47 | try { 48 | let resp = await fetch( api, { 49 | method: 'DELETE', 50 | headers: {'Content-Type': 'application/json'}, 51 | credentials: 'same-origin', 52 | }); 53 | resp = await resp.json(); 54 | if (resp.status !== 200) throw resp; 55 | this.setState({ 56 | fireSuccess: true, 57 | }); 58 | return; 59 | } catch (err) { 60 | if (err.status) alert(err.message); 61 | console.log(err); 62 | return; 63 | } 64 | } 65 | 66 | render() { 67 | return ( 68 |
69 |

Delete Class

70 |

Are you sure?

71 |

Enter name of your classroom to confirm.

72 |
73 | 74 | 75 | 80 | 81 | 82 | 83 | 84 | 85 |
86 | 87 | {this.state.fireRedirect && 88 | ()} 89 | {this.state.fireSuccess && ()} 90 |
91 | ); 92 | } 93 | } 94 | 95 | DeleteClass.propTypes = { 96 | location: PropTypes.shape({ 97 | state: PropTypes.shape({ 98 | name: PropTypes.string.isRequired, 99 | }).isRequired, 100 | }).isRequired, 101 | match: PropTypes.shape({ 102 | params: PropTypes.shape({ 103 | classId: PropTypes.string.isRequired, 104 | }).isRequired, 105 | }).isRequired, 106 | }; 107 | 108 | export default DeleteClass; 109 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/classroom/Leaderboard.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Table} from 'reactstrap'; 3 | import {connect} from 'react-redux'; 4 | import Notifications, {error} 5 | from 'react-notification-system-redux'; 6 | import Loadable from 'react-loading-overlay'; 7 | import {LinkContainer} from 'react-router-bootstrap'; 8 | 9 | class Leaderboard extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | loadingState: true, 15 | loadingMessage: '', 16 | data: [], 17 | }; 18 | } 19 | 20 | handleError(err) { 21 | if (err.status) { 22 | this.props.showNotification(error({ 23 | title: 'Error', 24 | message: err.message, 25 | autoDismiss: 500, 26 | })); 27 | } 28 | console.error(err); 29 | } 30 | 31 | async componentWillMount() { 32 | const {classId} = this.props.match.params; 33 | try { 34 | let resp = await fetch(`/api/v1/classrooms/${classId}/leaderboard`, { 35 | method: 'GET', 36 | credentials: 'same-origin', 37 | }); 38 | resp = await resp.json(); 39 | if (resp.status !== 200) throw resp; 40 | this.setState({ 41 | loadingState: false, 42 | data: resp.data, 43 | }); 44 | } catch (err) { 45 | this.handleError(err); 46 | } finally { 47 | this.setState({ 48 | loadingState: false, 49 | }); 50 | } 51 | } 52 | 53 | dataTable() { 54 | const {data} = this.state; 55 | const {ojnames} = this.props; 56 | const ojnamesOnly = ojnames.map((x)=>x.name).filter((x)=> x !== 'vjudge'); 57 | 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | {ojnamesOnly.map((x)=> )} 66 | 67 | 68 | 69 | {data.map((u, index)=>{ 70 | return ( 71 | 72 | 73 | 80 | 81 | {ojnamesOnly.map((ojname)=>)} 82 | 83 | ); 84 | })} 85 | 86 |
#UsernameTotal {x}
{index + 1} 74 | 75 | 76 | {u.username} 77 | 78 | 79 | {u.totalSolved}{u[ojname]?u[ojname]:'-'}
87 | ); 88 | } 89 | 90 | render() { 91 | return ( 92 | 94 | 95 | 96 |

Leaderboard

97 | {this.dataTable()} 98 | 99 |
100 | ); 101 | } 102 | } 103 | 104 | function mapStateToProps(state) { 105 | return { 106 | notifications: state.notifications, 107 | user: state.user, 108 | ojnames: state.ojnames, 109 | }; 110 | } 111 | 112 | function mapDispatchToProps(dispatch) { 113 | return { 114 | showNotification(msg) { 115 | dispatch(msg); 116 | }, 117 | }; 118 | } 119 | 120 | export default connect(mapStateToProps, mapDispatchToProps)(Leaderboard); 121 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/classroom/ProblemListClassroom.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Table} from 'reactstrap'; 3 | import {WhoSolvedIt} from 'components/problemList/WhoSolvedIt'; 4 | 5 | export class ProblemListClassroom extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | problemListId: '', 11 | modalRanklist: false, 12 | }; 13 | 14 | this.toggleModalRanklist = this.toggleModalRanklist.bind(this); 15 | } 16 | 17 | toggleModalRanklist() { 18 | this.setState({ 19 | modalRanklist: !this.state.modalRanklist, 20 | }); 21 | } 22 | render() { 23 | const {problemLists} = this.props; 24 | if (problemLists.length === 0) return null; 25 | 26 | return ( 27 |
28 |

Problem Lists

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {problemLists.map((x, index)=>{ 38 | return ( 39 | 40 | 41 | 49 | 50 | ) 51 | })} 52 | 53 |
#List Name
{index + 1} 42 | this.setState({ 43 | problemListId: x._id, 44 | modalRanklist: true, 45 | })}> 46 | {`${x.createdBy.username} - ${x.title}`} 47 | 48 |
54 | 61 |
62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/contestPortal/AddContest.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {LinkContainer} from 'react-router-bootstrap'; 3 | import {Redirect} from 'react-router-dom'; 4 | import {Form, FormGroup, Label, Input, Button} from 'reactstrap'; 5 | import PropTypes from 'prop-types'; 6 | 7 | class AddContest extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | classId: this.props.match.params.classId, 12 | contestName: '', 13 | contestUrl: '', 14 | fireRedirect: false, 15 | contestId: '', // New contest created 16 | }; 17 | 18 | this.handleInputChange = this.handleInputChange.bind(this); 19 | this.handleSubmit = this.handleSubmit.bind(this); 20 | } 21 | 22 | handleInputChange(event) { 23 | const target = event.target; 24 | const value = target.type === 'checkbox' ? target.checked : target.value; 25 | const name = target.name; 26 | 27 | this.setState({ 28 | [name]: value, 29 | }); 30 | } 31 | 32 | async handleSubmit(event) { 33 | event.preventDefault(); 34 | const data = { 35 | name: this.state.contestName, 36 | link: this.state.contestUrl, 37 | classroomId: this.state.classId, 38 | }; 39 | try { 40 | const api = '/api/v1/contests'; 41 | let resp = await fetch(api, { 42 | method: 'POST', 43 | headers: {'Content-Type': 'application/json'}, 44 | body: JSON.stringify(data), 45 | credentials: 'same-origin', 46 | }); 47 | resp = await resp.json(); 48 | if (resp.status !== 201) throw resp; 49 | this.setState({ 50 | fireRedirect: true, 51 | contestId: resp.data._id, 52 | }); 53 | } catch (err) { 54 | console.log(err); 55 | } 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |

Add Contest

62 | 63 |
64 | 65 | 66 | 71 | 72 | 73 | 74 | 80 | 81 | 82 | 83 | 84 | 85 |
86 | 87 | {this.state.fireRedirect && ()} 90 |
91 | ); 92 | } 93 | } 94 | 95 | AddContest.propTypes = { 96 | match: PropTypes.shape({ 97 | params: PropTypes.shape({ 98 | classId: PropTypes.string.isRequired, 99 | }).isRequired, 100 | }).isRequired, 101 | }; 102 | 103 | export default AddContest; 104 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/contestPortal/AddStandings.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {LinkContainer} from 'react-router-bootstrap'; 3 | import {Redirect} from 'react-router-dom'; 4 | import {Form, FormGroup, Label, Input, Button} from 'reactstrap'; 5 | import PropTypes from 'prop-types'; 6 | import StandingsPreview from './StandingsPreview'; 7 | 8 | export class AddStandings extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | contestStandings: '', 13 | modal: false, 14 | rawData: '', 15 | fireRedirect: false, 16 | }; 17 | 18 | this.handleInputChange = this.handleInputChange.bind(this); 19 | this.handleSubmit = this.handleSubmit.bind(this); 20 | this.toggle = this.toggle.bind(this); 21 | this.addStandings = this.addStandings.bind(this); 22 | } 23 | 24 | async addStandings(standings) { 25 | const {classId, contestId} = this.props.match.params; 26 | const data = { 27 | classroomId: classId, 28 | contestId: contestId, 29 | standings, 30 | }; 31 | try { 32 | const api = '/api/v1/standings'; 33 | let resp = await fetch(api, { 34 | method: 'POST', 35 | headers: {'Content-Type': 'application/json'}, 36 | body: JSON.stringify(data), 37 | credentials: 'same-origin', 38 | }); 39 | resp = await resp.json(); 40 | if (resp.status !== 201) throw resp; 41 | this.setState({ 42 | fireRedirect: true, 43 | }); 44 | } catch (err) { 45 | console.log(err); 46 | } 47 | } 48 | 49 | handleInputChange(event) { 50 | const target = event.target; 51 | const value = target.type === 'checkbox' ? target.checked : target.value; 52 | const name = target.name; 53 | 54 | this.setState({ 55 | [name]: value, 56 | }); 57 | } 58 | 59 | handleSubmit(event) { 60 | event.preventDefault(); 61 | 62 | this.setState({ 63 | rawData: this.state.contestStandings, 64 | }); 65 | this.toggle(); 66 | } 67 | 68 | toggle() { 69 | this.setState({ 70 | modal: !this.state.modal, 71 | }); 72 | } 73 | 74 | render() { 75 | const {classId, contestId} = this.props.match.params; 76 | return ( 77 |
78 |

Add Standings

79 | 80 |
81 | 82 | 83 | 90 | 91 | 92 | 93 | 94 | 95 |
96 | 97 | 104 | 105 | {this.state.fireRedirect && ()} 108 |
109 | ); 110 | } 111 | } 112 | 113 | AddStandings.propTypes = { 114 | match: PropTypes.shape({ 115 | params: PropTypes.shape({ 116 | classId: PropTypes.string.isRequired, 117 | contestId: PropTypes.string.isRequired, 118 | }).isRequired, 119 | }).isRequired, 120 | }; 121 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/contestPortal/ContestPortal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import {LinkContainer} from 'react-router-bootstrap'; 4 | import { 5 | Table, Row, Col, Button, UncontrolledDropdown, DropdownToggle, DropdownMenu, 6 | DropdownItem, 7 | } from 'reactstrap'; 8 | import PropTypes from 'prop-types'; 9 | 10 | /** Setting List */ 11 | 12 | function SettingsList({classId}) { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | SettingsList.propTypes = { 30 | classId: PropTypes.string.isRequired, 31 | }; 32 | 33 | /** Contest List */ 34 | 35 | function ContestPortal(props) { 36 | const {classId, data, owner} = props; 37 | let tabulatedContestList = data.map((s, ind) => ( 38 | 39 | {ind + 1} 40 | 41 | {s.name} 42 | 43 | 44 | )); 45 | 46 | if (!owner && data.length === 0) return null; 47 | return ( 48 |
49 | 50 | 51 |

Contest Portal

52 | 53 | { 54 | owner? 55 | 56 | 57 | : 58 | 59 | } 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | { tabulatedContestList } 70 | 71 |
Index Contest
72 |
73 | ); 74 | } 75 | 76 | ContestPortal.propTypes = { 77 | classId: PropTypes.string.isRequired, 78 | data: PropTypes.arrayOf(PropTypes.shape({ 79 | _id: PropTypes.string.isRequired, 80 | name: PropTypes.string.isRequired, 81 | })).isRequired, 82 | owner: PropTypes.bool.isRequired, 83 | }; 84 | 85 | export default ContestPortal; 86 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/contestPortal/ContestPortalContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ContestPortal from './ContestPortal.js'; 4 | 5 | class ContestPortalContainer extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | classId: this.props.classId, 11 | data: [], 12 | }; 13 | } 14 | 15 | async componentWillMount() { 16 | const {classId} = this.state; 17 | try { 18 | let resp = await fetch(`/api/v1/contests?classroomId=${classId}`, { 19 | credentials: 'same-origin', 20 | }); 21 | resp = await resp.json(); 22 | 23 | if (resp.status !== 200) throw resp; 24 | this.setState({ 25 | data: resp.data, 26 | }); 27 | } catch (err) { 28 | if (err.status) alert(err.message); 29 | else console.log(err); 30 | } 31 | } 32 | 33 | render() { 34 | return ( 35 | 40 | ); 41 | } 42 | } 43 | 44 | ContestPortalContainer.propTypes = { 45 | classId: PropTypes.string.isRequired, 46 | }; 47 | 48 | 49 | export default ContestPortalContainer; 50 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/contestPortal/EditContest.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {LinkContainer} from 'react-router-bootstrap'; 3 | import {Redirect} from 'react-router-dom'; 4 | import {Form, FormGroup, Label, Input, Button} from 'reactstrap'; 5 | import PropTypes from 'prop-types'; 6 | 7 | class EditContest extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | classId: this.props.match.params.classId, 12 | contestId: this.props.match.params.contestId, 13 | contestName: '', 14 | contestUrl: '', 15 | fireRedirect: false, 16 | }; 17 | 18 | this.handleInputChange = this.handleInputChange.bind(this); 19 | this.handleSubmit = this.handleSubmit.bind(this); 20 | } 21 | 22 | handleInputChange(event) { 23 | const target = event.target; 24 | const value = target.type === 'checkbox' ? target.checked : target.value; 25 | const name = target.name; 26 | 27 | this.setState({ 28 | [name]: value, 29 | }); 30 | } 31 | 32 | async handleSubmit(event) { 33 | event.preventDefault(); 34 | const {contestId} = this.state; 35 | const data = { 36 | name: this.state.contestName, 37 | link: this.state.contestUrl, 38 | classroomId: this.state.classId, 39 | }; 40 | try { 41 | const api = `/api/v1/contests/${contestId}`; 42 | let resp = await fetch(api, { 43 | method: 'PUT', 44 | headers: {'Content-Type': 'application/json'}, 45 | body: JSON.stringify(data), 46 | credentials: 'same-origin', 47 | }); 48 | resp = await resp.json(); 49 | if (resp.status !== 200) throw resp; 50 | console.log(resp.data); 51 | this.setState({ 52 | fireRedirect: true, 53 | }); 54 | } catch (err) { 55 | console.log(err); 56 | } 57 | } 58 | 59 | render() { 60 | const {contestId} = this.state; 61 | return ( 62 |
63 |

Edit Contest: {contestId}

64 | 65 |
66 | 67 | 68 | 73 | 74 | 75 | 76 | 82 | 83 | 86 | 89 | 90 | 91 |
92 | 93 | {this.state.fireRedirect && ( 94 | 99 | )} 100 |
101 | ); 102 | } 103 | } 104 | 105 | EditContest.propTypes = { 106 | match: PropTypes.shape({ 107 | params: PropTypes.shape({ 108 | classId: PropTypes.string.isRequired, 109 | contestId: PropTypes.string.isRequired, 110 | }).isRequired, 111 | }).isRequired, 112 | }; 113 | 114 | export default EditContest; 115 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/contestPortal/SingleContestContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SingleContest from './SingleContest.js'; 4 | import {connect} from 'react-redux'; 5 | 6 | function mapStateToProps(state) { 7 | return { 8 | user: state.user, 9 | }; 10 | } 11 | 12 | class SingleContestContainer extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | data: [], // Array of standings 18 | coach: '', 19 | contest: {}, 20 | }; 21 | 22 | this.deleteStandings = this.deleteStandings.bind(this); 23 | } 24 | 25 | async deleteStandings(contestId) { 26 | try { 27 | const api = `/api/v1/contests/${contestId}`; 28 | let resp = await fetch(api, { 29 | method: 'DELETE', 30 | credentials: 'same-origin', 31 | }); 32 | resp = await resp.json(); 33 | if (resp.status !== 200) throw resp; 34 | alert('All standings have been removed'); 35 | this.setState({data: []}); 36 | } catch (err) { 37 | if (err.status) alert(err.message); 38 | console.log(err); 39 | } 40 | } 41 | 42 | async componentWillMount() { 43 | const {classId, contestId} = this.props.match.params; 44 | try { 45 | let resp = await fetch(`/api/v1/standings?contestId=${contestId}`, { 46 | credentials: 'same-origin', 47 | }); 48 | resp = await resp.json(); 49 | 50 | if (resp.status !== 200) throw resp; 51 | 52 | let resp2 = await fetch(`/api/v1/classrooms/${classId}`, { 53 | credentials: 'same-origin', 54 | }); 55 | resp2 = await resp2.json(); 56 | 57 | let resp3 = await fetch(`/api/v1/contests/${contestId}`, { 58 | credentials: 'same-origin', 59 | }); 60 | resp3 = await resp3.json(); 61 | 62 | this.setState({ 63 | data: resp.data, 64 | coach: resp2.data.coach._id, 65 | contest: resp3.data, 66 | }); 67 | } catch (err) { 68 | if (err.status) alert(err.message); 69 | else console.log(err); 70 | } 71 | } 72 | 73 | render() { 74 | const {classId, contestId} = this.props.match.params; 75 | const userId = this.props.user.userId; 76 | return ( 77 | 85 | ); 86 | } 87 | } 88 | 89 | SingleContestContainer.propTypes = { 90 | match: PropTypes.shape({ 91 | params: PropTypes.shape({ 92 | contestId: PropTypes.string.isRequired, 93 | classId: PropTypes.string.isRequired, 94 | }).isRequired, 95 | }).isRequired, 96 | user: PropTypes.shape({ 97 | userId: PropTypes.string.isRequired, 98 | }), 99 | }; 100 | 101 | export default connect(mapStateToProps)(SingleContestContainer); 102 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/contestPortal/StandingsPreview.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Table, Modal, ModalHeader, ModalBody, ModalFooter, 3 | Button} from 'reactstrap'; 4 | import PropTypes from 'prop-types'; 5 | import {asyncUsernameToUserId} from 'components/utility/index'; 6 | import {getNewRatings} from 'codeforces-rating-system'; 7 | 8 | export default class StandingsPreview extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | standings: [], 13 | rawData: [], 14 | }; 15 | this.formatStandings = this.formatStandings.bind(this); 16 | } 17 | 18 | async componentWillReceiveProps(nextProps) { 19 | const {rawData, classId} = nextProps; 20 | if (this.state.rawData === rawData) return; 21 | 22 | let standings = rawData.split('\n').filter((x)=> x); 23 | 24 | standings = await Promise.all(standings.map(async (s) => { 25 | const arr = s.split(',').map((x) => x.trim()); 26 | const position = parseInt(arr[0], 10); 27 | const username = arr[1]; 28 | let userId; 29 | let previousRating; 30 | try { 31 | userId = await asyncUsernameToUserId(username); 32 | const data = { 33 | classroomId: classId, 34 | userIds: [userId], // TODO: Reduce number of api calls 35 | }; 36 | const ratingApi = `/api/v1/ratings`; 37 | let resp = await fetch(ratingApi, { 38 | method: 'POST', 39 | headers: {'Content-Type': 'application/json'}, 40 | body: JSON.stringify(data), 41 | credentials: 'same-origin', 42 | }); 43 | resp = await resp.json(); 44 | if (resp.status === 200 || resp.status === 202) { 45 | previousRating = resp.data[0].currentRating; 46 | } else throw resp; 47 | } catch (err) { 48 | console.log(err); 49 | } 50 | if (previousRating === -1) previousRating = 1500; 51 | return {position, username, userId, previousRating}; 52 | })); 53 | 54 | standings = getNewRatings(standings); 55 | this.setState({standings, rawData}); 56 | } 57 | 58 | formatStandings() { 59 | const standings = this.state.standings; 60 | const tr = standings.map((s)=>{ 61 | return ( 62 | 63 | {s.position} 64 | {s.username} 65 | {s.userId} 66 | {s.newRating} 67 | {s.previousRating} 68 | {s.delta} 69 | 70 | ); 71 | }); 72 | return ( 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {tr} 86 | 87 |
PositionUsernameUserIdNew RatingPrevious RatingDelta
88 | ); 89 | } 90 | 91 | render() { 92 | const {modalState, toggle, addStandings} = this.props; 93 | return ( 94 | 95 | Standings Preview 96 | {this.formatStandings()} 99 | 100 | 104 | 105 | 106 | 107 | ); 108 | } 109 | } 110 | StandingsPreview.propTypes = { 111 | modalState: PropTypes.bool.isRequired, 112 | toggle: PropTypes.func.isRequired, 113 | rawData: PropTypes.string.isRequired, 114 | classId: PropTypes.string.isRequired, 115 | addStandings: PropTypes.func.isRequired, 116 | }; 117 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/dashboard/ClassroomList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import {Table} from 'reactstrap'; 4 | import PropTypes from 'prop-types'; 5 | import { 6 | Row, Col, Button, UncontrolledDropdown, DropdownToggle, DropdownMenu, 7 | DropdownItem, 8 | } from 'reactstrap'; 9 | import {LinkContainer} from 'react-router-bootstrap'; 10 | 11 | function SettingsList() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | class ClasroomList extends Component { 27 | constructor(props) { 28 | super(props); 29 | 30 | this.getRows = this.getRows.bind(this); 31 | } 32 | 33 | getRows() { 34 | const classrooms = this.props.classrooms; 35 | const rows = classrooms.map((classroom, ind) => { 36 | const {_id, name} = classroom; 37 | return ( 38 | 39 | {ind+1} 40 | {name} 41 | 42 | ); 43 | }); 44 | return rows; 45 | } 46 | 47 | render() { 48 | const classroomTable = ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | { this.getRows() } 58 | 59 |
#Classroom
60 | ); 61 | 62 | return ( 63 |
64 | 65 | 66 |

Classrooms

67 | 68 | 69 | 70 | 71 |
72 | 73 | 74 | 75 | {classroomTable} 76 | 77 | 78 |
79 | ); 80 | } 81 | } 82 | 83 | /** PropTypes */ 84 | ClasroomList.propTypes = { 85 | classrooms: PropTypes.array.isRequired, 86 | }; 87 | 88 | export default ClasroomList; 89 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/dashboard/ClassroomListContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ClassroomList from './ClassroomList'; 3 | 4 | export default class ClassroomListContainer extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | classDetails: [], 9 | }; 10 | } 11 | 12 | async componentDidMount() { 13 | const {handleError} = this.props; 14 | try { 15 | const api = '/api/v1/classrooms'; 16 | let resp = await fetch(api, { 17 | method: 'get', 18 | headers: {'Content-Type': 'application/json'}, 19 | credentials: 'same-origin', 20 | }); 21 | resp = await resp.json(); 22 | if (resp.status !== 200) { 23 | throw resp; 24 | } 25 | this.setState({ 26 | classDetails: resp.data, 27 | }); 28 | return; 29 | } catch (err) { 30 | handleError(err); 31 | return; 32 | } 33 | } 34 | 35 | render() { 36 | return ; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Col } from 'reactstrap'; 3 | import { connect } from 'react-redux'; 4 | import Notifications, { error, success } from 'react-notification-system-redux'; 5 | 6 | import ClassroomListContainer from './ClassroomListContainer'; 7 | import ProblemListContainer from './ProblemListContainer'; 8 | 9 | class Dashboard extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | loadingState: true, 15 | loadingMessage: '', 16 | }; 17 | 18 | this.handleError = this.handleError.bind(this); 19 | this.notifySuccess = this.notifySuccess.bind(this); 20 | this.propagateToChild = this.propagateToChild.bind(this); 21 | } 22 | 23 | handleError(err) { 24 | this.props.showNotification(error({ 25 | title: 'Error', 26 | message: err.message, 27 | autoDismiss: 500, 28 | })); 29 | console.error(err); 30 | } 31 | 32 | notifySuccess(msg) { 33 | this.props.showNotification(success({ 34 | title: 'Success', 35 | message: msg, 36 | autoDismiss: 10, 37 | })); 38 | } 39 | 40 | 41 | propagateToChild() { 42 | return { 43 | ...this.props, 44 | changeView: this.changeView, 45 | handleError: this.handleError, 46 | }; 47 | } 48 | 49 | render() { 50 | return ( 51 |
52 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 | ); 65 | } 66 | } 67 | 68 | function mapStateToProps(state) { 69 | return { 70 | notifications: state.notifications, 71 | user: state.user, 72 | }; 73 | } 74 | function mapDispatchToProps(dispatch) { 75 | return { 76 | showNotification(msg) { 77 | dispatch(msg); 78 | }, 79 | }; 80 | } 81 | 82 | export default connect(mapStateToProps, mapDispatchToProps)(Dashboard); 83 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/dashboard/ProblemListContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Loadable from 'react-loading-overlay'; 3 | import ProblemList from './ProblemList'; 4 | import {error as errorNotification} from 'react-notification-system-redux'; 5 | 6 | export default class ProblemListContainer extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | listDetails: [], 11 | loadingState: true, 12 | loadingMessage: 'Fetching data...', 13 | }; 14 | this.addListToState = this.addListToState.bind(this); 15 | } 16 | 17 | addListToState(x) { 18 | this.setState({ 19 | listDetails: [...this.state.listDetails, x], 20 | }); 21 | } 22 | 23 | async componentDidMount() { 24 | const {user} = this.props; 25 | try { 26 | const api = `/api/v1/problemlists?createdBy=${user.userId}`; 27 | let resp = await fetch(api, { 28 | method: 'get', 29 | credentials: 'same-origin', 30 | }); 31 | resp = await resp.json(); 32 | if (resp.status !== 200) { 33 | throw resp; 34 | } 35 | this.setState({ 36 | listDetails: resp.data, 37 | }); 38 | return; 39 | } catch (err) { 40 | if (err.status) { 41 | this.props.showNotification(errorNotification({ 42 | title: 'Error', 43 | message: err.message, 44 | autoDismiss: 500, 45 | })); 46 | } 47 | return console.error(err); 48 | } finally { 49 | this.setState({ 50 | loadingState: false, 51 | }); 52 | } 53 | } 54 | 55 | render() { 56 | return ( 57 | 60 | 65 | 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/problemList/SettingsList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem} 3 | from 'reactstrap'; 4 | import {Redirect} from 'react-router-dom'; 5 | 6 | export class SettingsList extends Component { 7 | constructor (props) { 8 | super(props); 9 | this.state = { 10 | redirectDashboard: false, 11 | }; 12 | } 13 | 14 | async deleteProblemList() { 15 | const {problemListId} = this.props.match.params; 16 | const {setLoadingState, handleError} = this.props; 17 | 18 | try { 19 | setLoadingState(true, 'Deleting the entire list...'); 20 | 21 | let resp = await fetch(`/api/v1/problemlists/${problemListId}`, { 22 | method: 'DELETE', 23 | credentials: 'same-origin', 24 | }); 25 | resp = await resp.json(); 26 | if (resp.status !== 201) throw resp; 27 | this.setState({ 28 | redirectDashboard: true, 29 | }); 30 | } catch (err) { 31 | handleError(err); 32 | } finally { 33 | if (!this.state.redirectDashboard) { 34 | setLoadingState(false); 35 | } 36 | } 37 | } 38 | 39 | render() { 40 | const {changeView} = this.props; 41 | return ( 42 | 43 | {this.state.redirectDashboard && ()} 44 | 45 | 46 | 47 |
changeView('addProblem')} 50 | > Add Problem
51 |
52 | 53 |
this.deleteProblemList()} 56 | > Delete List
57 |
58 |
59 |
60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/problemList/ViewAddProblem.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Form, FormGroup, Input, Label, Button} from 'reactstrap'; 3 | import {success} from 'react-notification-system-redux'; 4 | 5 | export class ViewAddProblem extends Component { 6 | 7 | constructor (props) { 8 | super(props); 9 | 10 | this.state = { 11 | ojname: '', 12 | problemId: '', 13 | } 14 | 15 | this.handleInputChange = this.handleInputChange.bind(this); 16 | this.handleSubmit = this.handleSubmit.bind(this); 17 | } 18 | 19 | handleInputChange(event) { 20 | const target = event.target; 21 | const value = target.type === 'checkbox' ? target.checked : target.value; 22 | const name = target.name; 23 | 24 | this.setState({ 25 | [name]: value, 26 | }); 27 | } 28 | 29 | async handleSubmit(e) { 30 | e.preventDefault(); 31 | 32 | const {problemListId} = this.props.match.params; 33 | const {setLoadingState, addProblem, handleError} = this.props; 34 | 35 | setLoadingState(true, 'Adding Problem to List...'); 36 | 37 | const {ojname, problemId} = this.state; 38 | if (!ojname || !problemId) return; 39 | 40 | try { 41 | let resp = await fetch(`/api/v1/problembanks/${ojname}/${problemId}`, { 42 | credentials: 'same-origin', 43 | }); 44 | resp = await resp.json(); 45 | if (resp.status !== 200) throw resp; 46 | 47 | const data = resp.data; 48 | resp = await fetch(`/api/v1/problemlists/${problemListId}/problems`, { 49 | method: 'PUT', 50 | headers: {'Content-Type': 'application/json'}, 51 | body: JSON.stringify(data), 52 | credentials: 'same-origin', 53 | }); 54 | resp = await resp.json(); 55 | if (resp.status !== 201) throw resp; 56 | 57 | this.setState({ 58 | problemId: '', 59 | }); 60 | addProblem(resp.data); 61 | 62 | this.props.showNotification(success({ 63 | title: 'Success', 64 | message: `Problem added: ${ojname}:${problemId}`, 65 | })); 66 | } catch (err) { 67 | handleError(err); 68 | } finally { 69 | setLoadingState(false); 70 | } 71 | } 72 | 73 | render() { 74 | const {ojnames} = this.props; 75 | const ojnamesOnly = ojnames.map((x)=>x.name); 76 | const ojnamesMap = {}; 77 | ojnames.forEach((x)=>ojnamesMap[x.name] = x); 78 | 79 | const {changeView} = this.props; 80 | return ( 81 |
82 |

Add Problem View

83 |
84 | 85 | 86 | 89 | 90 | {ojnamesOnly.map((x)=>)} 91 | 92 | 93 | 94 | 96 | 99 | 100 | { ' ' } 101 | 102 |
103 |
104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/studentPortal/AddStudent.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Redirect} from 'react-router-dom'; 3 | import {LinkContainer} from 'react-router-bootstrap'; 4 | import PropTypes from 'prop-types'; 5 | import {Form, Button, Input, Label, FormGroup} from 'reactstrap'; 6 | 7 | import {asyncUsernameToUserId} from 'components/utility'; 8 | 9 | class AddStudent extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | student: '', 14 | fireRedirect: false, 15 | }; 16 | this.handleInputChange = this.handleInputChange.bind(this); 17 | this.handleSubmit = this.handleSubmit.bind(this); 18 | } 19 | 20 | handleInputChange(event) { 21 | const target = event.target; 22 | const value = target.type === 'checkbox' ? target.checked : target.value; 23 | const name = target.name; 24 | 25 | this.setState({ 26 | [name]: value, 27 | }); 28 | } 29 | 30 | async handleSubmit(e) { 31 | e.preventDefault(); 32 | 33 | if (!this.state.student) { 34 | return alert('Student username cannot be empty'); 35 | } 36 | 37 | let students = this.state.student.split(',').map((x) => x.trim()).filter((x)=>x); 38 | try { 39 | students = await Promise.all(students.map(async (student)=>{ 40 | try { 41 | return await asyncUsernameToUserId(student); 42 | } catch (err) { 43 | console.error(err); 44 | return ''; 45 | } 46 | }) 47 | ); 48 | students = students.filter((s) => s); 49 | 50 | const data = { 51 | students, 52 | }; 53 | const {classId} = this.props.match.params; 54 | const api = `/api/v1/classrooms/${classId}/students`; 55 | let resp = await fetch(api, { 56 | method: 'POST', 57 | headers: {'Content-Type': 'application/json'}, 58 | body: JSON.stringify(data), 59 | credentials: 'same-origin', 60 | }); 61 | resp = await resp.json(); 62 | if ( resp.status !== 201 ) { 63 | throw resp; 64 | } 65 | this.setState({fireRedirect: true}); 66 | } catch (err) { 67 | console.log(err); 68 | if (err.status) alert(err.message); 69 | return; 70 | } 71 | } 72 | 73 | render() { 74 | return ( 75 |
76 |

Add Student

77 |
78 | 79 | 80 | 85 | 86 | 87 | 88 | 89 | 90 |
91 | 92 | {this.state.fireRedirect && 93 | ()} 94 |
95 | ); 96 | } 97 | } 98 | 99 | AddStudent.propTypes = { 100 | match: PropTypes.shape({ 101 | params: PropTypes.shape({ 102 | classId: PropTypes.string.isRequired, 103 | }).isRequired, 104 | }).isRequired, 105 | }; 106 | 107 | export default AddStudent; 108 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/studentPortal/RemoveStudent.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Redirect} from 'react-router-dom'; 3 | import {LinkContainer} from 'react-router-bootstrap'; 4 | import PropTypes from 'prop-types'; 5 | import {Form, Button, Input, Label, FormGroup} from 'reactstrap'; 6 | 7 | import {asyncUsernameToUserId} from 'components/utility'; 8 | 9 | class RemoveStudent extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | student: '', 14 | fireRedirect: false, 15 | }; 16 | this.handleInputChange = this.handleInputChange.bind(this); 17 | this.handleSubmit = this.handleSubmit.bind(this); 18 | } 19 | 20 | handleInputChange(event) { 21 | const target = event.target; 22 | const value = target.type === 'checkbox' ? target.checked : target.value; 23 | const name = target.name; 24 | 25 | this.setState({ 26 | [name]: value, 27 | }); 28 | } 29 | 30 | async handleSubmit(e) { 31 | e.preventDefault(); 32 | 33 | if (!this.state.student) { 34 | return alert('Student username cannot be empty'); 35 | } 36 | 37 | let student = this.state.student; 38 | 39 | try { 40 | student = await asyncUsernameToUserId(student); 41 | const {classId} = this.props.match.params; 42 | const api = `/api/v1/classrooms/${classId}/students/${student}`; 43 | let resp = await fetch(api, { 44 | method: 'DELETE', 45 | headers: {'Content-Type': 'application/json'}, 46 | credentials: 'same-origin', 47 | }); 48 | resp = await resp.json(); 49 | if ( resp.status !== 200 ) { 50 | throw resp; 51 | } 52 | this.setState({fireRedirect: true}); 53 | } catch (err) { 54 | console.log(err); 55 | if (err.status) alert(err.message); 56 | return; 57 | } 58 | } 59 | 60 | render() { 61 | return ( 62 |
63 |

Remove Student

64 |
65 | 66 | 67 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
79 | 80 | {this.state.fireRedirect && 81 | ()} 82 |
83 | ); 84 | } 85 | } 86 | 87 | RemoveStudent.propTypes = { 88 | match: PropTypes.shape({ 89 | params: PropTypes.shape({ 90 | classId: PropTypes.string.isRequired, 91 | }).isRequired, 92 | }).isRequired, 93 | }; 94 | 95 | export default RemoveStudent; 96 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/userProfile/ChangePassword.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Loadable from 'react-loading-overlay'; 3 | import PropTypes from 'prop-types'; 4 | import {Form, Input, FormGroup, Row, Col} from 'reactstrap'; 5 | 6 | export class ChangePassword extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | loadingState: false, 12 | loadingMessage: '', 13 | currentPassword: '', 14 | newPassword: '', 15 | repeatPassword: '', 16 | }; 17 | 18 | this.handleInputChange = this.handleInputChange.bind(this); 19 | this.handleSubmit = this.handleSubmit.bind(this); 20 | } 21 | 22 | handleInputChange(event) { 23 | const target = event.target; 24 | const value = target.type === 'checkbox' ? target.checked : target.value; 25 | const name = target.name; 26 | 27 | this.setState({ 28 | [name]: value, 29 | }); 30 | } 31 | 32 | async handleSubmit(e) { 33 | e.preventDefault(); 34 | 35 | this.setState({ 36 | loadingState: true, 37 | }); 38 | 39 | const {handleError, user, notifySuccess} = this.props; 40 | const {currentPassword, newPassword, repeatPassword} = this.state; 41 | const {username} = user; 42 | try { 43 | if (newPassword !== repeatPassword) { 44 | throw new Error('Repeat Password did not match with New Password'); 45 | } 46 | 47 | let resp = await fetch(`/api/v1/users/${username}/change-password`, { 48 | method: 'PUT', 49 | headers: {'Content-Type': 'application/json'}, 50 | body: JSON.stringify({currentPassword, newPassword, repeatPassword}), 51 | credentials: 'same-origin', 52 | }); 53 | resp = await resp.json(); 54 | 55 | if (resp.status !== 201) throw resp; 56 | notifySuccess('Password Changed Successfully'); 57 | } catch (err) { 58 | handleError(err); 59 | } finally { 60 | this.setState({ 61 | loadingState: false, 62 | currentPassword: '', 63 | newPassword: '', 64 | repeatPassword: '', 65 | }); 66 | } 67 | } 68 | 69 | render() { 70 | const {changeView} = this.props; 71 | return ( 72 | 74 |
75 |

Change Password

76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | changeView('normal')}/> 92 | 93 | 94 |
95 |
96 |
97 | ); 98 | } 99 | } 100 | 101 | ChangePassword.propTypes = { 102 | showNotification: PropTypes.func.isRequired, 103 | changeView: PropTypes.func.isRequired, 104 | }; 105 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/userProfile/UserProfileContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from 'react-redux'; 4 | import {Profile} from './Profile.js'; 5 | import qs from 'qs'; 6 | import Notifications, {error, success} from 'react-notification-system-redux'; 7 | 8 | class UserProfileContainer extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | userId: '', 14 | displayUser: {}, 15 | classrooms: [], 16 | ojnames: [], 17 | }; 18 | 19 | this.updateOjStats = this.updateOjStats.bind(this); 20 | this.handleError = this.handleError.bind(this); 21 | this.notifySuccess = this.notifySuccess.bind(this); 22 | this.propagateToChild = this.propagateToChild.bind(this); 23 | } 24 | 25 | propagateToChild() { 26 | return { 27 | updateOjStats: this.updateOjStats, 28 | handleError: this.handleError, 29 | notifySuccess: this.notifySuccess, 30 | }; 31 | } 32 | 33 | handleError(err) { 34 | this.props.showNotification(error({ 35 | title: 'Error', 36 | message: err.message, 37 | autoDismiss: 500, 38 | })); 39 | console.error(err); 40 | } 41 | 42 | notifySuccess(msg) { 43 | this.props.showNotification(success({ 44 | title: 'Success', 45 | message: msg, 46 | autoDismiss: 10, 47 | })); 48 | } 49 | 50 | updateOjStats(newOjStats) { 51 | const displayUser = this.state.displayUser; 52 | this.setState({ 53 | displayUser: { 54 | ...displayUser, 55 | ojStats: newOjStats, 56 | }, 57 | }); 58 | } 59 | 60 | async loadProfile(currentProps) { 61 | const {username} = currentProps.match.params; 62 | 63 | try { 64 | let resp = await fetch(`/api/v1/users/${username}`, { 65 | credentials: 'same-origin', 66 | }); 67 | resp = await resp.json(); 68 | 69 | if (resp.status !== 200) throw resp; 70 | const displayUser = resp.data; 71 | 72 | resp = await fetch(`/api/v1/users/${username}/root-stats`, { 73 | credentials: 'same-origin', 74 | }); 75 | resp = await resp.json(); 76 | const userRootStats = resp.data; 77 | displayUser.userRootStats = userRootStats; 78 | 79 | resp = await fetch(`/api/v1/users/username-userId/${username}`, { 80 | credentials: 'same-origin', 81 | }); 82 | resp = await resp.json(); 83 | const userId = resp.data; 84 | 85 | const query = { 86 | student: userId, 87 | select: '_id name coach', 88 | populate: ['coach', 'username'], 89 | }; 90 | 91 | resp = await fetch(`/api/v1/classrooms?${qs.stringify(query)}`, { 92 | credentials: 'same-origin', 93 | }); 94 | resp = await resp.json(); 95 | const classrooms = resp.data; 96 | 97 | this.setState({ 98 | displayUser, 99 | userId, 100 | classrooms, 101 | }); 102 | } catch (err) { 103 | this.handleError(err); 104 | } 105 | } 106 | 107 | async componentWillMount() { 108 | await this.loadProfile(this.props); 109 | } 110 | 111 | async componentWillReceiveProps(nextProps) { 112 | if (nextProps.match.params.username === this.props.match.params.username) return; 113 | this.loadProfile(nextProps); 114 | } 115 | 116 | render() { 117 | return ( 118 |
119 | 120 | 125 |
126 | ); 127 | } 128 | } 129 | 130 | UserProfileContainer.propTypes = { 131 | match: PropTypes.shape(), 132 | }; 133 | 134 | function mapStateToProps(state) { 135 | return { 136 | notifications: state.notifications, 137 | user: state.user, 138 | ojnames: state.ojnames, 139 | }; 140 | } 141 | function mapDispatchToProps(dispatch) { 142 | return { 143 | showNotification(msg) { 144 | dispatch(msg); 145 | }, 146 | }; 147 | } 148 | 149 | export default connect(mapStateToProps, mapDispatchToProps)(UserProfileContainer); 150 | -------------------------------------------------------------------------------- /client/modules/coach/src/components/utility/index.js: -------------------------------------------------------------------------------- 1 | async function asyncUsernameToUserId(username) { 2 | const api = `/api/v1/users/username-userId/${username}`; 3 | try { 4 | let resp = await fetch(api, { 5 | credentials: 'same-origin', 6 | }); 7 | resp = await resp.json(); 8 | if (resp.status !== 200) throw resp; 9 | return resp.data; 10 | } catch (err) { 11 | console.log(err); 12 | throw err; 13 | } 14 | } 15 | 16 | export {asyncUsernameToUserId}; 17 | -------------------------------------------------------------------------------- /client/modules/coach/src/css/my.css: -------------------------------------------------------------------------------- 1 | .pointer { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /client/modules/coach/src/css/progressive-button.css: -------------------------------------------------------------------------------- 1 | .pb-container { 2 | display: inline; 3 | text-align: center; 4 | width: 100%; 5 | } 6 | .pb-container .pb-button { 7 | background: transparent; 8 | border: 2px solid currentColor; 9 | border-radius: 27px; 10 | color: currentColor; 11 | cursor: pointer; 12 | padding: 0.7em 1em; 13 | text-decoration: none; 14 | text-align: center; 15 | height: 54px; 16 | width: 50%; 17 | -webkit-tap-highlight-color: transparent; 18 | outline: none; 19 | transition: background-color 0.3s, width 0.3s, border-width 0.3s, border-color 0.3s, border-radius 0.3s; 20 | } 21 | .pb-container .pb-button span { 22 | display: inherit; 23 | transition: opacity 0.3s 0.1s; 24 | font-size: 1em; 25 | font-weight: 100; 26 | } 27 | .pb-container .pb-button svg { 28 | height: 54px; 29 | width: 54px; 30 | position: absolute; 31 | transform: translate(-50%, -50%); 32 | pointer-events: none; 33 | } 34 | .pb-container .pb-button svg path { 35 | opacity: 0; 36 | fill: none; 37 | } 38 | .pb-container .pb-button svg.pb-progress-circle { 39 | animation: spin 0.9s infinite cubic-bezier(0.085, 0.260, 0.935, 0.710); 40 | } 41 | .pb-container .pb-button svg.pb-progress-circle path { 42 | stroke: currentColor; 43 | stroke-width: 5; 44 | } 45 | .pb-container .pb-button svg.pb-checkmark path, 46 | .pb-container .pb-button svg.pb-cross path { 47 | stroke: #fff; 48 | stroke-linecap: round; 49 | stroke-width: 4; 50 | } 51 | .pb-container.disabled .pb-button { 52 | cursor: not-allowed; 53 | } 54 | .pb-container.loading .pb-button { 55 | width: 54px; 56 | border-width: 6.5px; 57 | border-color: #ddd; 58 | cursor: wait; 59 | background-color: transparent; 60 | padding: 0; 61 | } 62 | .pb-container.loading .pb-button span { 63 | transition: all 0.15s; 64 | opacity: 0; 65 | display: none; 66 | } 67 | .pb-container.loading .pb-button .pb-progress-circle > path { 68 | transition: opacity 0.15s 0.3s; 69 | opacity: 1; 70 | } 71 | .pb-container.success .pb-button { 72 | border-color: #A0D468; 73 | background-color: #A0D468; 74 | } 75 | .pb-container.success .pb-button span { 76 | transition: all 0.15s; 77 | opacity: 0; 78 | display: none; 79 | } 80 | .pb-container.success .pb-button .pb-checkmark > path { 81 | opacity: 1; 82 | } 83 | .pb-container.error .pb-button { 84 | border-color: #ED5565; 85 | background-color: #ED5565; 86 | } 87 | .pb-container.error .pb-button span { 88 | transition: all 0.15s; 89 | opacity: 0; 90 | display: none; 91 | } 92 | .pb-container.error .pb-button .pb-cross > path { 93 | opacity: 1; 94 | } 95 | @keyframes spin { 96 | from { 97 | transform: translate(-50%, -50%) rotate(0deg); 98 | transform-origin: center center; 99 | } 100 | to { 101 | transform: translate(-50%, -50%) rotate(360deg); 102 | transform-origin: center center; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /client/modules/coach/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {Provider} from 'react-redux'; 4 | import {createStore, applyMiddleware} from 'redux'; 5 | import thunk from 'redux-thunk'; 6 | import rootReducer from './reducers'; 7 | import {BrowserRouter} from 'react-router-dom'; 8 | import './css/progressive-button.css'; 9 | import 'bootstrap/dist/css/bootstrap.css'; 10 | import 'font-awesome/css/font-awesome.min.css'; 11 | import './css/my.css'; 12 | 13 | import {fetchUser} from 'actions/userActions'; 14 | import {fetchOJnames} from 'actions/ojnameActions'; 15 | 16 | import App from './App'; 17 | 18 | const store = createStore( 19 | rootReducer, 20 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), 21 | applyMiddleware(thunk), 22 | ); 23 | 24 | async function main() { 25 | await Promise.all([ 26 | store.dispatch(fetchUser()), 27 | store.dispatch(fetchOJnames()), 28 | ]); 29 | 30 | ReactDOM.render(( 31 | 32 | 33 | 34 | 35 | 36 | ), 37 | document.getElementById('root') 38 | ); 39 | } 40 | 41 | main(); 42 | -------------------------------------------------------------------------------- /client/modules/coach/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import user from './user'; 3 | import ojnames from './ojnames.js'; 4 | import {reducer as notifications} from 'react-notification-system-redux'; 5 | 6 | export default combineReducers({ 7 | notifications, 8 | user, 9 | ojnames, 10 | }); 11 | -------------------------------------------------------------------------------- /client/modules/coach/src/reducers/ojnames.js: -------------------------------------------------------------------------------- 1 | import * as types from 'actions/actionTypes'; 2 | 3 | 4 | const defaultOjNames = []; 5 | 6 | export default function ojnames(state=defaultOjNames, action) { 7 | switch (action.type) { 8 | case types.SET_OJNAME: 9 | return action.ojnames; 10 | default: 11 | return state; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/modules/coach/src/reducers/user.js: -------------------------------------------------------------------------------- 1 | import * as types from 'actions/actionTypes'; 2 | 3 | 4 | const defaultUser = { 5 | username: '', 6 | userId: '', 7 | status: '', 8 | login: false, 9 | email: '', 10 | }; 11 | 12 | export default function user(state=defaultUser, action) { 13 | switch (action.type) { 14 | case types.SET_USER: 15 | return action.user; 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ueo pipefail 4 | 5 | TYPE="" 6 | main() { 7 | parseargs "$@" 8 | 9 | # Check if secret.js file exists 10 | if [[ ! -f server/secret.js ]]; then 11 | echo -e "${BOLD}File missing${OFF}: server/secret.js (Please read README.md)" 12 | exit 1 13 | fi 14 | 15 | case $TYPE in 16 | backup) deploy_backup ;; 17 | dev) deploy_dev ;; 18 | init) deploy_init ;; 19 | kuejs) deploy_kuejs ;; 20 | mongo) deploy_mongo ;; 21 | mongo_express) deploy_mongo_express ;; 22 | prod) deploy_prod ;; 23 | redis) deploy_redis ;; 24 | restore) deploy_restore ;; 25 | stop) deploy_stop ;; 26 | worker) deploy_worker ;; 27 | *) 28 | echo -e "${BOLD}Error${OFF}: Type flag is required." 29 | help_details 30 | ;; 31 | esac 32 | } 33 | 34 | parseargs() { 35 | while getopts hp:t: FLAG; do 36 | case $FLAG in 37 | h) help_details ;; 38 | t) TYPE=$OPTARG ;; 39 | *) echo "Unexpected option" && HELP_DETAILS && exit 1 ;; 40 | esac 41 | done 42 | } 43 | 44 | BOLD='\e[1;31m' # Bold Red 45 | REV='\e[1;32m' # Bold Green 46 | OFF='\e[0m' 47 | 48 | #Help function 49 | help_details() { 50 | echo -e "${REV}TYPE${OFF}: Can be one of dev, prod, beta, init, mongo or mongo-express" 51 | exit 1 52 | } 53 | 54 | echo_info() { 55 | echo -e "${REV}${1}${OFF}" 56 | } 57 | 58 | deploy_prod() { 59 | docker-compose down 60 | docker rmi $(docker images -f "dangling=true" -q) || ret=$? 61 | git pull origin 62 | git checkout master 63 | docker-compose build 64 | docker-compose up & 65 | sleep 5s 66 | echo_info "Sleep complete" 67 | docker cp server/secret.js cpps_app_1:/home/src/server/ 68 | docker exec -itd cpps_app_1 /bin/bash -c "gulp production && forever start server/index.js" 69 | docker exec -itd cpps_app_1 forever start server/node_modules/queue/worker.js 70 | echo_info "Done" 71 | } 72 | 73 | deploy_dev() { 74 | docker-compose down 75 | docker rmi $(docker images -f "dangling=true" -q) || ret=$? 76 | docker-compose build 77 | docker-compose up & 78 | sleep 5s 79 | docker cp server/secret.js cpps_app_1:/home/src/server/ 80 | docker exec -itd cpps_app_1 /bin/bash -c "cd /root/src && node server/node_modules/queue/worker.js" 81 | docker exec -it cpps_app_1 /bin/bash -c "cd /root/src && yarn install && gulp" 82 | } 83 | 84 | deploy_mongo() { 85 | docker exec -it cpps_db_1 mongo 86 | } 87 | 88 | deploy_mongo_express() { 89 | docker run -it --rm \ 90 | --name mongo-express \ 91 | --network cpps_ntw \ 92 | --link cpps_db_1:mongo \ 93 | -p 8081:8081 \ 94 | -e ME_CONFIG_OPTIONS_EDITORTHEME="ambiance" \ 95 | mongo-express 96 | } 97 | 98 | deploy_init() { 99 | docker exec -it cpps_app_1 node server/configuration/init.js 100 | } 101 | 102 | deploy_backup() { 103 | # Create backup and copy it out from docker 104 | # Run this from root folder 105 | docker exec -it cpps_db_1 mongodump --db cpps --out /root/volumes/cpps_db/$(date +"%m-%d-%y") 106 | docker cp cpps_db_1:/root/volumes/cpps_db ./backup/ 107 | docker exec -it cpps_db_1 rm -r /root/volumes/cpps_db/$(date +"%m-%d-%y") 108 | } 109 | 110 | deploy_restore() { 111 | # Copy the backup file to docker into path /root/volumes/cpps_db/restore/cpps 112 | # docker cp cpps_db_1:/root/volumes/cpps_db/restore 113 | # Then run this command 114 | docker exec cpps_db_1 rm -rf /root/volumes/cpps_db/restore 115 | docker cp ./backup/restore cpps_db_1:/root/volumes/cpps_db/restore 116 | docker exec -it cpps_db_1 mongorestore --db cpps --drop /root/volumes/cpps_db/restore/cpps/ 117 | } 118 | 119 | deploy_kuejs() { 120 | docker exec -it cpps_app_1 node_modules/kue/bin/kue-dashboard -p 3050 -r redis://cpps_redis_1:6379 121 | } 122 | 123 | deploy_redis() { 124 | docker exec -it cpps_redis_1 redis-cli flushall 125 | } 126 | 127 | deploy_worker() { 128 | docker exec -it cpps_app_1 /bin/bash -c "cd /root/src && node server/node_modules/queue/worker.js" 129 | } 130 | 131 | deploy_stop() { 132 | docker-compose down 133 | docker rmi $(docker images -f "dangling=true" -q) 134 | } 135 | 136 | # Call main function 137 | main "$@" 138 | -------------------------------------------------------------------------------- /doc/db_migrations.md: -------------------------------------------------------------------------------- 1 | ``` 2 | # Clean folders 3 | db.gates.update({ 4 | type: 'folder' 5 | }, { 6 | $unset: { 7 | platform: undefined, 8 | pid: undefined, 9 | } 10 | }, { 11 | multi: true, 12 | }); 13 | ``` 14 | -------------------------------------------------------------------------------- /doc/doc.md: -------------------------------------------------------------------------------- 1 | Documents 2 | ========= 3 | 4 | # User Session 5 | 6 | Key |Value 7 | -|- 8 | login |true if they are logged in 9 | verified |true if their mail is verified 10 | verificationValue| present only if `verified` is false 11 | email |email address of user 12 | status |{root,admin,user} 13 | userId |user id from db 14 | 15 | 16 | # Context Variables 17 | 18 | |Variables|Value| 19 | |----------|---| 20 | |login| Boolean| 21 | |email| string| 22 | |status| {root,admin,user}| 23 | |superUser| true if not user| 24 | 25 | # Docker 26 | docker run -itd --name cpps_app -v $PWD:/home/src -p 8000:8002 --expose 3000 -p 3000:3000 cpps /bin/bash 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | redis: 5 | image: redis 6 | networks: 7 | - ntw 8 | volumes: 9 | - redis_db:/data 10 | db: 11 | image: mongo:3.4 12 | networks: 13 | - ntw 14 | volumes: 15 | - db:/root/volumes/cpps_db 16 | - db_data:/data/db 17 | restart: always 18 | app: 19 | build: . 20 | image: cpps 21 | depends_on: 22 | - db 23 | - redis 24 | ports: 25 | - "${PORT}:8002" 26 | - "3000:3000" 27 | - "3050:3050" 28 | networks: 29 | - ntw 30 | volumes: 31 | - app:/home/volumes/cpps_app 32 | - logs:/home/src/logs 33 | - ./:/root/src/ 34 | command: tail -f /dev/null 35 | 36 | networks: 37 | ntw: 38 | 39 | volumes: 40 | db_data: 41 | db: 42 | app: 43 | logs: 44 | redis_db: 45 | -------------------------------------------------------------------------------- /gulp/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dirs: { 3 | public: './public', 4 | temp: './temp', 5 | output: './public', 6 | css_build: './css_build', 7 | }, 8 | pug: './views/**/*.pug', 9 | css: { 10 | src: './src/**/*.css', 11 | all: ['./src/**/*.css'], 12 | }, 13 | scss: './src/**/*.scss', 14 | js: './src/**/*.js', 15 | pdf: './src/**/*.pdf', 16 | vendorOutput: { 17 | js: './public/js/vendor', 18 | css: './public/css/vendor', 19 | }, 20 | browsersync: ['./views/**/*.pug'], 21 | sassInclude: ['./public'], 22 | browserifyPath: ['./node_modules', './src', './server/node_modules'], 23 | }; 24 | -------------------------------------------------------------------------------- /gulp/copy.js: -------------------------------------------------------------------------------- 1 | const config = require('./config.js'); 2 | const changed = require('gulp-changed'); 3 | 4 | module.exports = function(gulp) { 5 | function copyRest(folder) { 6 | // Copy everything except css, scss, js and image 7 | // Unless it is in vendor folder 8 | return gulp.src( 9 | [`./${folder}/**`, 10 | `!./${folder}/**/*.{css,scss,JPG,jpg,png,gif,js}`, 11 | ]) 12 | .pipe(changed(config.dirs.public)) 13 | .pipe(gulp.dest(config.dirs.public)); 14 | } 15 | 16 | gulp.task('copy:src', function() { 17 | return copyRest('src'); 18 | }); 19 | 20 | gulp.task('copy:css_build', function() { 21 | return gulp.src('./css_build/**') 22 | .pipe(changed(config.dirs.public)) 23 | .pipe(gulp.dest(config.dirs.public)); 24 | }); 25 | 26 | gulp.task( 27 | 'copy', 28 | gulp.parallel('copy:src', 'copy:css_build' 29 | )); 30 | }; 31 | -------------------------------------------------------------------------------- /gulp/script.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const uglify = require('gulp-uglify'); 3 | const rootPath = path.join(__dirname, '../'); 4 | const rename = require('gulp-rename'); 5 | const browserify = require('browserify'); 6 | const glob = require('glob'); 7 | const source = require('vinyl-source-stream'); 8 | const buffer = require('vinyl-buffer'); 9 | const config = require('./config.js'); 10 | const sourcemaps = require('gulp-sourcemaps'); 11 | 12 | module.exports = function(gulp) { 13 | function browserified(filePath) { 14 | const fileName = path.basename(filePath); 15 | return browserify({ 16 | entries: [filePath], 17 | paths: config.browserifyPath, 18 | }) 19 | .transform('babelify', { 20 | presets: ['es2015'], 21 | }) 22 | .bundle() 23 | .on('error', function(err) { 24 | // print the error (can replace with gulp-util) 25 | console.log(err.message); 26 | // end this stream 27 | this.emit('end'); 28 | }) 29 | .pipe(source(fileName)); 30 | } 31 | 32 | gulp.task('script', function(done) { 33 | const filePathArray = glob.sync('./src/js/**/*.js'); 34 | filePathArray.forEach(function(filePath) { 35 | let destPath = path.relative(path.join(rootPath, 'src'), filePath); 36 | destPath = path.join('./public', destPath); 37 | const destDir = path.dirname(destPath); 38 | 39 | // Get Modified Time 40 | /* const mtimeSource = fs.statSync(path.join(rootPath, filePath)).mtime; 41 | let mtimeDest; 42 | try { 43 | mtimeDest = fs.statSync(path.join(rootPath, destPath)).mtime; 44 | } catch (e) { 45 | mtimeDest = mtimeSource; 46 | } 47 | 48 | if (mtimeSource < mtimeDest) return;*/ 49 | 50 | browserified(filePath) 51 | .pipe(gulp.dest(destDir)) 52 | .pipe(rename({ 53 | suffix: '.min', 54 | })) 55 | .pipe(buffer()) 56 | .pipe(sourcemaps.init()) 57 | .pipe(uglify()) 58 | .on('error', function(err) { 59 | // print the error (can replace with gulp-util) 60 | console.log(err.message); 61 | // end this stream 62 | this.emit('end'); 63 | }) 64 | .pipe(sourcemaps.write()) 65 | .pipe(gulp.dest(destDir)); 66 | }); 67 | done(); 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /gulp/server.js: -------------------------------------------------------------------------------- 1 | const browsersync = require('browser-sync'); 2 | const nodemon = require('gulp-nodemon'); 3 | const config = require('./config.js'); 4 | 5 | module.exports = function(gulp) { 6 | gulp.task('nodemon', function(cb) { 7 | let callbackCalled = false; 8 | return nodemon({ 9 | script: './server/index.js', 10 | ignore: ['./src', './public', './views'], 11 | }).on('start', function() { 12 | if (!callbackCalled) { 13 | callbackCalled = true; 14 | cb(); 15 | } 16 | }); 17 | }); 18 | 19 | gulp.task('browser-sync', gulp.series('nodemon', function() { 20 | return browsersync.init({ 21 | proxy: 'http://localhost:8002', 22 | files: config.browsersync, 23 | }); 24 | })); 25 | }; 26 | -------------------------------------------------------------------------------- /gulp/style.js: -------------------------------------------------------------------------------- 1 | const config = require('./config'); 2 | const sass = require('gulp-sass'); 3 | const absUrl = require('gulp-css-url-to-absolute'); 4 | const changed = require('gulp-changed'); 5 | 6 | const sassConfig = { 7 | outputStyle: 'compressed', 8 | sourceMapEmbed: true, 9 | includePaths: config.sassInclude, 10 | }; 11 | 12 | module.exports = function(gulp) { 13 | // Changes all relative urls inside css file to absolute urls 14 | 15 | gulp.task('cssAbsPath:src', function() { 16 | return gulp.src(config.css.src, { 17 | nodir: true, 18 | }) 19 | .pipe(changed(config.dirs.css_build)) 20 | .pipe(absUrl({ 21 | root: './src', 22 | })) 23 | .pipe(gulp.dest(config.dirs.css_build)); 24 | }); 25 | 26 | gulp.task('build:css', 27 | gulp.parallel('cssAbsPath:src' 28 | )); 29 | 30 | gulp.task('style:scss', gulp.series(function() { 31 | return gulp.src(config.scss) 32 | .pipe(sass.sync(sassConfig).on('error', sass.logError)) 33 | .pipe(gulp.dest(config.dirs.public)); 34 | })); 35 | }; 36 | -------------------------------------------------------------------------------- /gulp/util.js: -------------------------------------------------------------------------------- 1 | const del = require('del'); 2 | const config = require('./config.js'); 3 | 4 | module.exports = function(gulp) { 5 | gulp.task('clean', function(cb) { 6 | return del(config.dirs.public, cb); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /gulp/watch.js: -------------------------------------------------------------------------------- 1 | const config = require('./config.js'); 2 | 3 | module.exports = function(gulp) { 4 | gulp.task('watch:scss', function() { 5 | return gulp.watch(config.scss, gulp.series('style:scss')); 6 | }); 7 | gulp.task('watch:css', function() { 8 | return gulp.watch( 9 | config.css.all, gulp.series('build:css', 'copy:css_build', 'style:scss' 10 | )); 11 | }); 12 | gulp.task('watch:js', function() { 13 | return gulp.watch(config.js, gulp.series('script')); 14 | }); 15 | 16 | gulp.task( 17 | 'watch', gulp.parallel('watch:css', 'watch:scss', 'watch:js', 18 | )); 19 | }; 20 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | 3 | require('./gulp/server.js')(gulp); 4 | require('./gulp/util.js')(gulp); 5 | require('./gulp/style.js')(gulp); 6 | require('./gulp/copy.js')(gulp); 7 | require('./gulp/script.js')(gulp); 8 | require('./gulp/watch.js')(gulp); 9 | 10 | 11 | gulp.task('default', 12 | gulp.series( 13 | 'clean', 14 | 'build:css', 15 | 'copy', 16 | gulp.parallel('style:scss', 'script'), 17 | gulp.parallel('watch', 'browser-sync') 18 | ) 19 | ); 20 | 21 | gulp.task('production', 22 | gulp.series( 23 | 'clean', 24 | 'build:css', 25 | 'copy', 26 | gulp.parallel('style:scss', 'script') 27 | ) 28 | ); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cpps", 3 | "version": "1.9.0", 4 | "description": "A portal all about competitive programming and problem solving", 5 | "main": "server/index.js", 6 | "scripts": { 7 | "prestart": "node_modules/gulp/bin/gulp.js production", 8 | "start": "node server/index.js", 9 | "test": "./node_modules/mocha/bin/mocha --recursive --reporter spec ./tests" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/forthright48/cpps.git" 14 | }, 15 | "author": { 16 | "name": "Mohammad Samiul Islam", 17 | "email": "forthright48@gmail.com", 18 | "url": "http://www.forthright48.com" 19 | }, 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/forthright48/cpps/issues" 23 | }, 24 | "homepage": "https://github.com/forthright48/cpps#readme", 25 | "engines": { 26 | "node": "8" 27 | }, 28 | "private": true, 29 | "dependencies": { 30 | "async": "^2.6.3", 31 | "babel-preset-es2015": "^6.24.1", 32 | "babelify": "^7.3.0", 33 | "bcryptjs": "^2.4.3", 34 | "body-parser": "^1.19.0", 35 | "browser-sync": "^2.26.7", 36 | "browserify": "^14.5.0", 37 | "connect-flash": "^0.1.1", 38 | "connect-mongo": "^1.3.2", 39 | "cookie-parser": "^1.4.4", 40 | "del": "^2.2.2", 41 | "express": "^4.17.1", 42 | "express-recaptcha": "^2.3.0", 43 | "express-session": "^1.16.2", 44 | "font-awesome": "^4.7.0", 45 | "forever": "^0.15.3", 46 | "glob": "^7.1.4", 47 | "gulp": "^4.0.2", 48 | "gulp-changed": "^3.2.0", 49 | "gulp-clean-css": "^3.10.0", 50 | "gulp-concat": "^2.6.1", 51 | "gulp-css-url-to-absolute": "^1.0.1", 52 | "gulp-debug": "^3.2.0", 53 | "gulp-nodemon": "^2.4.2", 54 | "gulp-recursive-concat": "^0.2.0", 55 | "gulp-rename": "^1.4.0", 56 | "gulp-sass": "^3.2.1", 57 | "gulp-sourcemaps": "^2.6.5", 58 | "gulp-uglify": "^2.1.2", 59 | "jquery": "^3.4.1", 60 | "jquery-validation": "^1.19.1", 61 | "kue": "^0.11.6", 62 | "kue-unique": "^1.0.13", 63 | "lodash": "^4.17.15", 64 | "marked": "^0.3.19", 65 | "moment": "^2.24.0", 66 | "mongoose": "^4.13.19", 67 | "mongoose-paginate": "^5.0.3", 68 | "morgan": "^1.9.1", 69 | "nodemailer": "^4.7.0", 70 | "nodemailer-mailgun-transport": "^1.4.0", 71 | "notifyjs-browser": "git+https://github.com/notifyjs/notifyjs.git", 72 | "ojscraper": "^1.12.0", 73 | "pug": "^2.0.4", 74 | "request": "^2.88.0", 75 | "tether": "^1.4.7", 76 | "validator": "^7.2.0", 77 | "vinyl-buffer": "^1.0.1", 78 | "vinyl-source-stream": "^2.0.0", 79 | "winston": "^3.2.1" 80 | }, 81 | "devDependencies": { 82 | "eslint": "^4.15.0", 83 | "eslint-config-google": "^0.9.1", 84 | "eslint-plugin-react": "^7.5.1", 85 | "react-scripts": "1.0.12" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /server/api/v1/contests.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const {isRoot} = require('middlewares/userGroup'); 3 | const Contest = require('mongoose').model('Contest'); 4 | const Standing = require('mongoose').model('Standing'); 5 | const Classroom = require('mongoose').model('Classroom'); 6 | 7 | const router = express.Router(); 8 | 9 | router.get('/contests', getContests); 10 | router.get('/contests/:contestId', getContest); 11 | router.put('/contests/:contestId', editContest); 12 | router.post('/contests', isRoot, insertContest); 13 | router.delete('/contests/:contestId', isRoot, deleteStandings); 14 | 15 | module.exports = { 16 | addRouter(app) { 17 | app.use('/api/v1', router); 18 | }, 19 | }; 20 | 21 | async function getContests(req, res, next) { 22 | try { 23 | const {classroomId} = req.query; 24 | if (!classroomId) { 25 | const e = new Error('classroomId query is missing'); 26 | e.status = 400; 27 | throw e; 28 | } 29 | const contests = await Contest.find({classroomId}).exec(); 30 | return res.status(200).json({ 31 | status: 200, 32 | data: contests, 33 | }); 34 | } catch (err) { 35 | next(err); 36 | } 37 | } 38 | 39 | async function getContest(req, res, next) { 40 | try { 41 | const {contestId} = req.params; 42 | const contest = await Contest.findById(contestId).exec(); 43 | return res.status(200).json({ 44 | status: 200, 45 | data: contest, 46 | }); 47 | } catch (err) { 48 | next(err); 49 | } 50 | } 51 | 52 | async function insertContest(req, res, next) { 53 | const {name, link, classroomId} = req.body; 54 | const {userId} = req.session; 55 | 56 | try { 57 | const classroom = await Classroom.findOne({_id: classroomId}) 58 | .select('coach') 59 | .exec(); 60 | 61 | if (!classroom) { 62 | const e = new Error(`No such classroom with id ${classroom}`); 63 | e.status = 400; 64 | throw e; 65 | } 66 | 67 | if (classroom.coach.toString() !== userId.toString()) { 68 | const e = new Error(`You are not the owner of this classroom`); 69 | e.status = 400; 70 | throw e; 71 | } 72 | } catch (err) { 73 | err.message = err.message + ' Error during contest creation'; 74 | err.status = 500; 75 | err.type = 'contest-error'; 76 | return next(err); 77 | } 78 | 79 | const contest = new Contest({name, link, classroomId, coach: userId}); 80 | try { 81 | await contest.save(); 82 | return res.status(201).json({ 83 | status: 201, 84 | data: contest, 85 | }); 86 | } catch (err) { 87 | err.message = err.message + ' Error during contest creation'; 88 | err.status = 500; 89 | err.type = 'contest-error'; 90 | return next(err); 91 | } 92 | } 93 | 94 | async function editContest(req, res, next) { 95 | const {name, link} = req.body; 96 | const {userId} = req.session; 97 | const {contestId} = req.params; 98 | 99 | try { 100 | const contest = await Contest.findOneAndUpdate( 101 | {_id: contestId, coach: userId}, 102 | { 103 | $set: { 104 | name, 105 | link, 106 | }, 107 | }, 108 | { 109 | new: true, 110 | } 111 | ); 112 | return res.status(201).json({ 113 | status: 200, 114 | data: contest, 115 | }); 116 | } catch (err) { 117 | err.message = err.message + ' Error during contest update'; 118 | err.status = 500; 119 | err.type = 'contest-error'; 120 | return next(err); 121 | } 122 | } 123 | 124 | // Only allow deleting standings 125 | async function deleteStandings(req, res, next) { 126 | const {contestId} = req.params; 127 | const {userId} = req.session; 128 | 129 | // Now remove all related standings 130 | try { 131 | await Standing.remove({ 132 | contestId, 133 | coach: userId, 134 | }).exec(); 135 | return res.status(200).json({ 136 | status: 200, 137 | }); 138 | } catch (err) { 139 | err.message = err.message + ' Error in standings deletion'; 140 | err.status = 500; 141 | err.type = 'standings-error'; 142 | return next(err); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /server/api/v1/ojnames.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const {rootPath} = require('world'); 4 | const ojnames = require(path.join(rootPath, 'models/ojnames.js')); 5 | 6 | const router = express.Router(); 7 | 8 | router.get('/ojnames', getOjnames); 9 | 10 | module.exports = { 11 | addRouter(app) { 12 | app.use('/api/v1', router); 13 | }, 14 | }; 15 | 16 | function getOjnames(req, res, next) { 17 | return res.status(200).json({ 18 | status: 200, 19 | data: ojnames.data, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /server/api/v1/problemBank.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const ProblemBank = require('mongoose').model('ProblemBank'); 3 | const path = require('path'); 4 | const {rootPath} = require('world'); 5 | const {ojnamesOnly} = require(path.join(rootPath, 'models/ojnames')); 6 | const ojscraper = require('ojscraper'); 7 | 8 | const router = express.Router(); 9 | 10 | router.get('/problembanks/:platform/:problemId', getProblem); 11 | 12 | module.exports = { 13 | addRouter(app) { 14 | app.use('/api/v1', router); 15 | }, 16 | }; 17 | 18 | async function getProblem(req, res, next) { 19 | const {platform, problemId} = req.params; 20 | 21 | if (ojnamesOnly.findIndex((x)=>x===platform) === -1) { 22 | return res.status(400).json({ 23 | status: 400, 24 | message: `No such platform: ${platform} in problem bank`, 25 | }); 26 | } 27 | 28 | try { 29 | const problem = await ProblemBank.findOne({ 30 | platform, 31 | problemId, 32 | }).exec(); 33 | 34 | // Problem found in bank 35 | if (problem) { 36 | return res.status(200).json({ 37 | status: 200, 38 | data: problem, 39 | }); 40 | } 41 | 42 | const credential = require('world').secretModule.ojscraper.loj.credential; 43 | const info = await ojscraper.getProblemInfo({ 44 | ojname: platform, problemID: problemId, credential, 45 | }); 46 | 47 | const newProblem = new ProblemBank({ 48 | title: info.title, 49 | problemId: info.problemID, 50 | platform: info.platform, 51 | link: info.link, 52 | }); 53 | await newProblem.save(); 54 | 55 | return res.status(200).json({ 56 | status: 200, 57 | data: newProblem, 58 | }); 59 | } catch (err) { 60 | return next(err); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/api/v1/ratings.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const {isRoot} = require('middlewares/userGroup'); 3 | const Rating = require('mongoose').model('Rating'); 4 | const Standing = require('mongoose').model('Standing'); 5 | 6 | const router = express.Router(); 7 | 8 | router.post('/ratings', getRatings); 9 | router.put('/ratings/apply/contest/:contestId', isRoot, applyRating); 10 | 11 | module.exports = { 12 | addRouter(app) { 13 | app.use('/api/v1', router); 14 | }, 15 | }; 16 | 17 | async function getRatings(req, res, next) { 18 | try { 19 | const {classroomId, userIds} = req.body; 20 | if (!classroomId || !userIds || !userIds[0]) { 21 | const e = new Error( 22 | `classroomId: ${classroomId} or userIds: ${userIds} query is missing`); 23 | e.status = 400; 24 | throw e; 25 | } 26 | 27 | const rating = await Rating.find({classroomId, userId: userIds}).exec(); 28 | 29 | const missingIds = userIds.filter((id)=>{ 30 | const found = rating.some((x)=> x.userId.toString() === id); 31 | return !found; 32 | }); 33 | 34 | missingIds.forEach((id)=>{ 35 | rating.push({ 36 | userId: id, 37 | classroomId, 38 | currentRating: -1, 39 | }); 40 | }); 41 | return res.status(200).json({ 42 | status: 200, 43 | data: rating, 44 | }); 45 | } catch (err) { 46 | next(err); 47 | } 48 | } 49 | 50 | async function applyRating(req, res, next) { 51 | try{ 52 | const {contestId} = req.params; 53 | 54 | // Get rating changes due to contestId 55 | const standing = await Standing.find({contestId}).exec(); 56 | 57 | await Promise.all(standing.map(async(s)=>{ 58 | await Rating.findOneAndUpdate({ 59 | userId: s.userId, 60 | classroomId: s.classroomId, 61 | }, { 62 | $set: { 63 | userId: s.userId, 64 | classroomId: s.classroomId, 65 | currentRating: s.newRating, 66 | }, 67 | }, { 68 | upsert: true, 69 | }); 70 | })); 71 | 72 | return res.status(200).json({ 73 | status: 200, 74 | }); 75 | } catch(err) { 76 | next(err); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /server/api/v1/standings.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const {isRoot} = require('middlewares/userGroup'); 3 | const Standing = require('mongoose').model('Standing'); 4 | const Contest = require('mongoose').model('Contest'); 5 | 6 | const router = express.Router(); 7 | 8 | router.get('/standings', getStandings); 9 | router.post('/standings', isRoot, insertStandings); 10 | 11 | 12 | module.exports = { 13 | addRouter(app) { 14 | app.use('/api/v1', router); 15 | }, 16 | }; 17 | 18 | async function getStandings(req, res, next) { 19 | try { 20 | const {contestId} = req.query; 21 | if (!contestId) { 22 | const e = new Error( 23 | `contestId: ${contestId} query is missing`); 24 | e.status = 400; 25 | throw e; 26 | } 27 | const standings = await Standing.find({contestId}).exec(); 28 | 29 | return res.status(200).json({ 30 | status: 200, 31 | data: standings, 32 | }); 33 | } catch (err) { 34 | next(err); 35 | } 36 | } 37 | 38 | async function insertStandings(req, res, next) { 39 | const {classroomId, standings, contestId} = req.body; 40 | const {userId} = req.session; 41 | try { 42 | const contest = await Contest.findOne({_id: contestId}).exec(); 43 | if (!contest) { 44 | const e = new Error( 45 | `contestId: ${contestId} No such contest `); 46 | e.status = 400; 47 | throw e; 48 | } 49 | if (contest.classroomId.toString() !== classroomId ) { 50 | const e = new Error( 51 | `classroomId: ${classroomId} No such classroom `); 52 | e.status = 400; 53 | throw e; 54 | } 55 | 56 | if (contest.coach.toString() !== userId.toString()) { 57 | const e = new Error('You are not the owner'); 58 | e.status = 400; 59 | throw e; 60 | } 61 | 62 | const data = await Promise.all(standings.map(async (s)=>{ 63 | const standing = new Standing({ 64 | username: s.username, 65 | userId: s.userId, 66 | position: s.position, 67 | contestId, 68 | classroomId, 69 | coach: userId, 70 | previousRating: s.previousRating, 71 | newRating: s.newRating, 72 | }); 73 | await standing.save(); 74 | return standing; 75 | })); 76 | 77 | return res.status(201).json({ 78 | status: 201, 79 | data, 80 | }); 81 | } catch (err) { 82 | err.message = err.message + ' Error in standings creation'; 83 | err.status = 500; 84 | err.type = 'standings-error'; 85 | return next(err); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /server/configuration/database.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const dburl = require('world').secretModule.dburl; 3 | 4 | mongoose.Promise = global.Promise; 5 | mongoose.connect(dburl, function(err) { 6 | if (err) console.log(err); 7 | else console.log('Successfully connected to database'); 8 | }); 9 | -------------------------------------------------------------------------------- /server/configuration/init.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Intiation Script 3 | * 4 | * 1. Creates a root folder if it does not exist 5 | * 2. Creates a notebook note if it does not exist 6 | * 3. Create admin 7 | */ 8 | 9 | const mongoose = require('mongoose'); 10 | const dburl = require('world').secretModule.dburl; 11 | const readline = require('readline'); 12 | const _ = require('lodash'); 13 | const rl = readline.createInterface({ 14 | input: process.stdin, 15 | output: process.stdout 16 | }); 17 | 18 | mongoose.Promise = global.Promise; 19 | const promise = mongoose.connect(dburl, { 20 | useMongoClient: true 21 | }); 22 | 23 | function warning (){ 24 | _.times(5, function(){ 25 | console.log("***Warning*** Creating root account"); 26 | }) 27 | } 28 | 29 | function handleError(err){ 30 | console.log(err); 31 | process.exit(); 32 | } 33 | 34 | promise.then(function(db) { 35 | console.log('Successfully connected to database'); 36 | 37 | require('../models/userModel'); 38 | const User = mongoose.model('User'); 39 | 40 | require('../models/gateModel') 41 | const Gate = mongoose.model('Gate'); 42 | 43 | require('../models/notebookModel'); 44 | const Notebook = mongoose.model('Notebook'); 45 | 46 | /** 47 | * Create root folder if it does not exist 48 | */ 49 | 50 | Gate.findOne({_id: '000000000000000000000000'}).exec() 51 | .then(function(root){ 52 | if ( !root ) { 53 | // If root does not exist, create it 54 | const newRoot = new Gate({ 55 | _id: '000000000000000000000000', 56 | type: 'folder', 57 | ancestor: [], 58 | ind: 0, 59 | title: 'Root' 60 | }) 61 | return newRoot.save(); 62 | } 63 | return 64 | }) 65 | // Insert notebook if not present 66 | .then(function(){ 67 | return Notebook.findOne({slug: 'notebook'}).exec(); 68 | }) 69 | .then(function(doc){ 70 | if ( doc ) return doc; 71 | const newNote = new Notebook({ 72 | title: 'Notebook', 73 | slug: 'notebook', 74 | body: '', 75 | }) 76 | return newNote.save(); 77 | }) 78 | // Create new user 79 | .then( function() { 80 | warning(); 81 | return new Promise(function(resolve, reject) { 82 | rl.question('Enter email for admin: ', function(email){ 83 | return resolve(email); 84 | }); 85 | }); 86 | }) 87 | .then(function(email) { 88 | return new Promise(function(resolve, reject) { 89 | rl.question('Enter password for admin: ', function(password) { 90 | return resolve({email, password}); 91 | }) 92 | }); 93 | }) 94 | .then(function({email, password}){ 95 | const pass = User.createHash(password); 96 | const user = new User({ 97 | email, 98 | password: pass, 99 | status: 'root', 100 | verified: 'true' 101 | }); 102 | return user.save(); 103 | }) 104 | .then(function(){ 105 | process.exit(); // All done 106 | }).catch(handleError) 107 | }).catch(handleError); 108 | -------------------------------------------------------------------------------- /server/configuration/session.js: -------------------------------------------------------------------------------- 1 | const session = require('express-session'); 2 | const MongoStore = require('connect-mongo')(session); 3 | const secret = require('world').secretModule.secret; 4 | const mongoose = require('mongoose'); 5 | const cookieParser = require('cookie-parser'); 6 | 7 | module.exports = { 8 | addSession(app) { 9 | app.use(cookieParser(secret)); 10 | 11 | app.use(session({ 12 | secret, 13 | resave: false, 14 | saveUninitialized: false, 15 | store: new MongoStore({ 16 | mongooseConnection: mongoose.connection, 17 | ttl: 2 * 60 * 60, 18 | touchAfter: 2 * 3600 19 | }) 20 | })); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /server/controllers/admin/dashboard.js: -------------------------------------------------------------------------------- 1 | /** 2 | URL Path: /admin 3 | Dashboard for admins 4 | */ 5 | 6 | const express = require('express'); 7 | const User = require('mongoose').model('User'); 8 | const userGroup = require('middlewares/userGroup'); 9 | const mailer = require('mailer').mailer; 10 | const {domain, subdomain, siteName} = require('world').secretModule; 11 | const _ = require('lodash'); 12 | 13 | const router = express.Router(); 14 | 15 | router.get('/dashboard', get_dashboard); 16 | router.get('/invite-user', get_invite); 17 | router.post('/invite-user', post_invite); 18 | 19 | router.get('/user/list', get_user_list); 20 | router.get('/user/change-status/:userMail/:status', userGroup.isRoot, get_user_changeStatus); 21 | 22 | module.exports = { 23 | addRouter(app) { 24 | app.use('/admin', userGroup.isAdmin, router); 25 | } 26 | }; 27 | 28 | /** 29 | *Implementation 30 | */ 31 | function get_dashboard(req, res) { 32 | return res.render('admin/dashboard'); 33 | } 34 | 35 | function get_invite(req, res) { 36 | return res.render('admin/invite'); 37 | } 38 | 39 | function post_invite(req, res) { 40 | const email = User.normalizeEmail(req.body.email); 41 | const password = User.createHash(req.body.password); 42 | 43 | const user = new User({ 44 | email, 45 | password, 46 | verificationValue: _.random(100000, 999999), 47 | }); 48 | 49 | user.save(function(err) { 50 | if (err) { 51 | if (err.code === 11000) { 52 | req.flash('error', 'Email address already exists'); 53 | } else { 54 | req.flash('error', `An error occured. Error code: ${err.code}`); 55 | } 56 | return res.redirect('/admin/invite-user'); 57 | } 58 | req.flash('success', 'Successfully registered'); 59 | 60 | //Send activation email 61 | const emailMail = { 62 | to: [email], 63 | from: `CPPS ${siteName} `, 64 | subject: `You are invited to join CPPS-${siteName} Gateway`, 65 | text: `Welcome to CPPS Gateway (${subdomain}.${domain}). Your password is ${req.body.password}. Please make sure that you change it. Here is your verification code: ${user.verificationValue}`, 66 | html: `Welcome to CPPS Gateway (${subdomain}.${domain}). 67 | Your password is ${req.body.password}. Please make sure that you change it. Here is your verification code: ${user.verificationValue}` 68 | }; 69 | 70 | mailer.sendMail(emailMail, function(err) { 71 | if (err) { 72 | req.flash('error', 'There was some error while sending verification code. Try again.'); 73 | } else { 74 | req.flash('success', 'Verification Code sent to User'); 75 | } 76 | return res.redirect('/admin/invite-user'); 77 | }); 78 | }); 79 | } 80 | 81 | function get_user_list(req, res) { 82 | User.paginate({}, { 83 | select: 'createdAt email username status verified', 84 | sort: '-createdAt', 85 | limit: 100 86 | }, function(err, users) { 87 | return res.render('admin/userList', { 88 | users: users.docs 89 | }); 90 | }); 91 | } 92 | 93 | function get_user_changeStatus(req, res, next){ 94 | const { userMail, status } = req.params; 95 | 96 | User.update({email: userMail}, { 97 | $set: { 98 | status 99 | } 100 | }).exec( function(err) { 101 | if ( err ) return next (err); 102 | return res.redirect('/admin/user/list'); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /server/controllers/admin/root.js: -------------------------------------------------------------------------------- 1 | /** 2 | URL Path: /root 3 | Dashboard for root 4 | */ 5 | 6 | const express = require('express'); 7 | const settings = require('settings'); 8 | const userGroup = require('middlewares/userGroup'); 9 | 10 | const router = express.Router(); 11 | 12 | router.get('/settings', get_settings); 13 | router.post('/settings/:key', post_settings); 14 | 15 | module.exports = { 16 | addRouter(app) { 17 | app.use('/root', userGroup.isRoot, router); 18 | } 19 | }; 20 | 21 | /** 22 | * Implementation 23 | */ 24 | 25 | function get_settings( req, res, next ) { 26 | return res.render('root/settings', { 27 | settings: settings.getAll() 28 | }); 29 | } 30 | 31 | function post_settings( req, res, next ) { 32 | const key = req.params.key; 33 | const value = req.body.value; 34 | 35 | settings.setKey(key,value) 36 | .then(function(){ 37 | req.flash('success', 'Settings saved'); 38 | return res.redirect('/root/settings'); 39 | }) 40 | .catch(next); 41 | } 42 | -------------------------------------------------------------------------------- /server/controllers/gateway/doneStat.js: -------------------------------------------------------------------------------- 1 | /** 2 | Path: /gateway/doneStat 3 | Contains logic to handle adding/removing problems from done list of user 4 | */ 5 | const express = require('express'); 6 | const Gate = require('mongoose').model('Gate'); 7 | 8 | const router = express.Router(); 9 | 10 | router.get('/add-done-list/:ID', addDoneList); 11 | router.get('/remove-done-list/:ID', removeDoneList); 12 | 13 | module.exports = { 14 | addRouter(app) { 15 | app.use('/gateway/doneStat', router); 16 | } 17 | }; 18 | 19 | /******************************************* 20 | Implementation 21 | *******************************************/ 22 | 23 | function addDoneList(req, res, next) { 24 | const ID = req.params.ID; 25 | const redirect = req.query.redirect || '000000000000000000000000'; 26 | const username = req.session.username; 27 | 28 | if (!req.session.login) { 29 | req.flash('error', 'Login required'); 30 | return res.redirect(`/gateway/get-children/${redirect}`); 31 | } 32 | 33 | Gate.update({ 34 | _id: ID 35 | }, { 36 | $addToSet: { 37 | doneList: username 38 | } 39 | }, function(err) { 40 | if (err) return next(err); 41 | 42 | return res.redirect(`/gateway/get-children/${redirect}`); 43 | }); 44 | } 45 | 46 | function removeDoneList(req, res, next) { 47 | const ID = req.params.ID; 48 | const redirect = req.query.redirect || '000000000000000000000000'; 49 | const username = req.session.username; 50 | 51 | if (!req.session.login) { 52 | req.flash('error', 'Login required'); 53 | return res.redirect(`/gateway/get-children/${redirect}`); 54 | } 55 | 56 | Gate.update({ 57 | _id: ID 58 | }, { 59 | $pull: { 60 | doneList: username 61 | } 62 | }, function(err) { 63 | if (err) return next(err); 64 | 65 | return res.redirect(`/gateway/get-children/${redirect}`); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /server/controllers/gateway/ojscraper.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const ojscraper = require('ojscraper'); 3 | const router = express.Router(); 4 | const {isAdmin} = require('middlewares/userGroup'); 5 | const _ = require('lodash'); 6 | 7 | router.get('/problemInfo/:ojname/:problemID', isAdmin, getProblemInfo); 8 | router.get('/userInfo/:ojname/:username', getUserInfo); 9 | 10 | module.exports = { 11 | addRouter(app) { 12 | app.use('/gateway/ojscraper', router); 13 | }, 14 | }; 15 | 16 | async function getProblemInfo(req, res, next) { 17 | const {ojname, problemID} = req.params; 18 | const credential = 19 | _.get(require('world').secretModule, 'ojscraper.loj.credential'); 20 | try { 21 | const info = await ojscraper.getProblemInfo({ 22 | ojname, problemID, credential, 23 | }); 24 | return res.json(info); 25 | } catch (err) { 26 | console.log(err); 27 | return res.json({error: err}); 28 | } 29 | } 30 | 31 | async function getUserInfo(req, res, next) { 32 | const {ojname, username} = req.params; 33 | const subojname = 'uva'; 34 | const credential = 35 | _.get(require('world').secretModule, 'ojscraper.loj.credential'); 36 | try { 37 | const info = await ojscraper.getUserInfo({ 38 | ojname, username, subojname, credential, 39 | }); 40 | return res.json({ 41 | solveCount: info.solveCount, 42 | }); 43 | } catch (err) { 44 | console.log(err); 45 | return res.json({error: err}); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/controllers/index/indexController.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | router.get('/', get_index); 5 | router.get('/bugsandhugs', get_bugsandhugs); 6 | router.get('/faq', get_faq); 7 | 8 | module.exports = { 9 | addRouter(app) { 10 | app.use('/', router); 11 | } 12 | }; 13 | 14 | /** 15 | *Implementation 16 | */ 17 | function get_index(req, res) { 18 | return res.render('index/index'); 19 | } 20 | 21 | function get_bugsandhugs(req, res) { 22 | return res.render('index/bugsandhugs'); 23 | } 24 | 25 | function get_faq(req, res) { 26 | return res.render('index/faq'); 27 | } 28 | -------------------------------------------------------------------------------- /server/controllers/notebook/otherController.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const Notebook = require('mongoose').model('Notebook'); 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/recent', get_recent); 7 | 8 | module.exports = { 9 | addRouter(app) { 10 | app.use('/notebook', router); 11 | } 12 | }; 13 | 14 | /** 15 | *Implementation 16 | */ 17 | function get_recent(req, res, next) { 18 | Notebook 19 | .find() 20 | .select('updatedAt createdAt title slug') 21 | .sort({ 22 | updatedAt: -1 23 | }) 24 | .limit(25) 25 | .exec(function(err, notes) { 26 | if (err) { 27 | return next(err); 28 | } 29 | return res.render('notebook/recent', { 30 | notes 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /server/controllers/user/loginController.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const User = require('mongoose').model('User'); 3 | const recaptcha = require('express-recaptcha'); 4 | const allowSignUp = require('middlewares/allowSignUp'); 5 | const _ = require('lodash'); 6 | const {sendEmailVerification} = require('./verificationController'); 7 | 8 | const router = express.Router(); 9 | 10 | router.get('/login', get_login); 11 | router.post('/login', post_login); 12 | router.get('/register', [recaptcha.middleware.render, allowSignUp], get_register); 13 | router.post('/register', [recaptcha.middleware.verify, allowSignUp], post_register); 14 | router.get('/logout', get_logout); 15 | 16 | module.exports = { 17 | addRouter(app) { 18 | app.use('/user', router); 19 | } 20 | }; 21 | 22 | /** 23 | *Implementation 24 | */ 25 | function get_login(req, res) { 26 | return res.render('user/login'); 27 | } 28 | 29 | function post_login(req, res, next) { 30 | const email = User.normalizeEmail(req.body.email); 31 | const password = req.body.password; 32 | 33 | User.findOne({ 34 | email 35 | }) 36 | .exec(function(err, user) { 37 | if (err) return next(err); 38 | if (!user) { 39 | req.flash('error', 'Email address not found or Password did not match'); 40 | return res.redirect('/user/login'); 41 | } 42 | if (user.comparePassword(password)) { 43 | req.flash('success', 'Successfully logged in'); 44 | req.session.login = true; 45 | req.session.verified = user.verified; 46 | if (!user.verified) req.session.verificationValue = user.verificationValue; 47 | req.session.email = email; 48 | req.session.status = user.status; 49 | req.session.userId = user._id; 50 | req.session.username = user.username; 51 | return res.redirect('/'); 52 | } else { 53 | req.flash('error', 'Email address not found or Password did not match'); 54 | return res.redirect('/user/login'); 55 | } 56 | }); 57 | } 58 | 59 | function get_register(req, res) { 60 | return res.render('user/register', { 61 | recaptcha: req.recaptcha 62 | }); 63 | } 64 | 65 | async function post_register(req, res, next) { 66 | if (req.recaptcha.error) { 67 | req.flash('error', 'Please complete the captcha'); 68 | return res.redirect('/user/register'); 69 | } 70 | const email = User.normalizeEmail(req.body.email); 71 | const password = User.createHash(req.body.password); 72 | 73 | const user = new User({ 74 | email, 75 | password, 76 | verificationValue: _.random(100000, 999999), 77 | }); 78 | 79 | try { 80 | await user.save(); 81 | req.flash('success', 'Successfully registered'); 82 | req.session.login = true; 83 | req.session.verified = false; 84 | req.session.verificationValue = user.verificationValue; 85 | req.session.email = email; 86 | req.session.status = user.status; 87 | req.session.userId = user._id; 88 | } catch (err) { 89 | if (err.code === 11000) { 90 | req.flash('error', 'Email address already exists'); 91 | } else { 92 | req.flash('error', `An error occured while creating user. Error code: ${err.code}`); 93 | } 94 | return res.redirect('/user/register'); 95 | } 96 | 97 | try { 98 | await sendEmailVerification(user.email, user.verificationValue); 99 | req.flash('success', 'Verification Code sent to your email'); 100 | } catch (err) { 101 | req.flash('error', `An error occured while sending email for verfication.`); 102 | } finally { 103 | return res.redirect('/user/verify'); 104 | } 105 | } 106 | 107 | function get_logout(req, res, next) { 108 | req.session.destroy(function(err) { 109 | if (err) next(err); 110 | return res.redirect('/'); 111 | }); 112 | } 113 | -------------------------------------------------------------------------------- /server/controllers/user/verificationController.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const User = require('mongoose').model('User'); 3 | const {login} = require('middlewares/login'); 4 | const mailer = require('mailer').mailer; 5 | 6 | const router = express.Router(); 7 | 8 | router.get('/verify', login, get_verify); 9 | router.post('/verify', login, post_verify); 10 | router.get('/send-code', login, get_send_code); 11 | 12 | module.exports = { 13 | addRouter(app) { 14 | app.use('/user', router); 15 | }, 16 | sendEmailVerification, 17 | }; 18 | 19 | /** 20 | *Implementation 21 | */ 22 | function get_verify(req, res) { 23 | return res.render('user/verify.pug'); 24 | } 25 | 26 | function post_verify(req, res) { 27 | if (req.session.verified) { 28 | req.flash('info', 'Email already verified'); 29 | return res.redirect('/'); 30 | } 31 | const code = req.body.code; 32 | 33 | if (code !== req.session.verificationValue) { 34 | req.flash('error', 'Wrong verification code'); 35 | return res.redirect('/user/verify'); 36 | } 37 | 38 | User.findOne({ 39 | email: req.session.email 40 | }).exec(function(err, user) { 41 | if (err) { 42 | req.flash('error', 'Some error occured. Try again.'); 43 | return res.redirect('/user/verify'); 44 | } 45 | user.verified = true; 46 | user.verificationValue = undefined; 47 | user.save(function(err) { 48 | if (err) { 49 | req.flash('error', 'Some error occured. Try again.'); 50 | return res.redirect('/user/verify'); 51 | } 52 | req.session.verified = true; 53 | req.flash('success', 'Verification successful'); 54 | return res.redirect('/'); 55 | }); 56 | }); 57 | } 58 | 59 | async function sendEmailVerification(emailAddress, verificationValue) { 60 | const email = { 61 | to: [emailAddress], 62 | from: 'CPPS BACS ', 63 | subject: 'Verfication Code for CPPS', 64 | text: `Here is your verification code: ${verificationValue}`, 65 | html: `Here is your verification code: ${verificationValue}`, 66 | }; 67 | return mailer.sendMail(email); 68 | } 69 | 70 | async function get_send_code(req, res) { 71 | if (req.session.verified) { 72 | req.flash('info', 'Email already verified'); 73 | return res.redirect('/'); 74 | } 75 | 76 | try { 77 | await sendEmailVerification(req.session.email, req.session.verificationValue); 78 | req.flash('success', 'Verification Code sent to your email'); 79 | } catch (err) { 80 | console.log(err); 81 | req.flash('error', 'There was some error while sending verification code. Try again.'); 82 | } finally { 83 | return res.redirect('/user/verify'); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /server/models/classroomModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const ObjectId = mongoose.Schema.Types.ObjectId; 3 | 4 | const classroomSchema = new mongoose.Schema({ 5 | name: { 6 | type: String, 7 | set: removeNullOrBlank, 8 | required: true, 9 | }, 10 | coach: { 11 | type: ObjectId, 12 | ref: 'User', 13 | required: true, 14 | index: true, 15 | }, 16 | students: [{type: ObjectId, ref: 'User'}], 17 | }, { 18 | timestamps: true, 19 | }); 20 | 21 | mongoose.model('Classroom', classroomSchema); 22 | 23 | /* 24 | * Implementation 25 | */ 26 | 27 | function removeNullOrBlank(data) { 28 | if (data === null || data === '') return undefined; 29 | return data; 30 | } 31 | 32 | /** 33 | * Related To: 34 | * 1. ProblemList 35 | */ 36 | -------------------------------------------------------------------------------- /server/models/contestModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const ObjectId = mongoose.Schema.Types.ObjectId; 3 | 4 | const contestSchema = new mongoose.Schema({ 5 | name: { 6 | type: String, 7 | required: true, 8 | set: removeNullOrBlank, 9 | trim: true, 10 | }, 11 | link: { 12 | type: String, 13 | set: removeNullOrBlank, 14 | trim: true, 15 | }, 16 | classroomId: { 17 | type: ObjectId, 18 | ref: 'Classroom', 19 | }, 20 | coach: { 21 | type: ObjectId, 22 | ref: 'User', 23 | }, 24 | }, { 25 | timestamps: true, 26 | }); 27 | 28 | mongoose.model('Contest', contestSchema); 29 | 30 | /* 31 | * Implementation 32 | */ 33 | 34 | function removeNullOrBlank(data) { 35 | if (data === null || data === '') return undefined; 36 | return data; 37 | } 38 | -------------------------------------------------------------------------------- /server/models/gateModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const schema = new mongoose.Schema({ 4 | 5 | // items: folder, text, problem 6 | type: { 7 | type: String, 8 | set: removeNullOrBlank, 9 | required: true, 10 | enum: ['folder', 'text', 'problem'], 11 | }, 12 | // For children query 13 | parentId: { 14 | type: mongoose.Schema.ObjectId, 15 | set: removeNullOrBlank, 16 | }, 17 | // For subtree query 18 | ancestor: [mongoose.Schema.ObjectId], 19 | // To reorder items inside same folder 20 | ind: { 21 | type: Number, 22 | set: removeNullOrBlank, 23 | }, 24 | title: { 25 | type: String, 26 | set: removeNullOrBlank, 27 | trim: true, 28 | }, 29 | // Contains text body 30 | body: { 31 | type: String, 32 | set: removeNullOrBlank, 33 | trim: true, 34 | }, 35 | platform: { 36 | type: String, 37 | set: removeNullOrBlank, 38 | }, 39 | pid: { 40 | type: String, 41 | set: removeNullOrBlank, 42 | trim: true, 43 | }, 44 | // Link for problem or text 45 | link: { 46 | type: String, 47 | set: removeNullOrBlank, 48 | trim: true, 49 | }, 50 | // Stores the userID who solved the problem 51 | doneList: [String], 52 | createdBy: { 53 | type: String, 54 | // required: true enforced by system 55 | }, 56 | lastUpdatedBy: { 57 | type: String, 58 | // required: true enforced by system 59 | }, 60 | }, { 61 | timestamps: true, 62 | }); 63 | 64 | schema.statics.getRoot = function() { 65 | return mongoose.Types.ObjectId('000000000000000000000000'); // eslint-disable-line 66 | }; 67 | 68 | /** 69 | * Deals with "createdBy" and "updatedBy" 70 | */ 71 | schema.pre('save', function(next, req) { 72 | if ( !req.session ) { 73 | return next(); 74 | } 75 | 76 | // only admins are allowed in here 77 | if ( req.session.status == 'user' ) { 78 | return next(); 79 | } 80 | 81 | const doc = this; 82 | 83 | // Don't update when doneList gets changed 84 | if ( this.isNew === false && doc.isModified('doneList')) { 85 | return next(); 86 | } 87 | 88 | if (!doc.createdBy) doc.createdBy = req.session.username; 89 | doc.lastUpdatedBy = req.session.username; 90 | return next(); 91 | }); 92 | 93 | mongoose.model('Gate', schema); 94 | 95 | /* 96 | * Implementation 97 | */ 98 | 99 | function removeNullOrBlank(data) { 100 | if (data === null || data === '') return undefined; 101 | return data; 102 | } 103 | -------------------------------------------------------------------------------- /server/models/notebookModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const noteSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | required: true 7 | }, 8 | slug: { 9 | type: String, 10 | required: true, 11 | unique: true, 12 | validate: { 13 | validator: matchSlug, 14 | message: 'Invalid Slug - Small letters, digits and hyphens only' 15 | } 16 | }, 17 | body: { 18 | type: String 19 | }, 20 | createdBy: { 21 | type: String 22 | // required: true enforced by system 23 | }, 24 | lastUpdatedBy: { 25 | type: String 26 | // required: true enforced by system 27 | } 28 | }, { 29 | timestamps: true 30 | }); 31 | 32 | function matchSlug(val) { 33 | const re = new RegExp('[a-z0-9-]+'); 34 | return re.test(val); 35 | } 36 | 37 | noteSchema.pre('save', function(next, req) { 38 | if ( !req.session ) { 39 | return next(); 40 | } 41 | const doc = this; 42 | if (!doc.createdBy) doc.createdBy = req.session.username; 43 | doc.lastUpdatedBy = req.session.username; 44 | return next(); 45 | }); 46 | 47 | mongoose.model('Notebook', noteSchema); 48 | -------------------------------------------------------------------------------- /server/models/ojnames.js: -------------------------------------------------------------------------------- 1 | const fourDigits = '^\\d{4}$'; 2 | const capitalAlphaNumeric = '^[A-Z0-9_]+$'; 3 | const normal = '^[A-Za-z0-9_\\-.]+$'; 4 | const digitsOnly = '^\\d+$'; 5 | 6 | // TODO: Show full names in bracket 7 | 8 | const data = [ 9 | { 10 | name: 'atc', 11 | displayName: 'AtCoder', 12 | format: '^[a-z0-9_]+_[a-z]$', 13 | usernamePattern: normal, 14 | profileLink: 'http://kenkoooo.com/atcoder/?user=$$$$$', 15 | }, 16 | { 17 | name: 'cc', 18 | displayName: 'CodeChef', 19 | format: capitalAlphaNumeric, 20 | usernamePattern: normal, 21 | profileLink: 'https://www.codechef.com/users/$$$$$', 22 | }, 23 | { 24 | name: 'cf', 25 | displayName: 'Codeforces', 26 | format: '^\\d+[A-Z]$', 27 | usernamePattern: normal, 28 | profileLink: 'http://codeforces.com/profile/$$$$$', 29 | }, 30 | { 31 | name: 'csa', 32 | displayName: 'CSAcademy', 33 | format: normal, 34 | usernamePattern: normal, 35 | profileLink: 'https://csacademy.com/user/$$$$$', 36 | }, 37 | { 38 | name: 'hdu', 39 | displayName: 'HDU', 40 | format: fourDigits, 41 | usernamePattern: normal, 42 | profileLink: 'http://acm.hdu.edu.cn/userstatus.php?user=$$$$$', 43 | }, 44 | { 45 | name: 'loj', 46 | displayName: 'LightOJ', 47 | format: fourDigits, 48 | usernamePattern: digitsOnly, 49 | profileLink: 'http://www.lightoj.com/volume_userstat.php?user_id=$$$$$', 50 | }, 51 | { 52 | name: 'poj', 53 | displayName: 'POJ', 54 | format: fourDigits, 55 | usernamePattern: normal, 56 | profileLink: 'http://poj.org/userstatus?user_id=$$$$$', 57 | }, 58 | { 59 | name: 'spoj', 60 | displayName: 'SPOJ', 61 | format: capitalAlphaNumeric, 62 | usernamePattern: normal, 63 | profileLink: 'http://www.spoj.com/users/$$$$$', 64 | }, 65 | { 66 | name: 'uva', 67 | displayName: 'UVa Online Judge', 68 | format: '^\\d{3,5}$', // 3 to 5 digits 69 | usernamePattern: normal, 70 | profileLink: 'http://uhunt.onlinejudge.org/u/$$$$$', 71 | }, 72 | { 73 | name: 'vjudge', 74 | displayName: 'VJudge', 75 | profileLink: 'https://vjudge.net/user/$$$$$', 76 | }, 77 | ]; 78 | 79 | const ojnamesOnly = data.map((x)=>x.name); 80 | 81 | module.exports = { 82 | data, 83 | ojnamesOnly, 84 | }; 85 | -------------------------------------------------------------------------------- /server/models/problemBankModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const schema = new mongoose.Schema({ 4 | title: { 5 | required: true, 6 | type: String, 7 | set: removeNullOrBlank, 8 | trim: true, 9 | }, 10 | platform: { 11 | required: true, 12 | type: String, 13 | set: removeNullOrBlank, 14 | }, 15 | problemId: { 16 | required: true, 17 | type: String, 18 | set: removeNullOrBlank, 19 | trim: true, 20 | }, 21 | // Link for problem or text 22 | link: { 23 | required: true, 24 | type: String, 25 | set: removeNullOrBlank, 26 | trim: true, 27 | }, 28 | }); 29 | 30 | schema.index({platform: 1, problemId: 1}, {unique: true}); 31 | 32 | mongoose.model('ProblemBank', schema); 33 | 34 | /* 35 | * Implementation 36 | */ 37 | 38 | function removeNullOrBlank(data) { 39 | if (data === null || data === '') return undefined; 40 | return data; 41 | } 42 | -------------------------------------------------------------------------------- /server/models/problemListModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const ObjectId = mongoose.Schema.Types.ObjectId; 3 | 4 | const schema = new mongoose.Schema({ 5 | title: { 6 | required: true, 7 | type: String, 8 | set: removeNullOrBlank, 9 | trim: true, 10 | }, 11 | createdBy: { 12 | type: ObjectId, 13 | required: true, 14 | ref: 'User', 15 | }, 16 | problems: [{ 17 | platform: String, 18 | problemId: String, 19 | title: String, 20 | link: String, 21 | }], 22 | sharedWith: { 23 | type: [{type: ObjectId, ref: 'Classroom'}], 24 | default: [], 25 | }, 26 | }, { 27 | timestamps: true, 28 | }); 29 | 30 | 31 | mongoose.model('ProblemList', schema); 32 | 33 | /* 34 | * Implementation 35 | */ 36 | 37 | function removeNullOrBlank(data) { 38 | if (data === null || data === '') return undefined; 39 | return data; 40 | } 41 | -------------------------------------------------------------------------------- /server/models/ratingModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const ObjectId = mongoose.Schema.Types.ObjectId; 3 | 4 | const ratingSchema = new mongoose.Schema({ 5 | userId: { 6 | type: ObjectId, 7 | ref: 'User', 8 | required: true, 9 | }, 10 | classroomId: { 11 | type: ObjectId, 12 | ref: 'Classroom', 13 | required: true, 14 | }, 15 | currentRating: { 16 | type: Number, 17 | required: true, 18 | set: removeNullOrBlank, 19 | }, 20 | }, { 21 | timestamps: true, 22 | }); 23 | 24 | mongoose.model('Rating', ratingSchema); 25 | 26 | /* 27 | * Implementation 28 | */ 29 | 30 | function removeNullOrBlank(data) { 31 | if (data === null || data === '') return undefined; 32 | return data; 33 | } 34 | -------------------------------------------------------------------------------- /server/models/settingModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const schema = new mongoose.Schema({ 4 | key: { 5 | type: String, 6 | required: true, 7 | unique: true, 8 | }, 9 | value: { 10 | type: String, 11 | required: true, 12 | }, 13 | createdBy: { 14 | type: String, 15 | // required: true enforced by system 16 | }, 17 | lastUpdatedBy: { 18 | type: String, 19 | // required: true enforced by system 20 | }, 21 | }, { 22 | timestamps: true, 23 | }); 24 | 25 | /** 26 | * Deals with "createdBy" and "updatedBy" 27 | */ 28 | schema.pre('save', function(next, req) { 29 | const doc = this; 30 | if (!doc.createdBy) doc.createdBy = req.session.username; 31 | doc.lastUpdatedBy = req.session.username; 32 | return next(); 33 | }); 34 | 35 | mongoose.model('Setting', schema); 36 | -------------------------------------------------------------------------------- /server/models/standingModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const ObjectId = mongoose.Schema.Types.ObjectId; 3 | 4 | const standingSchema = new mongoose.Schema({ 5 | username: { 6 | type: String, 7 | required: true, 8 | }, 9 | userId: { 10 | type: ObjectId, 11 | ref: 'User', 12 | required: true, 13 | }, 14 | position: { 15 | type: Number, 16 | required: true, 17 | set: removeNullOrBlank, 18 | }, 19 | contestId: { 20 | type: ObjectId, 21 | ref: 'Contest', 22 | required: true, 23 | }, 24 | classroomId: { // Find all ratings for user U who is member of classroom C 25 | type: ObjectId, 26 | ref: 'Classroom', 27 | required: true, 28 | }, 29 | coach: { 30 | type: ObjectId, 31 | ref: 'User', 32 | }, 33 | previousRating: { 34 | type: Number, 35 | required: true, 36 | set: removeNullOrBlank, 37 | }, 38 | newRating: { 39 | type: Number, 40 | required: true, 41 | set: removeNullOrBlank, 42 | }, 43 | }, { 44 | timestamps: true, 45 | }); 46 | 47 | mongoose.model('Standing', standingSchema); 48 | 49 | /* 50 | * Implementation 51 | */ 52 | 53 | function removeNullOrBlank(data) { 54 | if (data === null || data === '') return undefined; 55 | return data; 56 | } 57 | -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const mongoosePaginate = require('mongoose-paginate'); 3 | const validator = require('validator'); 4 | const bcrypt = require('bcryptjs'); 5 | 6 | const userSchema = new mongoose.Schema({ 7 | username: { 8 | type: String, 9 | unique: true, 10 | sparse: true, // Allows null 11 | // TODO: Validate 12 | maxlength: 256, 13 | }, 14 | email: { 15 | type: String, 16 | required: true, 17 | unique: true, 18 | validate: { 19 | validator: validator.isEmail, 20 | message: 'Email not valid', 21 | }, 22 | maxlength: 256, 23 | }, 24 | password: { 25 | type: String, 26 | required: true, 27 | minlength: 6, 28 | maxlength: 256, 29 | }, 30 | status: { 31 | type: String, 32 | required: true, 33 | default: 'user', 34 | enum: ['root', 'admin', 'user'], 35 | }, 36 | verified: { 37 | type: Boolean, 38 | required: true, 39 | default: false, 40 | }, 41 | verificationValue: { 42 | type: String, 43 | }, 44 | /** Stores usernames/userIDs of the user in various online judge */ 45 | ojStats: [{ 46 | ojname: String, 47 | userIds: [String], /** Some people have multiple CF accounts for example*/ 48 | solveCount: Number, 49 | solveList: [String], 50 | }], 51 | }, { 52 | timestamps: true, 53 | }); 54 | 55 | userSchema.plugin(mongoosePaginate); 56 | 57 | userSchema.statics.createSalt = function() { 58 | return bcrypt.genSaltSync(10); 59 | }; 60 | userSchema.statics.createHash = function(val) { 61 | return bcrypt.hashSync(val, 10); 62 | }; 63 | userSchema.methods.comparePassword = function(val) { 64 | return bcrypt.compareSync(val, this.password); 65 | }; 66 | userSchema.statics.normalizeEmail = validator.normalizeEmail; 67 | 68 | mongoose.model('User', userSchema); 69 | -------------------------------------------------------------------------------- /server/node_modules/escapeLatex/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(input) { 2 | let output = ''; 3 | let state = 'normal'; 4 | 5 | const len = input.length; 6 | 7 | for (let i = 0; i < len;) { 8 | const c = input[i]; 9 | if (c === '$') { 10 | // Something is going to happen 11 | if (state === 'normal') { 12 | // Maybe go from normal to latex? 13 | if (i + 1 < len && input[i + 1] === '$') { 14 | output += '$$'; 15 | i += 2; 16 | state = 'double$'; 17 | } else { 18 | output += '$'; 19 | i++; 20 | state = 'single$'; 21 | } 22 | } else if (state === 'double$') { 23 | // Close double $$ latex? 24 | if (i + 1 < len && input[i + 1] === '$') { 25 | // Close down $$ 26 | output += '$$'; 27 | i += 2; 28 | state = 'normal'; 29 | } else { 30 | // No change in state 31 | output += c; 32 | i++; 33 | } 34 | } else if (state === 'single$') { 35 | // Close single $ latex? 36 | output += c; 37 | i++; 38 | state = 'normal'; 39 | } 40 | } else { 41 | if (state === 'normal') { 42 | output += c; 43 | i++; 44 | } else if (state === 'single$' || state === 'double$') { 45 | // Escape special characters 46 | if (c === '_' || c === '*') { 47 | output += `\\${c}`; 48 | } else { 49 | output += c; 50 | } 51 | i++; 52 | } 53 | } 54 | } 55 | 56 | return output; 57 | }; 58 | -------------------------------------------------------------------------------- /server/node_modules/logger/index.js: -------------------------------------------------------------------------------- 1 | const {createLogger, format, transports} = require('winston'); 2 | const {combine, timestamp, prettyPrint} = format; 3 | 4 | const logger = createLogger({ 5 | format: combine( 6 | timestamp(), 7 | prettyPrint() 8 | ), 9 | transports: [ 10 | new transports.Console({ 11 | colorize: true, 12 | }), 13 | new transports.File({ 14 | filename: 'logs/combined.log', 15 | format: format.json(), 16 | }), 17 | ], 18 | exceptionHandlers: [ 19 | new transports.File({ 20 | filename: 'logs/exceptions.log', 21 | }), 22 | ], 23 | }); 24 | 25 | module.exports = logger; 26 | -------------------------------------------------------------------------------- /server/node_modules/mailer/index.js: -------------------------------------------------------------------------------- 1 | const mailApi = require('world').secretModule.mailApi; 2 | const nodemailer = require('nodemailer'); 3 | const transport = require('nodemailer-mailgun-transport'); 4 | const domain = require('world').secretModule.domain; 5 | 6 | const mailer = nodemailer.createTransport(transport({ 7 | auth : { 8 | api_key: mailApi, 9 | domain: `no-reply.${domain}`, 10 | } 11 | })); 12 | 13 | module.exports = { 14 | mailer, 15 | }; 16 | -------------------------------------------------------------------------------- /server/node_modules/middlewares/allowSignUp.js: -------------------------------------------------------------------------------- 1 | /** Middleware that controls whether user Sign Up is allowed or not 2 | * 3 | * Signup is controlled by "invite_only" settings value 4 | */ 5 | const settings = require('settings'); 6 | module.exports = function(req, res, next) { 7 | if (settings.getKey('invite_only') === "on") { 8 | req.flash("info", "Sign up is currently switched off. You need an invitation."); 9 | return res.redirect("/"); 10 | } 11 | next(); 12 | } 13 | -------------------------------------------------------------------------------- /server/node_modules/middlewares/flash.js: -------------------------------------------------------------------------------- 1 | /*Responsible for inserting all flash messages to res.locals for rendering*/ 2 | /*One time use only*/ 3 | 4 | module.exports = function(req, res, next) { 5 | const flash = { 6 | info: req.flash('info'), 7 | success: req.flash('success'), 8 | warning: req.flash('warn'), 9 | error: req.flash('error') 10 | }; 11 | res.locals.flash = JSON.stringify(flash); 12 | next(); 13 | }; 14 | -------------------------------------------------------------------------------- /server/node_modules/middlewares/login.js: -------------------------------------------------------------------------------- 1 | function apiLogin(req, res, next) { 2 | const regexApi = /^\/api\/v1/g; 3 | if (regexApi.exec(req.originalUrl)) { 4 | if (req.session && req.session.login) return next(); 5 | return res.status(401).json({ 6 | status: 401, 7 | message: 'Unauthorized: You need to login', 8 | }); 9 | } else { 10 | return next(); 11 | } 12 | } 13 | 14 | function login(req, res, next) { 15 | const sess = req.session || {}; 16 | if (sess.login) return next(); 17 | else { 18 | req.flash('info', 'Please login and try again'); 19 | return res.status(401).redirect('/user/login'); 20 | } 21 | }; 22 | 23 | module.exports = { 24 | login, 25 | apiLogin, 26 | }; 27 | -------------------------------------------------------------------------------- /server/node_modules/middlewares/passSession.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pass session variables to jade context 3 | */ 4 | 5 | module.exports = function(req, res, next) { 6 | if ( req.session.login ) { 7 | res.locals.login = true; 8 | res.locals.email = req.session.email; 9 | res.locals.status = req.session.status; 10 | res.locals.superUser = req.session.status !== 'user'; 11 | res.locals.username = req.session.username; 12 | } 13 | next(); 14 | }; 15 | -------------------------------------------------------------------------------- /server/node_modules/middlewares/privateSite.js: -------------------------------------------------------------------------------- 1 | /** 2 | *Middle ware to make site private depending on settings 3 | */ 4 | const settings = require('settings'); 5 | module.exports = function(req, res, next) { 6 | if(settings.getKey('private_site') !== 'on') return next(); 7 | const url = req.originalUrl; 8 | if ( url === '/' || url === '/user/login' || url === '/user/register' ) { 9 | return next(); 10 | } 11 | const regexApi = /^\/api\/v1/g; 12 | if (regexApi.exec(url)) { 13 | return next(); 14 | } 15 | const sess = req.session || {}; 16 | if (sess.login) return next(); 17 | else { 18 | req.flash('info', 'This is a private site. You must login to continue.'); 19 | return res.redirect('/user/login'); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /server/node_modules/middlewares/userGroup.js: -------------------------------------------------------------------------------- 1 | /* Middle ware to maintain User Hierarchy 2 | * 3 | * 0. root 4 | * 1. admin 5 | */ 6 | 7 | module.exports = { 8 | isRoot(req, res, next) { 9 | const sess = req.session || {}; 10 | if (sess.status === 'root') return next(); 11 | 12 | const regexApi = /^\/api\/v1/g; 13 | if (regexApi.exec(req.originalUrl)) { 14 | return res.status(401).json({ 15 | status: 401, 16 | message: 'Unauthorized: You are not Root', 17 | route: req.originalUrl, 18 | }); 19 | } 20 | 21 | req.flash('info', 'You must be root to proceed'); 22 | return res.status(401).redirect('/'); 23 | }, 24 | 25 | isAdmin(req, res, next) { 26 | const sess = req.session || {}; 27 | if (sess.status === 'root' || sess.status === 'admin') return next(); 28 | 29 | const regexApi = /^\/api\/v1/g; 30 | if (regexApi.exec(req.originalUrl)) { 31 | return res.status(401).json({ 32 | status: 401, 33 | message: 'Unauthorized: You are not Admin', 34 | route: req.originalUrl, 35 | }); 36 | } 37 | 38 | req.flash('info', 'You must be at least "admin" to proceed'); 39 | return res.status(401).redirect('/'); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /server/node_modules/middlewares/username.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A middleware that blocks user from progressing if their username is not set 3 | * 4 | * Blocks only if user is logged in and verified. 5 | */ 6 | module.exports = function(req, res, next) { 7 | if ( !req.session.login ) return next(); 8 | if ( !req.session.verified) return next(); 9 | 10 | // Don't stop if user is logging out or setting username 11 | const url = req.originalUrl; 12 | if ( url === '/user/profile/set-username' || url === '/user/logout' ) { 13 | return next(); 14 | } 15 | if ( !req.session.username ) { 16 | req.flash('info', 'You need to set your username'); 17 | return res.redirect('/user/profile/set-username'); 18 | } 19 | next(); 20 | }; 21 | -------------------------------------------------------------------------------- /server/node_modules/middlewares/verification.js: -------------------------------------------------------------------------------- 1 | /*Redirect users to verification route if they are logged in but not verified yet 2 | One time use only 3 | */ 4 | module.exports = function(req, res, next) { 5 | const sess = req.session || {}; 6 | const url = req.originalUrl; 7 | if (sess.login && !sess.verified && url !== '/user/verify' && url !== '/user/logout' && url !== '/user/send-code') { 8 | req.flash('info', 'Please verify your email'); 9 | return res.redirect('/user/verify'); 10 | } 11 | return next(); 12 | }; 13 | -------------------------------------------------------------------------------- /server/node_modules/queue/index.js: -------------------------------------------------------------------------------- 1 | const kue = require('kue-unique'); 2 | const queue = kue.createQueue({ 3 | redis: 'redis://cpps_redis_1:6379', 4 | }); 5 | 6 | module.exports = queue; 7 | -------------------------------------------------------------------------------- /server/node_modules/settings/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Setting = mongoose.model('Setting'); 3 | const _ = require('lodash'); 4 | 5 | const settings = { 6 | invite_only: false, 7 | private_site: false, 8 | }; 9 | 10 | Setting.find({}).exec() 11 | .then(function(pairs){ 12 | return _.forEach(pairs, function(pair){ 13 | settings[pair.key] = pair.value; 14 | }) 15 | }) 16 | .catch(console.log); 17 | 18 | module.exports = { 19 | getKey(key){ 20 | return settings[key]; 21 | }, 22 | setKey(key, value){ 23 | return Setting.findOneAndUpdate({key}, {key, value}, {upsert: true}).exec() 24 | .then(function(){ 25 | return settings[key] = value; 26 | }) 27 | }, 28 | getAll() { 29 | return settings; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/node_modules/world/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.resolve(__dirname, '../../'); 4 | const secretPath = path.join(rootPath, 'secret.js'); 5 | const secretModule = require(secretPath); 6 | 7 | module.exports = { 8 | rootPath, 9 | secretModule 10 | }; 11 | -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | .recaptcha div { 2 | margin-left: auto; 3 | margin-right: auto; 4 | } 5 | 6 | textarea { 7 | height: 50rem; 8 | resize: none; 9 | } 10 | 11 | .table-cell-center td, .table-cell-center th { 12 | text-align: center; 13 | vertical-align: middle; 14 | } 15 | 16 | .list-group-item:hover { 17 | background-color: #eceeef; 18 | color: #292b2c; 19 | } 20 | 21 | .debug { 22 | border: 1px red solid; 23 | } 24 | 25 | button { 26 | cursor: pointer; /* Cause bootstrap doesn't show pointer*/ 27 | } 28 | 29 | /** Loading Modal 30 | * Taken from https://stackoverflow.com/questions/1964839/how-can-i-create-a-please-wait-loading-animation-using-jquery 31 | * 32 | **/ 33 | /* Start by setting display:none to make this hidden. 34 | Then we position it in relation to the viewport window 35 | with position:fixed. Width, height, top and left speak 36 | for themselves. Background we set to 80% white with 37 | our animation centered, and no-repeating */ 38 | .loadingSign { 39 | background: rgba( 255, 255, 255, .8 ) 40 | url('http://i.stack.imgur.com/FhHRx.gif') 41 | 50% 50% 42 | no-repeat; 43 | display: none; 44 | height: 100%; 45 | left: 0; 46 | position: fixed; 47 | top: 0; 48 | width: 100%; 49 | z-index: 1000000; 50 | } 51 | 52 | /* When the body has the loading class, we turn 53 | the scrollbar off with overflow:hidden */ 54 | body.loadingModal { 55 | overflow: hidden; 56 | } 57 | 58 | /* Anytime the body has the loading class, our 59 | modal element will be visible */ 60 | body.loadingModal .loadingSign { 61 | display: block; 62 | } 63 | -------------------------------------------------------------------------------- /src/js/common/simplemde.js: -------------------------------------------------------------------------------- 1 | const marked = require('marked'); 2 | const escapeLatex = require('escapeLatex'); 3 | 4 | const simplemde = new SimpleMDE({ 5 | element: $(".simplemde")[0], 6 | previewRender: function(plainText, preview) { // Async method 7 | const text = escapeLatex(plainText); 8 | return marked(text); 9 | } 10 | }); 11 | 12 | //setup before functions 13 | let typingTimer; //timer identifier 14 | let doneTypingInterval = 1000; //time in ms, 5 second for example 15 | const $input = $('.CodeMirror'); 16 | 17 | //on keyup, start the countdown 18 | $input.on('keyup', function () { 19 | clearTimeout(typingTimer); 20 | typingTimer = setTimeout(doneTyping, doneTypingInterval); 21 | }); 22 | 23 | //on keydown, clear the countdown 24 | $input.on('keydown', function () { 25 | clearTimeout(typingTimer); 26 | }); 27 | 28 | //user is "finished typing," do something 29 | function doneTyping () { 30 | MathJax.Hub.Typeset(); 31 | } 32 | -------------------------------------------------------------------------------- /src/js/gateway/getChildren/index.js: -------------------------------------------------------------------------------- 1 | const fillView = require('js/layout/fill-view.js'); 2 | 3 | function main() { 4 | showFormParts(); 5 | $('#type').change(showFormParts); 6 | $('#inlineForm').submit(handleSubmit); 7 | } 8 | 9 | main(); 10 | 11 | /* Implementation*/ 12 | 13 | function hideEverything() { 14 | $('.folder, .problem').hide(); 15 | } 16 | 17 | function showFormParts() { 18 | hideEverything(); 19 | 20 | const val = $('#type option:selected').val(); 21 | 22 | if (val === 'problem') { 23 | $('.problem').show(); 24 | } else if (val === 'folder') { 25 | $('.folder').show(); 26 | } 27 | fillView($); 28 | } 29 | 30 | function handleSubmit(event) { 31 | $('.d-hide').hide(); 32 | 33 | const itemType = $('#type option:selected').val(); 34 | 35 | if( itemType == 'folder' ) { 36 | const lastIndex = parseInt($('.indexNumber').last().text()); 37 | $('#ind').val(lastIndex? lastIndex+1: 1); 38 | return true; 39 | } 40 | 41 | event.preventDefault(); 42 | event.stopImmediatePropagation(); 43 | 44 | const ojname = $('#platform').val(); 45 | const problemID = $('#pid').val(); 46 | 47 | if ( !ojname || !problemID) { 48 | alert('platform or problem id cannot be blank'); 49 | return false; 50 | } 51 | 52 | $('#wait').show(); 53 | $('#problemDetails').modal('show'); 54 | 55 | $.ajax({ 56 | url: `/gateway/ojscraper/problemInfo/${ojname}/${problemID}`, 57 | }).done(function(info) { 58 | if ( info.error ) { 59 | console.log(info.error); 60 | $('#error').html(`

${info.error.message}

`); 61 | $('#wait').hide(); 62 | $('#error').show(); 63 | return; 64 | } 65 | $('#wait').hide(); 66 | const lastIndex = parseInt($('.indexNumber').last().text()); 67 | $('#p-index').val(lastIndex? lastIndex+1: 1); 68 | $('#p-platform').val(ojname); 69 | $('#p-pid').val(problemID); 70 | $('#p-title').val(info.title); 71 | $('#p-link').val(info.link); 72 | $('#p-link2').attr('href', info.link); 73 | $('#showDetails').show(); 74 | $('#addProblem').show(); 75 | }); 76 | return false; 77 | } 78 | -------------------------------------------------------------------------------- /src/js/layout/fill-view.js: -------------------------------------------------------------------------------- 1 | module.exports = function($) { 2 | const windowHeight = $(window).height(); 3 | const htmlHeight = $('html').height(); 4 | 5 | const fillView = $('#fill-view').height(); 6 | if (htmlHeight + 5 < windowHeight) { 7 | const dif = windowHeight - htmlHeight; 8 | $('#fill-view').animate({ 9 | "min-height": fillView + dif 10 | }, 300); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/js/layout/flash.js: -------------------------------------------------------------------------------- 1 | module.exports = function($) { 2 | $.notify.defaults({ 3 | autoHideDelay: 15000 4 | }); 5 | 6 | for (const val in flash) { 7 | const len = flash[val].length; 8 | for (let i = 0; i < len; i++) { 9 | $.notify(flash[val][i], val); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/js/layout/formLogic.js: -------------------------------------------------------------------------------- 1 | /** Logics related to forms */ 2 | 3 | /* Prevent user from clicking submit button twice*/ 4 | function clickFactory() { 5 | const clickedItems = {}; 6 | return function() { 7 | if (clickedItems[this]) return false; 8 | clickedItems[this] = true; 9 | $(this).addClass('disabled'); 10 | return true; 11 | }; 12 | } 13 | let currentClickFn = clickFactory(); 14 | function disableOnClick() { 15 | currentClickFn = clickFactory(); 16 | $('.disableOnClick').on('click', currentClickFn); 17 | }; 18 | 19 | disableOnClick(); 20 | 21 | $('form').on('input', function() { 22 | $('.disableOnClick').off('click', currentClickFn).removeClass('disabled'); 23 | disableOnClick(); 24 | }); 25 | -------------------------------------------------------------------------------- /src/js/layout/index.js: -------------------------------------------------------------------------------- 1 | /*Add flash messages*/ 2 | require('./flash')($); 3 | 4 | require('./fill-view')($); 5 | 6 | $('.moment-date').each(function() { 7 | let date = new Date($(this).text()); 8 | date = moment(date).fromNow(); 9 | $(this).html(date); 10 | }); 11 | 12 | // For popover 13 | $(function () { 14 | $('[data-toggle="popover"]').popover() 15 | }); 16 | $('.popover-dismiss').popover({ 17 | trigger: 'focus' 18 | }) 19 | 20 | // Hide all elements with d-hide class 21 | $('.d-hide').hide(); 22 | 23 | require('./formLogic'); 24 | -------------------------------------------------------------------------------- /src/js/user/changePassword.js: -------------------------------------------------------------------------------- 1 | require('jquery-validation'); 2 | 3 | $('form').validate({ 4 | rules: { 5 | repeat: { 6 | equalTo: '#newpass' 7 | } 8 | }, 9 | messages: { 10 | repeat: { 11 | equalTo: 'Must match with new password' 12 | } 13 | }, 14 | errorClass: 'alert alert-danger' 15 | }); 16 | -------------------------------------------------------------------------------- /src/js/user/profile.js: -------------------------------------------------------------------------------- 1 | /** Display a form for each username not set*/ 2 | $('.setUserName').on('click', function(event) { 3 | const ojname = $(this).attr('data-ojname'); 4 | $('#ojname').val(ojname); 5 | $('#setUserNameModal').modal('show'); 6 | }); 7 | 8 | $('.syncButton').click(function(event) { 9 | $('body').addClass('loadingModal'); 10 | }); 11 | 12 | $('#setUserNameModal form').on('keyup keypress', function(e) { 13 | const keyCode = e.keyCode || e.which; 14 | if (keyCode === 13) { 15 | e.preventDefault(); 16 | return false; 17 | } 18 | }); 19 | 20 | $('.checkUserId').click(function(event) { 21 | $('body').addClass('loadingModal'); 22 | const ojname = $('#ojname').val(); 23 | const userId = $('#userId').val(); 24 | 25 | if ( !userId ) { 26 | alert('User Id cannot be blank'); 27 | $('body').removeClass('loadingModal'); 28 | return false; 29 | } 30 | 31 | $.ajax({ 32 | url: `/gateway/ojscraper/userInfo/${ojname}/${userId}`, 33 | }).done(function(info) { 34 | if ( info.error ) { 35 | console.log(info.error); 36 | alert('Some error occured'); 37 | $('body').removeClass('loadingModal'); 38 | return false; 39 | } 40 | // eslint-disable-next-line max-len 41 | const normalMsg = `The user ${userId} has solved ${info.solveCount}. Is this you?`; 42 | // eslint-disable-next-line max-len 43 | const vjudgeMsg = `The user ${userId} has solved ${info.solveCount} in Vjudge-UVa. Is this you?`; 44 | if ( confirm(ojname === 'vjudge'? vjudgeMsg: normalMsg) ) { 45 | $('#setUserNameModal form').submit(); 46 | } 47 | $('body').removeClass('loadingModal'); 48 | }); 49 | 50 | return false; 51 | }); 52 | -------------------------------------------------------------------------------- /src/js/user/register.js: -------------------------------------------------------------------------------- 1 | require('jquery-validation'); 2 | 3 | $('form').validate({ 4 | rules: { 5 | repass: { 6 | equalTo: '#password' 7 | } 8 | }, 9 | messages: { 10 | repass: { 11 | equalTo: 'Must match with password typed above' 12 | } 13 | }, 14 | errorClass: 'alert alert-danger' 15 | }); 16 | -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import "css/markdown"; 2 | @import "css/style"; 3 | -------------------------------------------------------------------------------- /test/nodemailer.test.js: -------------------------------------------------------------------------------- 1 | /** A script to check if nodemailer is configured correctly or not*/ 2 | 3 | const mailer = require('mailer').mailer; 4 | 5 | const args = process.argv.slice(2); 6 | if ( !args.length ) { 7 | console.log("Please pass email as argument"); 8 | process.exit(); 9 | } 10 | const email = args[0]; 11 | 12 | const emailMail = { 13 | to: [email], 14 | from: 'CPPS BACS ', 15 | subject: 'Testing email subject', 16 | text: `Testing email text`, 17 | html: `Test email html` 18 | }; 19 | 20 | mailer.sendMail(emailMail, function(err) { 21 | if (err) { 22 | console.log("Some error occured"); 23 | console.log(err); 24 | process.exit(1); 25 | } else { 26 | console.log('email sent'); 27 | } 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /views/admin/dashboard.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view.justify-content-center 5 | .col.text-center 6 | h2 Dashboard 7 | .list-group.d-inline-flex 8 | a(href='/admin/invite-user').list-group-item.list-group-item-action 9 | i.fa.fa-user-plus.mr-2 10 | span Invite User 11 | a(href='/admin/user/list').list-group-item.list-group-item-action 12 | i.fa.fa-vcard.mr-2 13 | span User List 14 | if status == 'root' 15 | a(href='/root/settings').list-group-item.list-group-item-action 16 | i.fa.fa-wrench.mr-2 17 | span Settings 18 | a(href='/gateway/sync-problems').list-group-item.list-group-item-action.danger 19 | i.fa.fa-refresh.mr-2 20 | span Sync Problems 21 | -------------------------------------------------------------------------------- /views/admin/invite.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .offset-sm-3.col-sm-6.text-center 6 | h2 Invite User 7 | form(action='/admin/invite-user', method='post') 8 | .form-group 9 | label(for='email') Email 10 | input.form-control( id='email', name='email', type='email', maxlength=256, required, placeholder='Enter Email') 11 | .form-group 12 | label(for='password') Password 13 | input.form-control( id='password', name='password', type='password', minlength=6, maxlength=256, title='Must be between 6 to 256 characters', required, placeholder='Enter Password' ) 14 | input.btn.btn-primary.disableOnClick(type='submit', value='Invite') 15 | -------------------------------------------------------------------------------- /views/admin/userList.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row#fill-view 5 | .col 6 | h3.text-center User List 7 | table.table.table-cell-center 8 | thead 9 | tr 10 | th Index 11 | th User 12 | th Status 13 | th Created 14 | tbody 15 | each user, index in users 16 | tr 17 | td= index + 1 18 | if (user.username) 19 | td 20 | a(href=`/user/profile/${user.username}`)= user.username 21 | else 22 | td= user.email 23 | td 24 | .dropdown 25 | button.btn.btn-secondary.dropdown-toggle#dropdownStatusButton(data-toggle="dropdown",aria-haspopup="true", aria-expanded="false")= user.status 26 | .dropdown-menu(aria-labelledby="dropdownStatusButton") 27 | a.dropdown-item(href=`/admin/user/change-status/${user.email}/root`) root 28 | a.dropdown-item(href=`/admin/user/change-status/${user.email}/admin`) admin 29 | a.dropdown-item(href=`/admin/user/change-status/${user.email}/user`) user 30 | 31 | td.moment-date= user.createdAt 32 | -------------------------------------------------------------------------------- /views/gateway/doneList.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .col 6 | h3.text-center= title 7 | table.table.table-cell-center 8 | thead 9 | tr 10 | th Index 11 | th Username 12 | tbody 13 | each d, ind in doneList 14 | tr 15 | td= ind + 1 16 | td 17 | a(href=`/users/profile/${d}`)= d 18 | -------------------------------------------------------------------------------- /views/gateway/editItem.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view.m-b-1 5 | .col-sm.text-center 6 | h3 Edit Item 7 | form(action='/gateway/edit-item', method='post') 8 | input(type='hidden', value=item._id, name='id') 9 | .row 10 | .col-sm-6 11 | .form-group 12 | label(for='parentId') Parent ID 13 | input#parentId.form-control(type='text', value=item.parentId.toString(), name='parentId', required) 14 | .col-sm-6 15 | .form-group 16 | label(for='title') Title 17 | input#title.form-control(type='text', name='title', value=item.title, required) 18 | .row 19 | .col-sm-6 20 | .form-group 21 | label(for='ind') Index 22 | input#ind.form-control(type='number', name='ind', step=0.1, value=item.ind, required) 23 | .col-sm-6 24 | .form-group 25 | label(for='type') Type 26 | select#type.form-control(required,disabled) 27 | option(value="folder",selected=item.type==="folder") Folder 28 | option(value="problem",selected=item.type==="problem") Problem 29 | option(value="text",selected=item.type==="text") Text 30 | input(type="hidden", name="type", value=item.type) 31 | 32 | #problem 33 | .form-group 34 | label(for='link') Link 35 | input#link.form-control(type='url', name='link', value=item.link) 36 | .row 37 | .col-sm-6 38 | .form-group 39 | label(for='platform') Platform 40 | select#platform.form-control(name="platform", onchange="addPattern(this.selectedOptions[0].dataset.pattern);") 41 | each val in ojnames.data 42 | - if(!val.format) val.format=".+"; 43 | option(value=val.name, selected=item.platform===val.name, data-pattern=val.format)= val.name 44 | .col-sm-6 45 | .form-group 46 | label(for='pid') Problem ID 47 | input#pid.form-control(type='text', name='pid', value=item.pid) 48 | 49 | 50 | 51 | #body.form-group 52 | label(for='body2') 53 | .text-left 54 | textarea#body2.form-control(name='body')= item.body 55 | 56 | 57 | input.btn.btn-primary.mr-1.disableOnClick(type='submit', value='Update') 58 | button.btn.btn-danger.mr-1(type='button', data-target='#delete', data-toggle="modal") Delete 59 | a.btn.btn-secondary(href=`/gateway/get-children/${item.parentId}`) Cancel 60 | 61 | 62 | #delete.modal.fade 63 | form(action=`/gateway/delete-item/${item._id}`,method='post') 64 | .modal-dialog 65 | .modal-content 66 | .modal-header 67 | button.close(type='button', data-dismiss='modal') × 68 | h3.modal-title Delete Item 69 | .modal-body 70 | p Are you sure you want to delete this? If so, please enter the text "Delete" below: 71 | .form-group 72 | label(for='item') Enter Text: 73 | input.form-control#item(name='delete',type='text',required,pattern='Delete',title='Please type the word: Delete') 74 | .modal-footer 75 | input(type="hidden" value=item.parentId name='parentId') 76 | input.btn.btn-danger.mr-1.disableOnClick(type='submit', value='Delete') 77 | button.btn.btn-secondary(data-dismiss="modal") Cancel 78 | 79 | block scripts 80 | script( defer src="/js/common/simplemde.js") 81 | script( defer src='/js/gateway/addItem/index.js') 82 | script(defer). 83 | var pattern = $("#platform").find(":selected")[0].dataset.pattern; 84 | addPattern(pattern); 85 | 86 | function addPattern(pattern) { 87 | if ( !pattern ) pattern = ".+"; 88 | $('#pid') 89 | .attr('pattern', pattern) 90 | .attr('title', `Please match the RegExp: ${pattern}`); 91 | } 92 | -------------------------------------------------------------------------------- /views/gateway/form/inline-addItem.pug: -------------------------------------------------------------------------------- 1 | form#inlineForm.form-inline.justify-content-center(action=`/gateway/add-item/${root._id}`, method='post') 2 | .form-group.mx-1 3 | label.sr-only(for="type") Type 4 | select#type.form-control(name='type',required) 5 | option(value='folder') Folder 6 | option(value='problem', selected) Problem 7 | 8 | //- Form fields for problem. Gets intercepted by jquery. 9 | .form-group.mx-1.problem 10 | label.sr-only(for='platform') Platform 11 | select#platform.form-control(name="platform", onchange="addPattern(this.selectedOptions[0].dataset.pattern);") 12 | option(disabled, selected, value=undefined) Select a Platform 13 | each val in ojnames.data 14 | option(value=val.name, data-pattern=val.format )= val.name 15 | .form-group.mx-1.problem 16 | label.sr-only(for='pid') Problem ID 17 | input#pid.form-control(type='text', name='pid', placeholder="Problem ID") 18 | 19 | //- Form fields for folder. Goes to server directly 20 | .form-group.mx-1.folder.d-hide 21 | label.sr-only(for='title') Title 22 | input#title.form-control(type='text', name='title', placeholder="Name") 23 | .form-group.mx-1.d-hide 24 | label.sr-only(for='ind') Index 25 | input#ind.form-control(type='number', name='ind') 26 | 27 | button.btn.btn-primary.mr-1.disableOnClick(type='submit') Insert 28 | 29 | block scripts 30 | script( defer src='/js/gateway/getChildren/index.js') 31 | script( defer ). 32 | function addPattern(pattern) { 33 | if ( !pattern ) pattern = ".+"; 34 | $('#pid') 35 | .attr('pattern', pattern) 36 | .attr('title', `Please match the RegExp: ${pattern}`); 37 | } 38 | 39 | 40 | #problemDetails.modal(tabindex="-1") 41 | form(action=`/gateway/add-item/${root._id}`, method='post') 42 | .modal-dialog 43 | .modal-content 44 | .modal-header 45 | h3.modal-title Problem Details 46 | button.close(type='button', data-dismiss='modal') × 47 | .modal-body 48 | #error.d-hide 49 | #wait 50 | p Retrieving details. Please Wait. 51 | i.fa.fa-spinner.fa-spin 52 | #showDetails.d-hide 53 | .form-group.row.d-hide 54 | label.col-3.col-form-label(for='p-index') Index 55 | input.col-9.form-control-plaintext(id='p-index', name='ind', value="0") 56 | .form-group.row.d-hide 57 | label.col-3.col-form-label(for='p-type') Type 58 | input.col-9.form-control-plaintext(id='p-type', name='type', value='problem') 59 | .form-group.row 60 | label.col-3.col-form-label(for='p=platform') Platform 61 | input#p-platform.col-9.form-control-plaintext(type='text', name='platform') 62 | .form-group.row 63 | label.col-3.col-form-label(for='p-pid') PID 64 | input#p-pid.col-9.form-control-plaintext(type='text', name='pid') 65 | .form-group.row 66 | label.col-3.col-form-label(for='p-title') Title 67 | input.col-9.form-control-plaintext(id='p-title',type='text', name='title', required) 68 | .form-group.row 69 | label.col-3.col-form-label(for='p-link') 70 | a#p-link2(href='#', target="_blank") Link 71 | input#p-link.col-9.form-control-plaintext(type='url', name='link') 72 | 73 | .modal-footer 74 | button#addProblem.btn.btn-primary.d-hide(type="submit") Add Problem 75 | button.btn.btn-secondary(type="button" data-dismiss="modal") Close 76 | -------------------------------------------------------------------------------- /views/gateway/getChildren.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row#fill-view 5 | .col-sm.text-center 6 | .row.small.mb-2 7 | .col 8 | if ( root.parentId ) 9 | a.mr-1(href=`/gateway/get-children/${root.parentId}`) #[i.fa.fa-level-up] Go Up 10 | .row.mb-2 11 | .col 12 | if (superUser) 13 | include form/inline-addItem.pug 14 | 15 | .row.align-items-center 16 | .col-sm-4 17 | span Solved in this folder: #{root.userCount}/#{root.totalCount} 18 | .col-sm-4 19 | h2= root.title 20 | table.table.text-center.table-bordered.table-cell-center 21 | thead 22 | th Index 23 | th Title 24 | th Type 25 | th Action 26 | if (login) 27 | th Done 28 | if (superUser) 29 | th Admin 30 | tbody 31 | each val, index in items 32 | tr 33 | td 34 | a(name=val._id) 35 | span.indexNumber= index + 1 36 | if (superUser) 37 | span= " | " + val.ind 38 | if val.type !== "problem" && val.type !== "text" 39 | td 40 | a(href="/gateway/get-children/"+val._id)= val.title 41 | td 42 | i.fa.fa-folder-o(aria-hidden="true") 43 | span   #{val.userCount}/#{val.totalCount} 44 | td #[a(href="/gateway/get-children/"+val._id) Open] 45 | else if val.type === "text" 46 | td 47 | a(href="/gateway/read-item/"+val._id)= val.title 48 | td #[i.fa.fa-file-text-o(aria-hidden="true")] 49 | td #[a(href="/gateway/read-item/"+val._id) Read] 50 | else if val.type === "problem" 51 | td 52 | a(target="_blank", href=val.link)= val.platform + " " + val.pid + " - " + val.title 53 | td #[i.fa.fa-link(aria-hidden="true") ] 54 | td 55 | a(target="_blank", href=val.link) Solve 56 | if val.body 57 | span= " | " 58 | a(href="/gateway/read-item/"+val._id) Hints 59 | if (login) 60 | if ( val.type === "problem" || val.type === "text" ) 61 | td 62 | if doneList.indexOf(val._id.toString()) > -1 63 | i.fa.fa-check-square(aria-hidden="true") 64 | else 65 | i.fa.fa-square-o(aria-hidden="true") 66 | span     67 | a.btn.btn-light.py-0(href=`/gateway/done-list/${val._id}`) 68 | i.fa.fa-user-times(aria-hidden="true") 69 | span= val.userSolved 70 | else 71 | td --- 72 | if (superUser) 73 | td #[a(href="/gateway/edit-item/"+val._id) Edit] 74 | -------------------------------------------------------------------------------- /views/gateway/leaderboard.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row#fill-view 5 | .col 6 | h3.text-center Leaderboard 7 | table.table.table-cell-center 8 | thead 9 | tr 10 | th Index 11 | th Username 12 | th Total Solved 13 | each ojname in ojnames 14 | th= ojname.name 15 | tbody 16 | each user, index in data 17 | tr 18 | td= index + 1 19 | td 20 | a(href=`/user/profile/${user.username}`)= user.username 21 | td= user.totalSolved 22 | each ojname in ojnames 23 | if (user[ojname.name] !== undefined) 24 | td= user[ojname.name] 25 | else 26 | td - 27 | -------------------------------------------------------------------------------- /views/gateway/random.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .col 6 | h3.text-center A Random Unsolved Problem 7 | table.table.table-cell-center 8 | thead 9 | th Index 10 | th Title 11 | th Type 12 | th Action 13 | if (login) 14 | th Done 15 | tbody 16 | tr 17 | td= 1 18 | td 19 | a(target="_blank", href=problem.link)= problem.platform + " " + problem.pid + " - " + problem.title 20 | td #[i.fa.fa-link(aria-hidden="true") ] 21 | td 22 | a(target="_blank", href=problem.link) Solve 23 | if problem.body 24 | span= " | " 25 | a(href="/gateway/read-item/"+problem._id) Hints 26 | if (login) 27 | if ( problem.type === "problem" || problem.type === "text" ) 28 | td 29 | i.fa.fa-square-o(aria-hidden="true") 30 | span     31 | a.btn.btn-light.py-0(href=`/gateway/done-list/${problem._id}`) 32 | i.fa.fa-user-times(aria-hidden="true") 33 | span= problem.userSolved 34 | else 35 | td --- 36 | -------------------------------------------------------------------------------- /views/gateway/readItem.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row#fill-view 5 | .col 6 | if ( superUser ) 7 | .text-center.small 8 | a(href=`/gateway/edit-item/${item._id}`) #[i.fa.fa-edit] Edit 9 | .text-center.small 10 | a(href=`/gateway/get-children/${item.parentId}`) #[i.fa.fa-level-up] Go Up 11 | .markdown-body. 12 |

#{item.title} - Hints

13 | !{item.body} 14 | -------------------------------------------------------------------------------- /views/gateway/recent.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .col 6 | h3.text-center Recently Added/Modified 7 | table.table.table-cell-center 8 | thead 9 | tr 10 | th Created 11 | th Title 12 | if (status == 'root') 13 | th Created By* 14 | tbody 15 | each p in problems 16 | tr 17 | td.moment-date= p.createdAt 18 | td 19 | a(href=`/gateway/get-children/${p.parentId}`) #{p.platform} #{p.pid} #{p.title} 20 | if ( status == 'root' ) 21 | td= p.createdBy 22 | -------------------------------------------------------------------------------- /views/gateway/search.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row#fill-view.align-items-center 5 | .col.text-center 6 | h1 Search Problem 7 | form#inlineForm.form-inline.justify-content-center(action=`/gateway/search-problem`, method='post') 8 | //- Form fields for problem. Gets intercepted by jquery. 9 | .form-group.mx-1.problem 10 | label.sr-only(for='platform') Platform 11 | select#platform.form-control(name="platform", onchange="addPattern(this.selectedOptions[0].dataset.pattern);") 12 | option(disabled, selected, value=undefined) Select a Platform 13 | each val in ojnames 14 | option(value=val.name, data-pattern=val.format )= val.name 15 | .form-group.mx-1.problem 16 | label.sr-only(for='pid') Problem ID 17 | input#pid.form-control(type='text', name='pid', placeholder="Problem ID") 18 | 19 | button.btn.btn-primary.mr-1.disableOnClick(type='submit') Search 20 | 21 | block scripts 22 | script( defer ). 23 | function addPattern(pattern) { 24 | if ( !pattern ) pattern = ".+"; 25 | $('#pid') 26 | .attr('pattern', pattern) 27 | .attr('title', `Please match the RegExp: ${pattern}`); 28 | } 29 | -------------------------------------------------------------------------------- /views/index/bugsandhugs.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center.text-center#fill-view 5 | .col 6 | h3 Bugs & Hugs 7 | 8 | p If you find any kind of bug/mistake anywhere on the site, please kindly open an issue on Github. If possible, please check whether any similar issue is already open or not. 9 | 10 | p For hugs, feel free to send them in any way you want :) 11 | -------------------------------------------------------------------------------- /views/index/faq.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .col 6 | h3.text-center Frequently Asked Questions 7 | 8 | .list-group 9 | .list-group-item 10 | div 11 | h4.list-group-item-heading Is everything on this site 100% accurate 12 | 13 | p.list-group-item-text Of course not. The contents are provided by human, so there is bound to be errors. Some of the codes we have given are not even tested. Same goes for the hints. Do not assume everything you read to be true/correct. Use your own judgment and logic to decide whether the facts given on the site is right or wrong. If you spot any mistake, please report the bug. 14 | 15 | .list-group-item 16 | div 17 | h4.list-group-item-heading Why isn't there any comment/forum section? I want to discuss stuffs. 18 | 19 | p.list-group-item-text There are already plenty of places to discuss stuffs. Creating another location will simply dilute the whole thing. 20 | p.list-group-item-text If you have any question about any particular problem, then it is better to go its corresponding online judge and discuss it on their forum. 21 | p.list-group-item-text If you have question about some article on this site unrelated to any online judge, then please find some other place ( facebook group, codeforces, topcoder, codechef ) to discuss them. 22 | p.list-group-item-text If you want to discuss technical stuffs like mistakes in artcle/code, broken link/css or any kind of bugs, then we have Github for that. 23 | 24 | .list-group-item 25 | div 26 | h4.list-group-item-heading I didn't understand the article on topic X. What should I do? 27 | 28 | p.list-group-item-text Google to find other resources on topic X. There are plenty of resource over net. If you are unable to find any satisfactory result, then try asking questions on forums/sites like stackoverflow, codeforces, topcoder and etc. 29 | 30 | .list-group-item 31 | div 32 | h4.list-group-item-heading It will be really cool to have the feature X. Can you implement it? 33 | 34 | p.list-group-item-text Depends. Feel free to request the feature by raising an issue on github. We can discuss about feasibility, relevance and implementation of the issue there and come to a conclusion. 35 | 36 | .list-group-item 37 | div 38 | h4.list-group-item-heading This FAQ isn't enough. I have more questions. 39 | 40 | p.list-group-item-text Raise an issue on github with a question tag. I will try to answer there. 41 | -------------------------------------------------------------------------------- /views/index/index.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center.text-center#fill-view 5 | .col 6 | p A minimalistic portal for all about competitive programming and problem solving. 7 | p Checkout Notebook for theories and Gateway for practice problems. 8 | -------------------------------------------------------------------------------- /views/layouts/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | 5 | script(async, src="https://www.googletagmanager.com/gtag/js?id=UA-65362633-3") 6 | script. 7 | if (document.location.hostname.search("localhost") === -1) { 8 | window.dataLayer = window.dataLayer || []; 9 | function gtag(){dataLayer.push(arguments);} 10 | gtag('js', new Date()); 11 | gtag('config', 'UA-65362633-3'); 12 | } 13 | title CPPS 14 | 15 | link(rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css") 16 | link(rel="stylesheet", href="/scss/main.css", media="screen", title="no title", charset="utf-8") 17 | link(rel="stylesheet",href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css") 18 | link(rel="stylesheet", href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css") 19 | 20 | block head 21 | 22 | body 23 | .container 24 | include ../partials/header.pug 25 | 26 | .px-2 27 | block content 28 | 29 | include ../partials/footer.pug 30 | 31 | .loadingSign 32 | 33 | script. 34 | var flash = !{flash}; 35 | 36 | script(type='text/x-mathjax-config'). 37 | MathJax.Hub.Config({ 38 | extensions: ["tex2jax.js"], 39 | jax: ["input/TeX", "output/HTML-CSS"], 40 | tex2jax: { 41 | inlineMath: [ ['$','$'], ["\\(","\\)"] ], 42 | displayMath: [ ['$$','$$'], ["\\[","\\]"] ], 43 | processEscapes: true 44 | }, 45 | "HTML-CSS": { availableFonts: ["TeX"] } 46 | }); 47 | script(type='text/javascript', src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML") 48 | 49 | script(src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js") 50 | script(src="https://cdnjs.cloudflare.com/ajax/libs/notify/0.4.2/notify.min.js") 51 | script(src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.2/moment.min.js") 52 | script(src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.1/js/tether.min.js") 53 | script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js") 54 | script(src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js") 55 | script(src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js") 56 | 57 | script(src='/js/layout/index.js') 58 | 59 | block scripts 60 | -------------------------------------------------------------------------------- /views/notebook/addNote.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view.m-b-1 5 | .col.text-center 6 | h3 Add New Note 7 | form(action='/notebook/add-note', method='post') 8 | .form-group 9 | label(for='title') Title 10 | input.form-control(id='title', name='title',type='text',required, value=title) 11 | .form-group 12 | label(for='slug') Slug 13 | input.form-control(id='slug',name='slug',type='text', pattern='[a-z0-9\-]+', title='Small letters, digits and hyphens only',required, value=slug) 14 | .form-group 15 | label(for='body') Body 16 | .text-left 17 | textarea.form-control(id='body',name='body')= body 18 | input.btn.btn-primary.ml-1.disableOnClick(type='submit', value='Insert') 19 | a.btn.btn-secondary.ml-1(href='/notebook') Cancel 20 | 21 | block scripts 22 | script( defer src="/js/common/simplemde.js") 23 | -------------------------------------------------------------------------------- /views/notebook/editNote.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view.m-b-1 5 | .col-sm.text-center 6 | h3 Edit Note: #{slug} 7 | form(action=`/notebook/edit-note/${slug}`, method='post') 8 | .form-group 9 | label(for='title') Title 10 | input.form-control( id='title', name='title',type='text',required, value=title) 11 | .form-group 12 | label(for='slug') Slug 13 | input.form-control(id='slug',name='slug',type='text', pattern='[a-z0-9\-]+', title='Small letters, digits and hyphens only',required, value=slug) 14 | .form-group 15 | label(for='body') Body 16 | .text-left 17 | textarea.form-control(id='body',name='body')= body 18 | 19 | input.btn.btn-primary.ml-1.disableOnClick(type='submit', value='Update') 20 | a.btn.btn-secondary.ml-1(href=`/notebook/view-note/${slug}`) Done 21 | button.btn.btn-danger.ml-1(type='button',data-toggle="modal", data-target="#delete") Delete 22 | 23 | #delete.modal.fade(tabindex="-1",role='dialog', aria-labelledby="myModalLabel") 24 | form(action=`/notebook/delete-note/${slug}`,method='post') 25 | .modal-dialog(role="document") 26 | .modal-content 27 | .modal-header 28 | h4.modal-title(id='myModalLabel') Delete Slug 29 | button.close(type='button', data-dismiss='modal') × 30 | .modal-body 31 | p Are you sure you want to delete this? If so, please enter the slug of the note below: 32 | .form-group 33 | input.form-control(placeholder='Enter Slug',name='slug',type='text',required,pattern=slug,title='Slug entered must match the note to be deleted') 34 | 35 | .modal-footer 36 | input.btn.btn-danger.disableOnClick(type='submit', value='Delete Note') 37 | button.btn.btn-secondary.ml-1(data-dismiss='modal') Cancel 38 | 39 | 40 | block scripts 41 | script( defer src="/js/common/simplemde.js") 42 | -------------------------------------------------------------------------------- /views/notebook/recent.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .col 6 | h3.text-center Recently Added/Modified 7 | table.table.table-cell-center 8 | thead 9 | tr 10 | th Created 11 | th Modified 12 | th Slug 13 | th Title 14 | if ( superUser ) 15 | th Edit 16 | tbody 17 | each note in notes 18 | tr 19 | td.moment-date= note.createdAt 20 | td.moment-date= note.updatedAt 21 | td #[a(href=`/notebook/view-note/${note.slug}`) #{note.slug}] 22 | td= note.title 23 | if ( superUser ) 24 | td.u-text-center 25 | a(href=`/notebook/edit-note/${note.slug}`) 26 | i.fa.fa-edit 27 | -------------------------------------------------------------------------------- /views/notebook/viewNote.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row#fill-view 5 | .col-sm 6 | if ( superUser ) 7 | .text-center 8 | a(href='/notebook/add-note') #[i.fa.fa-plus] Add 9 | a.ml-1(href=`/notebook/edit-note/${note.slug}`) #[i.fa.fa-edit] Edit 10 |
11 | small 12 | span Created By: #{note.createdBy}     13 | span Updated By: #{note.lastUpdatedBy}     14 | 15 | .markdown-body. 16 |

#{note.title}

17 | 18 | Created At: #{note.createdAt.toDateString()}     19 | Updated At: #{note.updatedAt.toDateString()} 20 | 21 | !{note.body} 22 | -------------------------------------------------------------------------------- /views/partials/footer.pug: -------------------------------------------------------------------------------- 1 | footer.text-center.small.text-muted.mt-2 2 | p 3 | span © 2016-2017 All Rights Reserved 4 | a(href="https://github.com/bacsbd/cpps") CPPS 5 | -------------------------------------------------------------------------------- /views/partials/header.pug: -------------------------------------------------------------------------------- 1 | h1.text-center CPPS 2 | 3 | .row.mb-2 4 | .col.d-inline-flex.justify-content-between.nav-tabs 5 | ul.nav 6 | li.nav-item 7 | a.nav-link(href='/') #[i.fa.fa-home] Home 8 | li.nav-item.dropdown.d-inline-flex 9 | .nav-item 10 | a.nav-link(href='/notebook') Notebook 11 | .nav-item 12 | a.nav-link(data-toggle='dropdown', href='#')  #[i.fa.fa-caret-down] 13 | .dropdown-menu 14 | a.dropdown-item(href='/notebook/recent') Recent 15 | li.nav-item.dropdown.d-inline-flex 16 | .nav-item 17 | a.nav-link(href='/gateway') Gateway 18 | .nav-item 19 | a.nav-link(data-toggle='dropdown', href='#')  #[i.fa.fa-caret-down] 20 | .dropdown-menu 21 | a.dropdown-item(href='/gateway/recent') Recent 22 | a.dropdown-item(href='/gateway/leaderboard') Leaderboard 23 | a.dropdown-item(href='/gateway/search-problem') Search Problem 24 | a.dropdown-item(href='/gateway/random-problem') Random Problem 25 | li.nav-item 26 | a.nav-link(href='/faq') FAQ 27 | li.nav-item 28 | a.nav-link(href='/bugsandhugs') Bugs & Hugs 29 | 30 | ul.nav 31 | if (login) 32 | if (superUser) 33 | li.nav-item 34 | a.nav-link(href='/coach') #[i.fa.fa-dashboard] Dashboard 35 | li.nav-item 36 | a.nav-link(href='/admin/dashboard') #[i.fa.fa-user-secret] Admin 37 | li.nav-item 38 | a.nav-link(href=`/users/profile/${username}`) #[i.fa.fa-user] Profile 39 | li.nav-item 40 | a.nav-link(href='/user/logout') #[i.fa.fa-sign-out] Logout 41 | else 42 | li.nav-item 43 | a.nav-link(href='/user/login') #[i.fa.fa-sign-in] Login 44 | -------------------------------------------------------------------------------- /views/root/settings.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view.justify-content-center 5 | .col.text-center 6 | h2 Settings 7 | .list-group.d-inline-flex 8 | form.form-inline.justify-content-center(action=`/root/settings/invite_only`, method='post') 9 | .form-check 10 | label.form-check-label 11 | input(type="checkbox", name="value", checked=(settings.invite_only?"checked":undefined)).form-check-input 12 | span Invite Only 13 | input.ml-2(type="submit", value="Save") 14 | form.form-inline.justify-content-center.mt-1(action=`/root/settings/private_site`, method='post') 15 | .form-check 16 | label.form-check-label 17 | input(type="checkbox", name="value", checked=(settings.private_site?"checked":undefined)).form-check-input 18 | span Private Site 19 | input.ml-2(type="submit", value="Save") 20 | -------------------------------------------------------------------------------- /views/user/changePassword.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .offset-sm-3.col-sm-6.text-center 6 | h3 Change Password 7 | form(action='/user/profile/change-password', method='post') 8 | .form-group 9 | input.form-control( id='current', name='current', type='password', maxlength=256, required, title='Must be between 6 to 256 characters', placeholder='Current Password') 10 | .form-group 11 | input.form-control( id='newpass', name='newpass', type='password', minlength=6, maxlength=256, title='Must be between 6 to 256 characters', required, placeholder='New Password' ) 12 | .form-group 13 | input.form-control( id='repeat', name='repeat', type='password', maxlength=256, required, title='Must be between 6 to 256 characters', placeholder='Repeat Password') 14 | .recaptcha.m-x-auto.m-b-1. 15 | !{recaptcha} 16 | input.btn.btn-primary.disableOnClick(type='submit', value='Submit') 17 | 18 | block scripts 19 | script(src='/js/user/changePassword.min.js') 20 | -------------------------------------------------------------------------------- /views/user/login.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .offset-sm-3.col-sm-6.text-center 6 | h2 Login 7 | form(action='/user/login', method='post') 8 | .form-group 9 | label(for='email') Email 10 | input.form-control( id='email', name='email', type='email', maxlength=256, required, placeholder='Enter Email') 11 | .form-group 12 | label(for='password') Password 13 | input.form-control( id='password', name='password', type='password', minlength=6, maxlength=256, title='Must be between 6 to 256 characters', required, placeholder='Enter Password' ) 14 | input.btn.btn-primary.disableOnClick(type='submit', value='Login') 15 | small New here? Click here to Register. 16 | -------------------------------------------------------------------------------- /views/user/profile.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .text-center 5 | h3 Profile Page 6 | .row.align-items-center.justify-content-center#fill-view 7 | .col.text-center 8 | h4 Personal Info 9 | ul.list-group.d-inline-flex 10 | if(owner) 11 | .list-group-item 12 | i.fa.fa-envelope.mr-2 13 | span= email 14 | .list-group-item 15 | i.fa.fa-user.mr-2 16 | span= displayUser.username 17 | if(owner) 18 | .list-group-item 19 | i.fa.fa-key.mr-2 20 | a(href='/user/profile/change-password') Change Password 21 | .list-group-item 22 | i.fa.fa-users.mr-2 23 | span= displayUser.status[0].toUpperCase() + displayUser.status.slice(1) 24 | if(owner) 25 | .col.text-center 26 | h4 Classrooms 27 | ul.list-group.d-inline-flex 28 | each val, index in classrooms 29 | .list-group-item 30 | a(href=`/classroom/${val._id}`)=`${val.coach.username}/${val.name}` 31 | table.table.text-center.table-bordered.table-cell-center 32 | thead 33 | th Index 34 | th OJ Name 35 | th User ID 36 | th Solve 37 | if(owner) 38 | th Sync 39 | tbody 40 | each val, index in data 41 | tr 42 | td= index + 1 43 | td= val.displayName 44 | if (!val.userId) 45 | if(owner) 46 | td 47 | button.btn.btn-link.setUserName(data-ojname=val.name) Set User ID 48 | else 49 | td - 50 | td - 51 | if(owner) 52 | td - 53 | else 54 | td= val.userId[0] 55 | td= val.solveCount? val.solveCount : 0 56 | if(owner) 57 | td 58 | form.form-inline.d-inline(action=`/user/profile/sync-ojsolve/${val.name}`, method="post") 59 | button.btn.btn-link.syncButton.disableOnClick(type="submit", title="Synchronize Count") 60 | i.fa.fa-refresh.ml-2 61 | 62 | #setUserNameModal.modal(tabindex="-1") 63 | form(action=`/user/profile/set-userId`, method='post') 64 | .modal-dialog 65 | .modal-content 66 | .modal-header 67 | h3.modal-title Set User ID 68 | button.close(type='button', data-dismiss='modal') × 69 | .modal-body 70 | .form-group.row 71 | label.col-3.col-form-label(for='ojname') OJ Name 72 | input.col-9.form-control-plaintext(id='ojname', name='ojname', value="Not set", readonly) 73 | .form-group.row 74 | label.col-3.col-form-label(for='userId') User Id 75 | input.col-9.form-control(id='userId', name='userId') 76 | .modal-footer 77 | button.btn.btn-primary.disableOnClick.checkUserId(type="button") Check 78 | button.btn.btn-secondary(type="button" data-dismiss="modal") Close 79 | 80 | block scripts 81 | script(src="/js/user/profile.js") 82 | -------------------------------------------------------------------------------- /views/user/register.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .offset-sm-3.col-sm-6.text-center 6 | h2 Register 7 | form(action='/user/register', method='post') 8 | .form-group 9 | input.form-control( id='email', name='email', type='email', maxlength=256, required, placeholder='Email') 10 | .form-group 11 | input.form-control( id='password', name='password', type='password', minlength=6, maxlength=256, title='Must be between 6 to 256 characters', required, placeholder='Password') 12 | .form-group 13 | input.form-control( id='repass', name='repass', type='password', minlength=6, maxlength=256, title='Must be between 6 to 256 characters', required, placeholder='Retype Password') 14 | .recaptcha.m-x-auto.m-b-1. 15 | !{recaptcha} 16 | input.btn.btn-primary.disableOnClick(type='submit', value='Register') 17 | small Already a member? Click here to Login. 18 | 19 | block scripts 20 | script(src='/js/user/register.min.js') 21 | -------------------------------------------------------------------------------- /views/user/setUsername.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .offset-sm-3.col-sm-6.text-center 6 | h3 Set Username 7 | form(action='/user/profile/set-username', method='post') 8 | .form-group 9 | input.form-control#username( name='username', maxlength=256, required, title='Must be between 6 to 256 characters', placeholder='Set Username') 10 | input.btn.btn-primary(type='submit', value='Submit') 11 | -------------------------------------------------------------------------------- /views/user/verify.pug: -------------------------------------------------------------------------------- 1 | extends ../layouts/layout.pug 2 | 3 | block content 4 | .row.align-items-center#fill-view 5 | .offset-sm-3.col-sm-6.text-center 6 | .alert.alert-info.m-b-3 You have to verify your email address before proceeding. 7 | h3 Enter Verification Code 8 | form(action='/user/verify', method='post') 9 | .form-group 10 | label(for='code') Code 11 | input.form-control( id='code', name='code', type='text', maxlength=256, required) 12 | input.btn.btn-primary.disableOnClick(type='submit', value='Verify') 13 | small Did not receive your verification code? Click here. 14 | --------------------------------------------------------------------------------