Skip to content

Commit

Permalink
Merge pull request #1040 from jhuopensource/feature/error-handling
Browse files Browse the repository at this point in the history
Error fallback and report
  • Loading branch information
JiaqiWang18 authored Oct 10, 2023
2 parents ff27ea8 + 5e55563 commit 54d40e4
Show file tree
Hide file tree
Showing 17 changed files with 263 additions and 18 deletions.
8 changes: 8 additions & 0 deletions analytics/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,13 @@
# GNU General Public License for more details.

from django.contrib import admin
from .models import UIErrorLog

# Register your models here.


@admin.register(UIErrorLog)
class UIErrorLogAdmin(admin.ModelAdmin):
"""Enables admins to create, view, and edit news updates at /admin"""

list_display = ("name", "message", "user", "time_occurred")
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 3.2.20 on 2023-10-07 01:34

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


class Migration(migrations.Migration):

replaces = [
("analytics", "0022_uierrorlog"),
("analytics", "0023_alter_uierrorlog_user"),
("analytics", "0024_alter_uierrorlog_options"),
]

dependencies = [
("analytics", "0021_auto_20230203_1230"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("student", "0046_delete_registrationtoken"),
]

operations = [
migrations.CreateModel(
name="UIErrorLog",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("time_occurred", models.DateTimeField(auto_now_add=True)),
("message", models.CharField(max_length=1000)),
("name", models.CharField(max_length=100)),
("stack", models.TextField()),
("componentStack", models.TextField()),
(
"user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-time_occurred"],
"abstract": False,
},
),
]
30 changes: 30 additions & 0 deletions analytics/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from timetable.models import *
from student.models import Student
from django.contrib.auth.models import User


class SharedTimetable(Timetable):
Expand Down Expand Up @@ -112,3 +113,32 @@ class CalendarExport(models.Model):
time_created = models.DateTimeField(auto_now_add=True)
school = models.CharField(max_length=50)
is_google_calendar = models.BooleanField(blank=True, default=False)


class ErrorLog(models.Model):
"""
Logs errors that occur on the site.
"""

time_occurred = models.DateTimeField(auto_now_add=True)
message = models.CharField(max_length=1000)
name = models.CharField(max_length=100)
stack = models.TextField()
user = models.ForeignKey(
User, blank=True, null=True, on_delete=models.deletion.CASCADE
)

class Meta:
abstract = True
ordering = ["-time_occurred"]

def __str__(self) -> str:
return f"{self.name}: {self.message}"


class UIErrorLog(ErrorLog):
"""
UI errors that occur in the React components
"""

componentStack = models.TextField()
21 changes: 21 additions & 0 deletions analytics/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from rest_framework import serializers
from .models import UIErrorLog


class CurrentUserDefault(object):
def set_context(self, serializer_field):
self.user = serializer_field.context["request"].user

def __call__(self):
return self.user if self.user.is_authenticated else None

def __repr__(self):
return "%s()" % self.__class__.__name__


class UIErrorLogSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(default=CurrentUserDefault())

class Meta:
model = UIErrorLog
fields = "__all__"
43 changes: 43 additions & 0 deletions analytics/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

from django.contrib.auth.models import User
from rest_framework.test import APITestCase
from helpers.test.test_cases import UrlTestCase
from .models import UIErrorLog


class UrlsTest(UrlTestCase):
Expand All @@ -24,3 +27,43 @@ def test_urls_call_correct_views(self):
"/robots.txt", "analytics.views.view_analytics_dashboard"
)
self.assertUrlResolvesToView("/user/log_ical/", "student.views.log_ical_export")
self.assertUrlResolvesToView(
"/ui-error-logs/", "analytics.views.UIErrorLogCreateView"
)


class UIErrorLogCreateViewTest(APITestCase):
def setUp(self):
self.request_data = {
"name": "test name",
"message": "test message",
"stack": "test stack",
"componentStack": "test componentStack",
}
self.user = User.objects.create_user(
username="user", email="[email protected]", password="password"
)

