Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ node_modules/

# Firebase credentials
ohq-firebase-*.json

# Env variables
.env
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
question_files
6 changes: 4 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 All @@ -44,6 +44,8 @@ gunicorn = "*"
django-scheduler = "*"
typing-extensions = "*"
drf-excel = "*"
boto3 = "*"
django-storages = "*"

[requires]
python_version = "3"
1,736 changes: 850 additions & 886 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions backend/officehoursqueue/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

import dj_database_url

from dotenv import load_dotenv

load_dotenv()

DOMAINS = os.environ.get("DOMAINS", "example.com").split(",")

Expand Down Expand Up @@ -185,3 +188,10 @@
# Default to in-memory Channel Layer for dev and CI.

CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}


# Upload file storage
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID', None)
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY', None)
AWS_STORAGE_BUCKET_NAME = "ohq.question_files"
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
Loading