Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
question_files
4 changes: 2 additions & 2 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ verify_ssl = true

[dev-packages]
codecov = "*"
black = "==19.10b0"
black = "==22.3.0"
unittest-xml-reporting = "*"
flake8 = "*"
flake8-absolute-import = "*"
Expand All @@ -22,7 +22,7 @@ djangorestframework = "*"
psycopg2 = "*"
sentry-sdk = "*"
django = "==3.1.7"
django-cors-headers = "*"
django-cors-headers = "==3.11.0"
pyyaml = "*"
uritemplate = "*"
uwsgi = "*"
Expand Down
813 changes: 351 additions & 462 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

33 changes: 22 additions & 11 deletions backend/ohq/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,32 @@
MembershipInvite,
Profile,
Question,
QuestionFile,
Queue,
QueueStatistic,
Semester,
Tag,
)


admin.site.register(Course)
admin.site.register(CourseStatistic)
admin.site.register(Membership)
admin.site.register(MembershipInvite)
admin.site.register(Profile)
admin.site.register(Question)
admin.site.register(Queue)
admin.site.register(Semester)
admin.site.register(QueueStatistic)
admin.site.register(Announcement)
admin.site.register(Tag)
class DisplayIdAdmin(admin.ModelAdmin):
readonly_fields = ("id",)

def get_list_display(self, request):
list_display = list(super().get_list_display(request))
list_display.insert(0, "id")
return list_display


admin.site.register(Course, DisplayIdAdmin)
admin.site.register(CourseStatistic, DisplayIdAdmin)
admin.site.register(Membership, DisplayIdAdmin)
admin.site.register(MembershipInvite, DisplayIdAdmin)
admin.site.register(Profile, DisplayIdAdmin)
admin.site.register(Question, DisplayIdAdmin)
admin.site.register(Queue, DisplayIdAdmin)
admin.site.register(Semester, DisplayIdAdmin)
admin.site.register(QueueStatistic, DisplayIdAdmin)
admin.site.register(Announcement, DisplayIdAdmin)
admin.site.register(Tag, DisplayIdAdmin)
admin.site.register(QuestionFile, DisplayIdAdmin)
5 changes: 5 additions & 0 deletions backend/ohq/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django import forms


class UploadFileForm(forms.Form):
file = forms.FileField(widget=forms.ClearableFileInput(attrs={"multiple": True}))
4 changes: 3 additions & 1 deletion backend/ohq/management/commands/createcourse.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ def add_arguments(self, parser):
parser.add_argument("year", type=int)
parser.add_argument("--emails", nargs="+", type=str)
parser.add_argument(
"--roles", nargs="+", choices=[Membership.KIND_PROFESSOR, Membership.KIND_HEAD_TA],
"--roles",
nargs="+",
choices=[Membership.KIND_PROFESSOR, Membership.KIND_HEAD_TA],
)

def handle(self, *args, **kwargs):
Expand Down
39 changes: 31 additions & 8 deletions backend/ohq/migrations/0002_auto_20200816_1727.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,38 @@ class Migration(migrations.Migration):

operations = [
migrations.RenameField(
model_name="question", old_name="time_answered", new_name="time_responded_to",
model_name="question",
old_name="time_answered",
new_name="time_responded_to",
),
migrations.RemoveField(
model_name="question",
name="answered_by",
),
migrations.RemoveField(
model_name="question",
name="rejected_by",
),
migrations.RemoveField(
model_name="question",
name="rejected_reason_other",
),
migrations.RemoveField(
model_name="question",
name="time_last_updated",
),
migrations.RemoveField(
model_name="question",
name="time_rejected",
),
migrations.RemoveField(
model_name="question",
name="time_started",
),
migrations.RemoveField(
model_name="question",
name="time_withdrawn",
),
migrations.RemoveField(model_name="question", name="answered_by",),
migrations.RemoveField(model_name="question", name="rejected_by",),
migrations.RemoveField(model_name="question", name="rejected_reason_other",),
migrations.RemoveField(model_name="question", name="time_last_updated",),
migrations.RemoveField(model_name="question", name="time_rejected",),
migrations.RemoveField(model_name="question", name="time_started",),
migrations.RemoveField(model_name="question", name="time_withdrawn",),
migrations.AddField(
model_name="question",
name="responded_to_by",
Expand Down
4 changes: 3 additions & 1 deletion backend/ohq/migrations/0004_auto_20200825_1344.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class Migration(migrations.Migration):

operations = [
migrations.AlterField(
model_name="queue", name="estimated_wait_time", field=models.IntegerField(default=-1),
model_name="queue",
name="estimated_wait_time",
field=models.IntegerField(default=-1),
),
]
4 changes: 3 additions & 1 deletion backend/ohq/migrations/0005_auto_20201016_1702.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class Migration(migrations.Migration):
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name="question", name="resolved_note", field=models.BooleanField(default=True),
model_name="question",
name="resolved_note",
field=models.BooleanField(default=True),
),
]
4 changes: 3 additions & 1 deletion backend/ohq/migrations/0008_auto_20210119_2218.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ class Migration(migrations.Migration):

operations = [
migrations.AddField(
model_name="queue", name="rate_limit_enabled", field=models.BooleanField(default=False),
model_name="queue",
name="rate_limit_enabled",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="queue",
Expand Down
6 changes: 5 additions & 1 deletion backend/ohq/migrations/0010_auto_20210407_0145.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ class Migration(migrations.Migration):
]

operations = [
migrations.AlterField(model_name="announcement", name="content", field=models.TextField(),),
migrations.AlterField(
model_name="announcement",
name="content",
field=models.TextField(),
),
]
10 changes: 8 additions & 2 deletions backend/ohq/migrations/0013_auto_20210924_2056.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ class Migration(migrations.Migration):
]