def test_add_new_log(self):
response = self.client.post("/ui-error-logs/", self.request_data, format="json")
self.assertEqual(response.status_code, 201)

self.assertEqual(1, len(UIErrorLog.objects.all()))
created = UIErrorLog.objects.all()[0]
self.assert_attributes(created, None)

def test_add_new_log_logged_in(self):
self.client.force_login(self.user)
response = self.client.post("/ui-error-logs/", self.request_data, format="json")
self.assertEqual(response.status_code, 201)

self.assertEqual(1, len(UIErrorLog.objects.all()))
created = UIErrorLog.objects.all()[0]
self.assert_attributes(created, self.user)

def assert_attributes(self, created, user):
self.assertEqual(created.name, self.request_data["name"])
self.assertEqual(created.message, self.request_data["message"])
self.assertEqual(created.stack, self.request_data["stack"])
self.assertEqual(created.componentStack, self.request_data["componentStack"])
self.assertEqual(created.user, user)
1 change: 1 addition & 0 deletions analytics/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@
re_path(r"^analytics/*$", analytics.views.view_analytics_dashboard),
re_path(r"^robots.txt*$", analytics.views.view_analytics_dashboard),
re_path(r"^user/log_ical/*$", student.views.log_ical_export),
re_path(r"^ui-error-logs/*$", analytics.views.UIErrorLogCreateView.as_view()),
]
9 changes: 8 additions & 1 deletion analytics/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@
from django.views.decorators.csrf import csrf_exempt
from django.http import Http404
from django.db.models import Count

from rest_framework import generics
from rest_framework.response import Response
from student.models import Student

from student.utils import get_student
from student.models import *
from analytics.models import *
from analytics.serializers import UIErrorLogSerializer
from timetable.models import Semester
from parsing.schools.active import ACTIVE_SCHOOLS

Expand Down Expand Up @@ -240,3 +242,8 @@ def number_students_by_school():
)
result[school] = students.count()
return result


class UIErrorLogCreateView(generics.CreateAPIView):
serializer_class = UIErrorLogSerializer
queryset = UIErrorLog.objects.all()
32 changes: 20 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"react-dnd": "^14.0.4",
"react-dnd-html5-backend": "^14.0.2",
"react-dom": "^16.14.0",
"react-error-boundary": "^3.1.4",
"react-hot-loader": "^1.3.1",
"react-infinite-scroll-component": "^6.1.0",
"react-input-autosize": "^1.2.0",
Expand Down
1 change: 1 addition & 0 deletions static/css/timetable/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@
@import "partials/news_modal.scss";
@import "partials/user_acquisition_modal";
@import "partials/user_settings_modal";
@import "partials/fallback";
25 changes: 25 additions & 0 deletions static/css/timetable/partials/fallback.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.fallback-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;

h2,
p {
color: #000;
}

img {
width: 100%;
max-width: 350px;
}
}

@media (max-width: 768px) {
.fallback-container {
img {
max-width: 200px;
}
}
}
Binary file added static/img/sob.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions static/js/redux/constants/endpoints.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ export function getCourseShareLink(code, semester) {
}

export const getNewsEndpoint = () => "/notifications/news";
export const getUIErrorLogEndpoint = () => "/ui-error-logs/";
4 changes: 2 additions & 2 deletions static/js/redux/init.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { render } from "react-dom";
import { Provider } from "react-redux";
import { HTML5Backend } from "react-dnd-html5-backend";
import { DndProvider } from "react-dnd";
import Semesterly from "./ui/Semesterly";
import SemesterlyWithErrorBoundary from "./ui/Semesterly";
import {
fetchMostClassmatesCount,
handleAgreement,
Expand Down Expand Up @@ -126,7 +126,7 @@ store.dispatch(setup());
render(
<Provider store={store}>
<DndProvider backend={HTML5Backend}>
<Semesterly />
<SemesterlyWithErrorBoundary />
</DndProvider>
</Provider>,
document.getElementsByClassName("page")[0]
Expand Down
Loading

0 comments on commit 54d40e4

Please sign in to comment.