├── .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 |
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 |
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 | Username |
64 | Total |
65 | {ojnamesOnly.map((x)=> {x} | )}
66 |
67 |
68 |
69 | {data.map((u, index)=>{
70 | return (
71 |
72 | {index + 1} |
73 |
74 |
75 |
76 | {u.username}
77 |
78 |
79 | |
80 | {u.totalSolved} |
81 | {ojnamesOnly.map((ojname)=>{u[ojname]?u[ojname]:'-'} | )}
82 |
83 | );
84 | })}
85 |
86 |
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 | List Name |
34 |
35 |
36 |
37 | {problemLists.map((x, index)=>{
38 | return (
39 |
40 | {index + 1} |
41 |
42 | this.setState({
43 | problemListId: x._id,
44 | modalRanklist: true,
45 | })}>
46 | {`${x.createdBy.username} - ${x.title}`}
47 |
48 | |
49 |
50 | )
51 | })}
52 |
53 |
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 |
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 |
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 | Index |
65 | Contest |
66 |
67 |
68 |
69 | { tabulatedContestList }
70 |
71 |
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 |
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 | Position |
77 | Username |
78 | UserId |
79 | New Rating |
80 | Previous Rating |
81 | Delta |
82 |
83 |
84 |
85 | {tr}
86 |
87 |
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 | Classroom |
54 |
55 |
56 |
57 | { this.getRows() }
58 |
59 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------