Merge branch 'main' into alexa-auth-frontend

This commit is contained in:
afazio1
2024-04-11 00:40:04 -04:00
21 changed files with 860 additions and 17 deletions

View File

@@ -8,3 +8,30 @@ Repository for a course project of CS4675/CS6675 at Georgia Institute of Technol
<li>Raise a PR once you are ready and have checked your code for errors.</li>
<li>Mention a bullet point summary for all the features you are pushing as part of the PR within the description to ease the review process</li>
</ul>
## Getting Started
### Frontend
For our frontend we are using React + Vite + TypeScript!
- **React** -> JavaScript Framework for creating reactive user interfaces.
- **Vite** -> Development environment / build tool. Gives us access to features like hot reload, bundling, and plugins.
- **TypeScript** -> A superset of JavaScript allowing for static types.
Here's how to set up + run the frontend environment:
1. Download and install [Node.js](https://nodejs.org/en/download) v18+
Check your node version:
```sh
node -v
```
2. Clone the repo using Git
3. Install dependencies
```sh
cd frontend
npm install
```
4. Start the development server
```sh
npm run dev
```
5. Visit http://localhost:5173

67
backend/README.md Normal file
View File

@@ -0,0 +1,67 @@
# Django Backend
This is the backend for the Django project.
## Setup
1. Create a virtual environment:
```bash
python3 -m venv venv
```
2. Activate the virtual environment:
- For macOS/Linux:
```bash
source venv/bin/activate
```
- For Windows:
```bash
venv\Scripts\activate
```
3. Install the project dependencies:
```bash
pip install -r requirements.txt
```
## Running the Server
To start the Django server, run the following command:
```bash
python3 manage.py runserver
```
By default, the server will run on `http://localhost:8000/`.
## Admin Panel
In order to access the admin panel, you need to create a superuser. To do this, run the following command:
```bash
python3 manage.py createsuperuser
```
Then, you can access the admin panel by visiting `http://localhost:8000/admin/` and logging in with the superuser credentials.
## Contributing to the Codebase
1. Create a new branch:
```bash
git checkout -b <branch-name>
```
2. Make your changes.
3. Use black to format your code:
```bash
black .
```
4. Make your changes and commit them:
```bash
git add .
git commit -m "Your commit message"
```
5. Push your changes to the remote repository:
```bash
git push origin <branch-name>
```
6. Create a Pull Request on GitHub.

View File

@@ -1,5 +1,10 @@
from django.contrib import admin
from api.models import PeerUser
from api.models import PeerUser, Semester, Professor, Course, Topic, File
# Register your models here.
admin.site.register(PeerUser)
admin.site.register(Topic)
admin.site.register(Professor)
admin.site.register(Course)
admin.site.register(File)
admin.site.register(Semester)

View File

@@ -0,0 +1,138 @@
# Generated by Django 4.2.11 on 2024-03-26 04:40
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("api", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="Course",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=20)),
],
),
migrations.CreateModel(
name="Professor",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name="Semester",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=20)),
],
),
migrations.CreateModel(
name="Topic",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("description", models.TextField(blank=True)),
],
),
migrations.AddField(
model_name="peeruser",
name="last_poll",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.CreateModel(
name="File",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("filename", models.CharField(max_length=200)),
(
"original_author",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
(
"peer_users",
models.ManyToManyField(
related_name="shared_files", to=settings.AUTH_USER_MODEL
),
),
(
"professor",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="api.professor",
),
),
(
"semester",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="api.semester",
),
),
(
"topic",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="api.topic",
),
),
],
),
]

View File

