Tut 12: Exercise
The problem is based on web application called "RateMyTA" where students can rate and review their Teaching Assistants.
Read all the code before answering the questions in the Tut 12 slide
Stuff implemented here:
Server-side Data Management, manage two JSON files on the server:
- a. "users.txt" for storing user credentials (username, hashed password, role)
- b. "ratings.txt" for storing TA ratings and reviews
Pages and Functionality:
- Home page
- TA List page
- Rate a TA page (requires login)
- User Registration page
- User Login/Logout
- About Us page
Utility functions:
- A. calculateAverage(numbers)
- B. generateUniqueID()
- C. capitalizeWords(str)
- D. sortArrayByProperty(array, property, descending = false)
Form Handling:
- Forms for user registration, login, and TA rating submission.
- client-side and server-side validation.
Session Management:
- user sessions for logged-in users.
Code with blanks to answer the questions in the slides:
- File Structure:
RateMyTA/
├── server.js
├── views/
│ ├── (f1)/
│ │ ├── (f2)
│ │ └── (f3)
│ ├── (f4)
│ ├── ta-list.html
│ ├── rate-ta.html
│ ├── register.html
│ ├── login.html
│ └── about.html
├── (f5)/
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── main.js
├── data/
│ ├── users.txt
│ └── ratings.txt
└── utils/
└── helpers.js
- server.js:
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
const bcrypt = require('bcrypt');
const ejs = require('ejs');
const { calculateAverage, generateUniqueID, capitalizeWords, sortArrayByProperty } = require('./utils/helpers');
const app = express();
const PORT = 3000;
const usersFile = path.join(__dirname, 'data', 'users.txt');
const ratingsFile = path.join(__dirname, 'data', 'ratings.txt');
app.use(express.static('public'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({
secret: 'ratemyta-secret',
resave: false,
saveUninitialized: true
}));
// Middleware to inject header and footer
app.use((req, res, next) => {
ejs.renderFile(path.join(__dirname, 'views', 'partials', 'header.ejs'), { user: req.session.user }, (err, header) => {
if (err) return res.status(500).send('Error loading header');
ejs.renderFile(path.join(__dirname, 'views', 'partials', 'footer.ejs'), (err, footer) => {
if (err) return res.status(500).send('Error loading footer');
res.locals.header = header;
res.locals.footer = footer;
next();
});
});
});
// Utility function to read data from text files
function readData(filePath, callback) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) return callback(err);
const parsedData = data.trim().split('\n').map(line => line.split(':'));
callback(null, parsedData);
});
}
// Utility function to write data to text files
function writeData(filePath, data, callback) {
const content = data.map(item => item.join(':')).join('\n');
fs.writeFile(filePath, content, 'utf8', callback);
}
// Routes
app.get('/', (req, res) => {
fs.readFile(path.join(__dirname, 'views', '(2)'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading home page');
res.send(res.locals.header + content + res.locals.footer);
});
});
app.get('/ta-list', (req, res) => {
readData(ratingsFile, (err, ratingsData) => {
if (err) return res.status(500).send('Error reading ratings data');
const taAverages = {};
ratingsData.forEach(([reviewID, taName, rating, review, username]) => {
if (!taAverages[taName]) {
taAverages[taName] = [];
}
taAverages[taName].push(Number(rating));
});
const taList = Object.entries(taAverages).map(([taName, ratings]) => ({
taName: capitalizeWords(taName),
averageRating: calculateAverage(ratings)
}));
const sortedTAList = sortArrayByProperty(taList, 'averageRating', true);
fs.readFile(path.join(__dirname, 'views', 'ta-list.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading TA list page');
const renderedContent = ejs.render(content, { taList: sortedTAList });
res.send(res.locals.header + renderedContent + res.locals.footer);
});
});
});
app.get('/rate-ta', (req, res) => {
if (!req.session.user) {
return res.redirect('/login');
}
fs.readFile(path.join(__dirname, 'views', 'rate-ta.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading rate TA page');
res.send(res.locals.header + content + res.locals.footer);
});
});
app.post('/rate-ta', (req, res) => {
if (!req.session.user) {
return res.redirect('/login');
}
const { taName, rating, review } = req.body;
const reviewID = generateUniqueID();
readData(ratingsFile, (err, ratingsData) => {
if (err) return res.status(500).send('Error reading ratings data');
ratingsData.push([reviewID, taName.toLowerCase(), rating, review, req.session.user]);
writeData(ratingsFile, ratingsData, (err) => {
if (err) return res.status(500).send('Error writing ratings data');
res.redirect('/ta-list');
});
});
});
app.get('/register', (req, res) => {
fs.readFile(path.join(__dirname, 'views', 'register.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading registration page');
res.send(res.locals.header + content + res.locals.footer);
});
});
app.post('(3)', (req, res) => {
const { username, password } = (4);
const hashedPassword = bcrypt.hashSync(password, 10);
fs.readFile(usersFile, 'utf8', (err, data) => {
if (err) return res.status((5)).send('Server error.');
if (data.split((6)).some(line => line.split((7))[0] === username)) {
return res.send('(8)');
}
fs.appendFile(usersFile, `${username}:${hashedPassword}\n`, (err) => {
if (err) return res.status((5)).send('Server error.');
res.send('(9)');
});
});
});
app.get('/login', (req, res) => {
fs.readFile(path.join(__dirname, 'views', 'login.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading login page');
res.send(res.locals.header + content + res.locals.footer);
});
});
app.post('/login', (req, res) => {
const { username, password } = req.body;
readData(usersFile, (err, userData) => {
if (err) return res.status(500).send('Server error.');
const user = userData.find((10));
if (user && bcrypt.compareSync(password, user[1])) {
req.session.user = username;
res.redirect('/');
} else {
res.redirect('/login');
}
});
});
app.get('/logout', (req, res) => {
req.session.destroy();
res.redirect('/');
});
app.get('/about', (req, res) => {
fs.readFile(path.join(__dirname, 'views', 'about.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading about page');
res.send(res.locals.header + content + res.locals.footer);
});
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
- utils/helpers.js:
function calculateAverage(numbers) {
if (numbers.length === 0) return 0;
const sum = numbers.reduce((acc, num) => acc + num, 0);
return (sum / numbers.length).toFixed(2);
}
function generateUniqueID() {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 5);
}
function capitalizeWords(str) {
return str.replace(/\b\w/g, l => l.toUpperCase());
}
function sortArrayByProperty(array, property, descending = false) {
return array.sort((a, b) => descending ? b[property] - a[property] : a[property] - b[property]);
}
module.exports = {
calculateAverage,
generateUniqueID,
capitalizeWords,
sortArrayByProperty
};
- views/partials/header.ejs:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RateMyTA</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>RateMyTA</h1>
<nav>
<a href="/">Home</a>
<a href="/ta-list">TA List</a>
<% if (user) { %>
<a href="/rate-ta">Rate a TA</a>
<a href="/logout">Logout</a>
<% } else { %>
<a href="/register">Register</a>
<a href="/login">Login</a>
<% } %>
<a href="/about">About Us</a>
</nav>
<div id="current-time"></div>
</header>
<main>
- views/partials/footer.ejs:
</main>
<footer>
<a href="/terms">Terms of Service</a>
<a href="/privacy">Privacy Policy</a>
</footer>
<script src="/js/main.js"></script>
</body>
</html>
- public/js/main.js:
function updateTime() {
const now = new Date();
document.getElementById('current-time').textContent = now.toLocaleString();
}
setInterval(updateTime, 1000);
updateTime();
- views/about.html:
<h2>About RateMyTA</h2>
<p>RateMyTA is a platform designed to help students share their experiences with Teaching Assistants and make informed decisions about their education.</p>
<p>Our mission is to promote transparency and improve the quality of education by providing a space for constructive feedback and recognition of outstanding TAs.</p>
<p>We believe that by fostering open communication between students and TAs, we can create a better learning environment for everyone.</p>
- views/home.html:
<h2>Welcome to RateMyTA</h2>
<p>RateMyTA is a platform where students can rate and review their Teaching Assistants. Help your fellow students by sharing your experiences and find out which TAs are highly recommended by others.</p>
<p>To get started, check out our <a href="/ta-list">TA List</a> or <a href="/register">create an account</a> to submit your own ratings.</p>
- views/login.html:
<h2>Login</h2>
<form action="/login" method="(1)">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<button type="submit">Login</button>
</form>
<p>Don't have an account? <a href="/register">Register here</a></p>
- views/rate-ta.html:
<h2>Rate a TA</h2>
<form action="/rate-ta" method="POST">
<label for="taName">TA Name:</label>
<input type="text" id="taName" name="taName" required>
<label for="rating">Rating (1-5):</label>
<select id="rating" name="rating" required>
<option value="1">1 - Poor</option>
<option value="2">2 - Fair</option>
<option value="3">3 - Good</option>
<option value="4">4 - Very Good</option>
<option value="5">5 - Excellent</option>
</select>
<label for="review">Review:</label>
<textarea id="review" name="review" rows="4" required></textarea>
<button type="submit">Submit Rating</button>
</form>
- views/register.html:
<h2>Register</h2>
<form action="/register" method="POST">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<button type="submit">Register</button>
</form>
<p>Already have an account? <a href="/login">Login here</a></p>
- views/ta-list.html:
<h2>TA List</h2>
<ul class="ta-list">
<% taList.forEach(ta => { %>
<li class="ta-item">
<span class="ta-name"><%= ta.taName %></span>
<span class="ta-rating">Average Rating: <%= ta.averageRating %></span>
</li>
<% }) %>
</ul>
- public/css/style.css:
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--background-color: #ecf0f1;
--text-color: #34495e;
}
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 0;
}
header {
background-color: var(--primary-color);
color: white;
text-align: center;
padding: 1rem;
}
header h1 {
margin: 0;
}
nav {
display: flex;
justify-content: center;
margin-top: 1rem;
}
nav a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
margin: 0 0.5rem;
border-radius: 5px;
transition: background-color 0.3s;
}
nav a:hover {
background-color: rgba(255, 255, 255, 0.2);
}
main {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
h2 {
color: var(--primary-color);
}
form {
background-color: white;
padding: 2rem;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
input[type="text"],
input[type="password"],
textarea,
select {
width: 100%;
padding: 0.5rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 3px;
}
button {
background-color: var(--secondary-color);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #27ae60;
}
.ta-list {
list-style-type: none;
padding: 0;
}
.ta-item {
background-color: white;
margin-bottom: 1rem;
padding: 1rem;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.ta-name {
font-weight: bold;
color: var(--primary-color);
}
.ta-rating {
color: var(--secondary-color);
}
footer {
background-color: var(--primary-color);
color: white;
text-align: center;
padding: 1rem;
position: fixed;
bottom: 0;
width: 100%;
}
footer a {
color: white;
text-decoration: none;
margin: 0 0.5rem;
}
#current-time {
font-size: 0.9rem;
margin-top: 0.5rem;
}