operations = [
migrations.RemoveField(model_name="course", name="require_video_chat_url_on_questions",),
migrations.RemoveField(model_name="course", name="video_chat_enabled",),
migrations.RemoveField(
model_name="course",
name="require_video_chat_url_on_questions",
),
migrations.RemoveField(
model_name="course",
name="video_chat_enabled",
),
]
4 changes: 3 additions & 1 deletion backend/ohq/migrations/0016_auto_20211008_2136.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ class Migration(migrations.Migration):

operations = [
migrations.AddField(
model_name="queue", name="pin_enabled", field=models.BooleanField(default=False),
model_name="queue",
name="pin_enabled",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="queue",
Expand Down
4 changes: 3 additions & 1 deletion backend/ohq/migrations/0018_auto_20220125_0344.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class Migration(migrations.Migration):

operations = [
migrations.AlterField(
model_name="course", name="course_title", field=models.CharField(max_length=100),
model_name="course",
name="course_title",
field=models.CharField(max_length=100),
),
]
32 changes: 32 additions & 0 deletions backend/ohq/migrations/0020_questionfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 3.1.7 on 2022-04-17 17:53

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("ohq", "0019_auto_20211114_1800"),
]

operations = [
migrations.CreateModel(
name="QuestionFile",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("file", models.FileField(upload_to="question_files/")),
(
"question",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="ohq.question"
),
),
],
),
]
19 changes: 19 additions & 0 deletions backend/ohq/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
Expand Down Expand Up @@ -411,3 +413,20 @@ class Announcement(models.Model):
author = models.ForeignKey(User, related_name="announcements", on_delete=models.CASCADE)
time_updated = models.DateTimeField(auto_now=True)
course = models.ForeignKey(Course, related_name="announcements", on_delete=models.CASCADE)


class QuestionFile(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
file = models.FileField(upload_to="question_files/")


# Delete file on delete of QuestionFile object
@receiver(models.signals.post_delete, sender=QuestionFile)
def auto_delete_file_on_delete(sender, instance, **kwargs):
"""
Deletes file from filesystem
when corresponding `QuestionFile` object is deleted.
"""
if instance.file:
if os.path.isfile(instance.file.path):
os.remove(instance.file.path)
28 changes: 25 additions & 3 deletions backend/ohq/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from rest_framework import permissions
from schedule.models import Event, EventRelation, Occurrence

from ohq.models import Course, Membership, Question
from ohq.models import Course, Membership, Question, QuestionFile


# Hierarchy of permissions is usually:
Expand Down Expand Up @@ -130,12 +130,30 @@ class QuestionPermission(permissions.BasePermission):

def has_object_permission(self, request, view, obj):
membership = Membership.objects.get(course=view.kwargs["course_pk"], user=request.user)

# Students can get or modify their own question
# TAs+ can get or modify any questions
if view.action in ["retrieve", "update", "partial_update", "position"]:
if view.action in [
"retrieve",
"update",
"partial_update",
"position",
"upload_file",
"delete_all_file",
]:
return obj.asked_by == request.user or membership.is_ta

if view.action in ["delete_file"]:
if not (obj.asked_by == request.user or membership.is_ta):
return False

# all file ids must correspond
attempted_ids = request.GET.getlist("ids")
available_ids = QuestionFile.objects.filter(question=obj).values_list("id", flat=True)
for attempt in attempted_ids:
if attempt not in available_ids:
return False
return True

def has_permission(self, request, view):
# Anonymous users can't do anything
if not request.user.is_authenticated:
Expand Down Expand Up @@ -167,6 +185,10 @@ def has_permission(self, request, view):

return membership.kind == Membership.KIND_STUDENT and existing_question is None

if view.action in ["upload_file", "delete_file", "delete_all_file"]:
question = Question.objects.get(pk=view.kwargs["pk"])
return self.has_object_permission(request, view, question)

# Students+ can get, list, or modify questions
# With restrictions defined in has_object_permission
return True
Expand Down
34 changes: 32 additions & 2 deletions backend/ohq/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,36 @@ def get_operation(self, path, method):
return operation


class QuestionSchema(AutoSchema):
def get_operation(self, path, method):
op = super().get_operation(path, method)
if op["operationId"] == "uploadFileQuestion":
op["requestBody"] = {
"content": {
"multipart/form-data": {
"schema": {
"properties": {
"file": {"type": "array of files"},
}
}
}
}
}
elif op["operationId"] == "deleteFileQuestion":
op["parameters"].append(
{
"name": "id",
"in": "query",
"required": False,
"description": "A series of ids of files to be deleted <br>"
+ "e.g. /deleteFile/?id=1&id=2 "
+ "- where the numbers are the file pks",
"schema": {"type": "string"},
}
)
return op


class EventSchema(AutoSchema):
def get_operation(self, path, method):
op = super().get_operation(path, method)
Expand All @@ -47,7 +77,7 @@ def get_operation(self, path, method):
"name": "course",
"in": "query",
"required": True,
"description": "A series of api/events/?course=1&course=2 "
"description": "A series of /events/?course=1&course=2 "
+ "- where the numbers are the course pks",
"schema": {"type": "string"},
}
Expand All @@ -64,7 +94,7 @@ def get_operation(self, path, method):
"name": "course",
"in": "query",
"required": True,
"description": "A series of api/occurrences/?course=1&course=2 "
"description": "A series of occurrences/?course=1&course=2 "
+ "- where the numbers are the course pks",
"schema": {"type": "string"},
}
Expand Down
Loading