@@ -0,0 +1,65 @@
# Generated by Django 4.2.11 on 2024-03-26 16:30
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("api", "0002_course_professor_semester_topic_peeruser_last_poll_and_more"),
]
operations = [
migrations.AddField(
model_name="file",
name="course",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="files",
to="api.course",
),
),
migrations.AlterField(
model_name="file",
name="original_author",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="owned_files",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="file",
name="professor",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="files",
to="api.professor",
),
),
migrations.AlterField(
model_name="file",
name="semester",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="files",
to="api.semester",
),
),
migrations.AlterField(
model_name="file",
name="topic",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="files",
to="api.topic",
),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 4.2.11 on 2024-03-26 17:52
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("api", "0003_file_course_alter_file_original_author_and_more"),
]
operations = [
migrations.AddField(
model_name="course",
name="number",
field=models.CharField(
default=django.utils.timezone.now, max_length=20, unique=True
),
preserve_default=False,
),
migrations.AlterField(
model_name="course",
name="name",
field=models.CharField(blank=True, max_length=40),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.11 on 2024-03-26 23:14
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("api", "0004_course_number_alter_course_name"),
]
operations = [
migrations.AddField(
model_name="file",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
]

View File

@@ -5,3 +5,58 @@ from django.db import models
class PeerUser(AbstractUser):
ip_address = models.GenericIPAddressField(blank=True, null=True)
last_poll = models.DateTimeField(auto_now_add=True)
class Topic(models.Model):
name = models.CharField(max_length=100, blank=False, null=False)
description = models.TextField(blank=True)
def __str__(self) -> str:
return self.name
class Professor(models.Model):
name = models.CharField(max_length=100, blank=False, null=False)
def __str__(self) -> str:
return self.name
class Semester(models.Model):
name = models.CharField(max_length=20, blank=False, null=False)
def __str__(self) -> str:
return self.name
class Course(models.Model):
name = models.CharField(max_length=40, blank=True)
number = models.CharField(max_length=20, blank=False, null=False, unique=True)
def __str__(self) -> str:
return self.number + f" ({self.name})"
class File(models.Model):
filename = models.CharField(max_length=200, blank=False, null=False)
original_author = models.ForeignKey(
PeerUser, on_delete=models.SET_NULL, null=True, related_name="owned_files"
)
peer_users = models.ManyToManyField(PeerUser, related_name="shared_files")
topic = models.ForeignKey(
Topic, on_delete=models.SET_NULL, null=True, related_name="files"
)
professor = models.ForeignKey(
Professor, on_delete=models.SET_NULL, null=True, related_name="files"
)
semester = models.ForeignKey(
Semester, on_delete=models.SET_NULL, null=True, related_name="files"
)
course = models.ForeignKey(
Course, on_delete=models.SET_NULL, null=True, related_name="files"
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.filename

View File

@@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import PeerUser
from .models import PeerUser, Topic, Semester, Professor, Course, File
class LoginSerializer(serializers.Serializer):
@@ -11,3 +11,39 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = PeerUser
fields = ["id", "username", "email", "ip_address"]
class TopicSerializer(serializers.ModelSerializer):
class Meta:
model = Topic
fields = "__all__"
class ProfessorSerializer(serializers.ModelSerializer):
class Meta:
model = Professor
fields = "__all__"
class SemesterSerializer(serializers.ModelSerializer):
class Meta:
model = Semester
fields = "__all__"
class CourseSerializer(serializers.ModelSerializer):
class Meta:
model = Course
fields = "__all__"
class FileSerializer(serializers.ModelSerializer):
original_author = UserSerializer()
peer_users = UserSerializer(many=True)
topic = TopicSerializer()
professor = ProfessorSerializer()
semester = SemesterSerializer()
class Meta:
model = File
fields = "__all__"

View File

@@ -16,9 +16,45 @@ Including another URLconf
"""
from django.urls import path
from api.user_login import LoginView, SignupView
from api.user_login import LoginView, SignupView, PollOnlineView
from api import views
urlpatterns = [
path("api/login/", LoginView.as_view(), name="login"),
path("api/signup/", SignupView.as_view(), name="signup"),
path("api/poll/", PollOnlineView.as_view(), name="poll"),
path("api/topics/", views.TopicListCreateAPIView.as_view(), name="topic-list"),
path(
"api/topics/<int:pk>/", views.TopicDetailAPIView.as_view(), name="topic-detail"
),
path(
"api/professors/",
views.ProfessorListCreateAPIView.as_view(),
name="professor-list",
),
path(
"api/professors/<int:pk>/",
views.ProfessorDetailAPIView.as_view(),
name="professor-detail",
),
path(
"api/semesters/",
views.SemesterListCreateAPIView.as_view(),
name="semester-list",
),
path(
"api/semesters/<int:pk>/",
views.SemesterDetailAPIView.as_view(),
name="semester-detail",
),
path("api/courses/", views.CourseListCreateAPIView.as_view(), name="course-list"),
path(
"api/courses/<int:pk>/",
views.CourseDetailAPIView.as_view(),
name="course-detail",
),
path("api/files/", views.FileListCreateAPIView.as_view(), name="file-list"),
path("api/files/<int:pk>/", views.FileDetailAPIView.as_view(), name="file-detail"),
path("api/files/filter/", views.FileFilterView.as_view(), name="file-filter-view"),
path("api/register/", views.RegisterFile.as_view(), name="file-register"),
]

View File

@@ -4,15 +4,9 @@ from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from rest_framework.views import APIView
def get_client_ip(request):
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[0]
else:
ip = request.META.get("REMOTE_ADDR")
return ip
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from api.utils.get_client_ip import get_client_ip
class LoginView(APIView):
@@ -59,3 +53,24 @@ class SignupView(APIView):
)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class PollOnlineView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
try:
user = request.user
ip_address = get_client_ip(request)
user.ip_address = ip_address
user.save()
return Response(
{
"Message": "IP address updated successfully",
"username": user.username,
},
status=status.HTTP_200_OK,
)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)

View File

View File

@@ -0,0 +1,14 @@
def get_client_ip(request):
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[0]
else:
ip = request.META.get("REMOTE_ADDR")
return ip
def update_user_ip(request):
user = request.user
ip_address = get_client_ip(request)
user.ip_address = ip_address
user.save()

View File

@@ -1,6 +1,240 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from api.serializers import LoginSerializer
from api.models import Course, File, Professor, Semester, Topic, PeerUser
from api.serializers import (
CourseSerializer,
FileSerializer,
ProfessorSerializer,
SemesterSerializer,
TopicSerializer,
)
from datetime import datetime, timedelta
from django.contrib.auth import authenticate
from django.db import transaction
from rest_framework import generics, status
from rest_framework.authentication import TokenAuthentication
from rest_framework.authtoken.models import Token
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import filters
from rest_framework.exceptions import ValidationError, NotFound
from api.utils.get_client_ip import update_user_ip
class TopicListCreateAPIView(generics.ListCreateAPIView):
queryset = Topic.objects.all()
serializer_class = TopicSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
filter_backends = [filters.SearchFilter]
search_fields = ["name"]
class TopicDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Topic.objects.all()
serializer_class = TopicSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
class ProfessorListCreateAPIView(generics.ListCreateAPIView):
queryset = Professor.objects.all()
serializer_class = ProfessorSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
filter_backends = [filters.SearchFilter]
search_fields = ["name"]
class ProfessorDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Professor.objects.all()
serializer_class = ProfessorSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
class SemesterListCreateAPIView(generics.ListCreateAPIView):
queryset = Semester.objects.all()
serializer_class = SemesterSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
filter_backends = [filters.SearchFilter]
search_fields = ["name"]
class SemesterDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Semester.objects.all()
serializer_class = SemesterSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def delete(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
class CourseListCreateAPIView(generics.ListCreateAPIView):
queryset = Course.objects.all()
serializer_class = CourseSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
filter_backends = [filters.SearchFilter]
search_fields = ["name"]
class CourseDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Course.objects.all()
serializer_class = CourseSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
class FileListCreateAPIView(generics.ListCreateAPIView):
queryset = File.objects.all()
serializer_class = FileSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
filter_backends = [filters.SearchFilter]
search_fields = ["name"]
class FileDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = File.objects.all()
serializer_class = FileSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
class RegisterFile(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
update_user_ip(request)
data = request.data
try:
with transaction.atomic():
required_fields = set(
["filename", "topic", "semester", "professor", "course"]
)
provided_fields = set(request.data.keys())
missing_fields = required_fields - provided_fields
if any(["filename", "topic"]) in missing_fields:
raise ValidationError(
"Missing one or more of the required field(s): filename, topic"
)
file = File.objects.create(
filename=data["filename"], original_author=user
)
file.peer_users.add(user)
file.course = (
Course.objects.get(id=data.get("course"))
if data.get("course")
else None
)
file.professor = (
Professor.objects.get(id=data.get("professor"))
if data.get("professor")
else None
)
file.semester = (
Semester.objects.get(id=data.get("semester"))
if data.get("semester")
else None
)
topic_id = data["topic"]
try:
topic = Topic.objects.get(id=topic_id)
file.topic = topic
except Exception as e:
topic_serializer = TopicSerializer(data=topic_id)
if topic_serializer.is_valid():
topic = topic_serializer.save()
file.topic = topic
response_data = {
"id": file.id,
"filename": file.filename,
"topic": file.topic.name if file.topic else None,
"semester": file.semester.name if file.semester else None,
"course": file.course.name if file.course else None,
"professor": file.professor.name if file.professor else None,
}
file.save()
return Response(
response_data,
status=status.HTTP_201_CREATED,
)
except Exception as e:
print("Error", str(e))
return Response(
{"error(s)": "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST
)
class FileFilterView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, format=None):
try:
# Extract filters from the query parameters
topic_id = request.query_params.get("topic")
professor_id = request.query_params.get("professor")
course_id = request.query_params.get("course")
semester_id = request.query_params.get("semester")
# Check if provided filter IDs exist
if topic_id and not Topic.objects.filter(id=topic_id).exists():
return Response(
{"error": f"Topic with id {topic_id} does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
if professor_id and not Professor.objects.filter(id=professor_id).exists():
return Response(
{"error": f"Professor with id {professor_id} does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
if course_id and not Course.objects.filter(id=course_id).exists():
return Response(
{"error": f"Course with id {course_id} does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
if semester_id and not Semester.objects.filter(id=semester_id).exists():
return Response(
{"error": f"Semester with id {semester_id} does not exist"},
status=status.HTTP_400_BAD_REQUEST,
)
# Start with the base queryset
queryset = File.objects.all()
# Apply filters if they are provided
if topic_id:
queryset = queryset.filter(topic__id=topic_id)
if professor_id:
queryset = queryset.filter(professor__id=professor_id)
if course_id:
queryset = queryset.filter(course__id=course_id)
if semester_id:
queryset = queryset.filter(semester__id=semester_id)
# Filter files based on active peers in the past hour
active_peer_ids = PeerUser.objects.filter(
last_poll__gte=datetime.now() - timedelta(hours=1)
).values_list("id", flat=True)
queryset = queryset.filter(peer_users__in=active_peer_ids).distinct()
serializer = FileSerializer(queryset, many=True)
return Response(serializer.data)
except Exception as e:
return Response(
{"error": f"Something went wrong: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

View File

View File

@@ -25,7 +25,7 @@ SECRET_KEY = "django-insecure-rh0s*u8&#4$uugtt10cxbifjriaz%@&p1w!)c2=y^undd2*nx5
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ["10.20.4.109", "73.7.29.136", "localhost"]
# Application definition

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -1,5 +1,6 @@
import "./styles/App.css";
import { Link } from 'react-router-dom';
import MainSearch from './components/MainSearch';
export default function App() {
@@ -7,6 +8,16 @@ export default function App() {
<>
<Navbar />
<h1>PeerNotes</h1>
<div className="App" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<img
src="/PeerNotes.png"
alt="PeerNotes Logo"
style={{
height: "100px"
}}
/>
<MainSearch />
</div>
</>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -0,0 +1,39 @@
import React from 'react';
import '../screens/MainScreenWrapper.css';
const MainSearch: React.FC = () => {
return (
<div className="filter-container">
<div className="filter-item">
<label htmlFor="professors">Professors:</label>
<select id="professors">
<option value="">Select a Professor</option>
{/* temporary */}
<option value="professor1">Professor 1</option>
<option value="professor2">Professor 2</option>
</select>
</div>
<div className="filter-item">
<label htmlFor="courses">Course:</label>
<select id="courses">
<option value="">Select a Course</option>
{/* temporary */}
<option value="course1">Course 1</option>
<option value="course2">Course 2</option>
</select>
</div>
<div className="filter-item">
<label htmlFor="topics">Topic:</label>
<select id="topics">
<option value="">Select a Topic</option>
{/* temporary */}
<option value="topic1">Topic 1</option>
<option value="topic2">Topic 2</option>
</select>
</div>
<button type="submit" className="submit-button">Submit</button>
</div>
);
}
export default MainSearch;

View File

@@ -0,0 +1,52 @@
html, body {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
background: #0C2D48;
font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif;
}
.filter-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background: #B1D4E0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); /* shadow */
}
.filter-item + .filter-item {
margin-top: 20px; /* space between filter items */
}
label {
margin-bottom: 5px;
font-weight: bold;
}
select {
padding: 8px;
border: 1px solid #2E8BC0;
border-radius: 4px;
width: 100%; /* fill the container width */
}
.submit-button {
padding: 8px 16px;
margin-top: 20px; /* Adds space above the button */
background-color: #2E8BC0; /* Example background color */
color: white; /* Text color */
border: none;
border-radius: 4px;
cursor: pointer;
font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif;
}
.submit-button:hover {
background-color: #1D6A96; /* Darker shade for hover effect */
}