Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed issue 491 #500

Closed
wants to merge 16 commits into from
20 changes: 0 additions & 20 deletions Pipfile.lock

This file was deleted.

6 changes: 6 additions & 0 deletions backend/PennCourses/settings/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE
INTERNAL_IPS = ["127.0.0.1"]

DATABASES = {
"default": dj_database_url.config(
default="sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3"), conn_max_age=20
)
}

CSRF_TRUSTED_ORIGINS = ["http://localhost:3000"]

CACHES = {
Expand Down
3 changes: 3 additions & 0 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ scikit-learn = "*"
pandas = "*"
python-dateutil = "*"
docutils = "*"
ics = "*"
psycopg2-binary = "*"
drf-nested-routers = "*"

[requires]
python_full_version = "3.10"
966 changes: 723 additions & 243 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Welcome to the Penn Courses Backend (PCX)!

### Prerequisites

- Python 3.10 ([`pyenv`](https://github.com/pyenv/pyenv) is recommended)
- [`pipenv`](https://pipenv.pypa.io/en/latest/)
- [`docker` and `docker-compose`](https://docs.docker.com/get-docker/)
- Python 3.10 ([`pyenv`](https://github.com/pyenv/pyenv) is recommended)
- [`pipenv`](https://pipenv.pypa.io/en/latest/)
- [`docker` and `docker-compose`](https://docs.docker.com/get-docker/)
Expand All @@ -30,8 +33,8 @@ NOTE: when using `pipenv`, environment variables are only refreshed when you exi
- `echo 'export PATH="/usr/local/opt/openssl@3/bin:$PATH"' >> ~/.zshrc`
- `export LDFLAGS="-L/usr/local/opt/openssl@3/lib"`
- `export CPPFLAGS="-I/usr/local/opt/openssl@3/include"`
- **Windows/Linux**
1. `apt-get install gcc python3-dev libpq-dev`
- Windows (WSL) or Linux:
- `apt-get install gcc python3-dev libpq-dev`

3. Running Docker
1. Open a new terminal window (also in the `backend` directory) and run `docker-compose up`
Expand Down Expand Up @@ -66,6 +69,7 @@ NOTE: when using `pipenv`, environment variables are only refreshed when you exi
- With the backend server running, you can also run the frontend for any of our PCX products by following the instructions in the `frontend` README.
- Note: If you have not loaded the test data from the previous step (Step 4), ensure that you have created a local user named "Penn-Courses" with the password "postgres" in your PostgreSQL. To add the user, navigate to your pgAdmin, and follow the path of Object -> Create -> Login/Group Role and create the appropriate user.

7. Running tests
7. Running tests
- Run `python manage.py test` to run our test suite.
- To run a specific test, you can use the format `python manage.py test tests.review.test_api.OneReviewTestCase.test_course` (also note that in this example, you can use any prefix of that path to run a larger set of tests).
Expand All @@ -77,7 +81,7 @@ NOTE: when using `pipenv`, environment variables are only refreshed when you exi
- This will spin up a container from which you can run the server (with all required packages preinstalled).
3. In a separate terminal (from any directory), run `docker exec -it backend-development-1 /bin/bash` to open a shell in the container (if this says "no such container", run `docker container ls` and use the name of whatever container most closely matches the `backend_development` image). Just like exiting a Pipenv shell, you can exit the container by pressing `Ctrl+D` (which sends the "end of transmission" / EOF character).
- You might want to add an alias for this command so it is easier to run (e.g. `echo 'alias courses-backend="docker exec -it backend_development_1 /bin/bash"' >> ~/.zshrc && source ~/.zshrc`). Then you can just run `courses-backend` from any directory to connect to the Docker container from which you will run the server (assuming `courses-compose` is already running in another terminal).
4. Once you have a shell open in the container, you can continue running the rest of the commands in the [Setting Up the Backend](#setting-up-the-backend) section of this README (except you can skip `pipenv install --dev` since that has already been done for you).
4. Once you have a shell open in the container, you can continue running the rest of the commands in this README (except you can skip `pipenv install --dev` since that has already been done for you).
1. Remember to run `pipenv shell` (to open a [Pipenv] shell inside of a [docker container] shell inside of your computer's shell!). Note that the `/backend` directory inside the container is automatically synced with the `backend` directory on your host machine (from which you ran `docker-compose --profile=dev up`).
5. There's just one last complication. Due to some annoying details of Docker networking, you have to expose the server on IP address `0.0.0.0` inside the container, rather than `127.0.0.1` or `localhost` as is default (otherwise the server won't be accessible from outside of the container). To do this, instead of running `python manage.py runserver`, run `python manage.py runserver 0.0.0.0:8000`. In `Dockerfile.dev`, we automatically alias the command `runserver` to the latter, so in the container shell (in `/backend`, as is default) you can simply run the command `runserver` (no `python manage.py` necessary).

Expand Down
8 changes: 4 additions & 4 deletions backend/alert/management/commands/recomputestats.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ def recompute_has_reviews():
with connection.cursor() as cursor:
cursor.execute(
"""
UPDATE "courses_section" U0
UPDATE "courses_section" AS U0
SET "has_reviews" = CASE WHEN
EXISTS (SELECT id FROM "review_review" U1
EXISTS (SELECT id FROM "review_review" AS U1
WHERE U0."id" = U1."section_id")
THEN true ELSE false
END
Expand All @@ -67,9 +67,9 @@ def recompute_has_status_updates():
with connection.cursor() as cursor:
cursor.execute(
"""
UPDATE "courses_section" U0
UPDATE "courses_section" AS U0
SET "has_status_updates" = CASE WHEN
EXISTS (SELECT id FROM "courses_statusupdate" U1
EXISTS (SELECT id FROM "courses_statusupdate" AS U1
WHERE U0."id" = U1."section_id")
THEN true ELSE false
END
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.1.5 on 2023-01-15 16:39

import uuid

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("courses", "0053_alter_ngssrestriction_code"),
]

operations = [
migrations.AddField(
model_name="userprofile",
name="uuid_secret",
field=models.UUIDField(default=uuid.uuid4),
),
migrations.AlterField(
model_name="ngssrestriction",
name="inclusive",
field=models.BooleanField(
help_text='\nWhether this is an include or exclude restriction. Corresponds to the `incl_excl_ind`\nresponse field. `True` if include (ie, `incl_excl_ind` is "I") and `False`\nif exclude ("E").\n'
),
),
]
1 change: 1 addition & 0 deletions backend/courses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,7 @@ class UserProfile(models.Model):
email = models.EmailField(
blank=True, null=True, help_text="The email of the User. Defaults to null."
)
uuid_secret = models.UUIDField(default=uuid.uuid4)
push_notifications = models.BooleanField(
default=False,
help_text=dedent(
Expand Down
6 changes: 3 additions & 3 deletions backend/plan/urls.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from django.urls import include, path
from django.views.generic import TemplateView
from rest_framework import routers
from rest_framework_nested import routers

from plan.views import ScheduleViewSet, recommend_courses_view
from plan.views import CalendarAPIView, ScheduleViewSet, recommend_courses_view


router = routers.DefaultRouter()
router.register(r"schedules", ScheduleViewSet, basename="schedules")


urlpatterns = [
path("<int:schedule_pk>/calendar/", CalendarAPIView.as_view(), name="calendar-view"),
path("", TemplateView.as_view(template_name="plan/build/index.html")),
path("recommendations/", recommend_courses_view, name="recommend-courses"),
path("", include(router.urls)),
Expand Down
107 changes: 106 additions & 1 deletion backend/plan/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import arrow
from django.core.exceptions import ObjectDoesNotExist
from django.db import IntegrityError
from django.db.models import Prefetch, Q
from django.http import HttpResponse
from django.utils import timezone
from django_auto_prefetching import AutoPrefetchViewSetMixin
from ics import Calendar as ICSCal
from ics import Event as ICSEvent
from ics.grammar.parse import ContentLine
from rest_framework import status, viewsets
from rest_framework.decorators import api_view, permission_classes, schema
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from courses.models import Course, Section
from courses.models import Course, Meeting, Section
from courses.serializers import CourseListSerializer
from courses.util import get_course_and_section, get_current_semester
from PennCourses.docs_settings import PcxAutoSchema, reverse_func
Expand Down Expand Up @@ -399,3 +406,101 @@ def get_queryset(self):
"sections__meetings__room",
)
return queryset


class CalendarAPIView(APIView):
schema = PcxAutoSchema(
response_codes={
reverse_func("calendar-view", args=["schedule_pk"]): {
"GET": {
200: "Schedule exported successfully",
},
},
},
)

def get(self, *args, **kwargs):
"""
Return a .ics file of the user's selected schedule
---
responses:
"200":
description: Return a calendar file in ICS format.
content:
text/calendar:
schema:
type: string
---
"""
schedule_pk = kwargs["schedule_pk"]

schedule = (
Schedule.objects.filter(pk=schedule_pk)
.prefetch_related("sections", "sections__meetings")
.first()
)

if not schedule:
return Response({"detail": "Invalid schedule"}, status=status.HTTP_403_FORBIDDEN)

day_mapping = {"M": "MO", "T": "TU", "W": "WE", "R": "TH", "F": "FR"}

calendar = ICSCal(creator="Penn Labs")
calendar.extra.append(ContentLine(name="X-WR-CALNAME", value=f"{schedule.name} Schedule"))

for section in schedule.sections.all():
e = ICSEvent()
e.name = section.full_code
e.created = timezone.now()

days = []
for meeting in section.meetings.all():
days.append(day_mapping[meeting.day])
first_meeting = list(section.meetings.all())[0]

start_time = str(Meeting.int_to_time(first_meeting.start))
end_time = str(Meeting.int_to_time(first_meeting.end))

if not start_time:
start_time = ""
if not end_time:
end_time = ""

if first_meeting.start_date is None:
start_datetime = ""
end_datetime = ""
else:
start_datetime = first_meeting.start_date + " "
end_datetime = first_meeting.start_date + " "

if int(first_meeting.start) < 10:
start_datetime += "0"
if int(first_meeting.end) < 10:
end_datetime += "0"

start_datetime += start_time
end_datetime += end_time

e.begin = arrow.get(
start_datetime, "YYYY-MM-DD HH:mm A", tzinfo="America/New York"
).format("YYYYMMDDTHHmmss")
e.end = arrow.get(end_datetime, "YYYY-MM-DD HH:mm A", tzinfo="America/New York").format(
"YYYYMMDDTHHmmss"
)
end_date = arrow.get(
first_meeting.end_date, "YYYY-MM-DD", tzinfo="America/New York"
).format("YYYYMMDDTHHmmss")

e.extra.append(
ContentLine(
"RRULE",
{},
f'FREQ=WEEKLY;UNTIL={end_date}Z;WKST=SU;BYDAY={",".join(days)}',
)
)

calendar.events.add(e)

response = HttpResponse(calendar, content_type="text/calendar")
response["Content-Disposition"] = "attachment; pcp-schedule.ics"
return response
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@
"hooks": {
"pre-commit": "lerna run --concurrency 1 precommit"
}
},
"dependencies": {
}
}
6 changes: 6 additions & 0 deletions frontend/plan/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const REMOVE_SCHED_ITEM = "REMOVE_SCHED_ITEM";
export const DELETE_SCHEDULE = "DELETE_SCHEDULE";
export const RENAME_SCHEDULE = "RENAME_SCHEDULE";
export const DUPLICATE_SCHEDULE = "DUPLICATE_SCHEDULE";
export const DOWNLOAD_SCHEDULE = "DOWNLOAD_SCHEDULE";
export const CLEAR_SCHEDULE = "CLEAR_SCHEDULE";
export const ENFORCE_SEMESTER = "ENFORCE_SEMESTER";
export const CLEAR_ALL_SCHEDULE_DATA = "CLEAR_ALL_SCHEDULE_DATA";
Expand Down Expand Up @@ -82,6 +83,11 @@ export const duplicateSchedule = (scheduleName) => ({
scheduleName,
});

export const downloadSchedule = (scheduleName) => ({
type: DOWNLOAD_SCHEDULE,
scheduleName,
});

export const deleteSchedule = (scheduleName) => ({
type: DELETE_SCHEDULE,
scheduleName,
Expand Down
Loading
Loading