Assignments > Project 1: UNCA Course Lookup
Due on Thu, 11/14 @ 11:59PM. 20 Points.
How do I rebase main against my current working branch?
- Stage and commit all of the changes on the branch you’re currently on.
- Checkout main and pull down changes:
git checkout main git pull
- Check out your working branch
git checkout <your-feature-branch>
- Rebase:
git rebase main
- If there are conflicts:
- Manually resolve them in VS code
- Then stage the changes you just made:
git add .
- Then continue the rebase process:
git rebase --continue
- And when you’re done, modify latest commit message (optional).
- Verify that nothing broke by:
- run the tests
- run the app
- run the linter (and formatter if needed)
- Force push: because we rewrote history, we need to use the force flag:
git push --force
Introduction
For your first project, you are going to create a command line tool to replicate aspects of the UNCA Course Search app. A subset of the app’s functionality is shown in this video (please watch).
Learning Goals
This project will help you practice the concepts we have been covering so far, including:
- Breaking requirements up into logical units that can be worked on in parallel (i.e., minimizing the dependencies between components).
- Thinking about which “jobs” each component will have.
- Working with a common development environment usind Docker (e.g., operating system, python versions, coding conventions, testing suite) so that the app runs identically on any machine.
- Working with static analysis tools.
- Writing tests.
- Devising a workflow for integrating work.
- Coordinating and communicating across tasks to make sure everything can be successfully integrated.
Product Requirements
You and your team will create an app that implements the following features:
- A way to download the UNCA data file from the Internet (JSON)
- A way to display the search options.
- A way to allow the user to specify the search options
- A way to store the user’s preferences (for any of the options)
- A way to filter the courses according to the user’s preferences
- A way to display the courses that match a user’s preferences
- A way to add, remove, and view courses from a user’s schedule
- A way to download the schedule as a CSV file
- A way to email one’s schedule to someone (using Twilio’s SendGrid API)
System Architecture (Done as a Class)
During class on Tuesday (9/24), we discussed how to organize our system, and we collectively created the following diagram:
Team Assignments
Your teams are listed below. Each member of your team has been assigned a task from the GitHub Issue Tracker. Some of you will be working on the same Python class, so so make sure you only work on the tasks to which you were assigned:
Team 1
Please see your team’s repo and issue tracker for more information. Members of Team 1 are:
1. | Jacob (jforres1) | Implement the Course class |
2. | Anthony B. (anthonyablazer) | Implement the Courses class |
3. | Brian (bmungal) | Implement some of the UserPreferences class (Part 1) |
4. | Josie (jandersoj) | Implement some of the UserPreferences class (Part 2) |
5. | Jasmine (jnarehoo) | Implement the CourseFilter class |
6. | Ruby (runuss720) | Implement the UI functions (ui.py) |
7. | Kate (kgary432) | Implement the Schedule class (except for save_schedule and send_email ) |
8. | Zejun (Hayden514) | Implement the save_schedule method of the Schedule class |
9. | Sergio (sbaguena10) | Implement the send_email method of the Schedule class |
Team 2
Please see your team’s repo and issue tracker for more information. Members of Team 2 are:
1. | Travis (tgroom90) | Implement the Course class |
2. | Isreal (isrealmcd) | Implement the Courses class |
3. | Eli (eutterba) | Implement some of the UserPreferences class (Part 1) |
4. | Zachary (zRaNk1995) | Implement some of the UserPreferences class (Part 2) |
5. | Christian (Csluder2) | Implement the CourseFilter class |
6. | Tai (tairobinett) | Implement the UI functions (ui.py) |
7. | Andrew (awillis4) | Implement the Schedule class (except for save_schedule and send_email ) |
8. | Anthony I. (Aindelic) | Implement the save_schedule method of the Schedule class |
9. | Brendan (ringojuic) | Implement the send_email method of the Schedule class |
Tasks
Each task is listed below. Each student on your team has been assigned a task via the GitHub Issue Tracker, and will be completing some subset of the functionality described below:
1. Course
The job of the Course class (course.py
) is to parse the incoming dictionary into a more useable form. If you have been assigned this issue, please note that the rest of the app is highly dependent on this class, so the earlier you complete this task, the easier it will be for your teammates.
Sample dictionary item to be parsed
{
"CRN": 60002,
"Code": "ACCT 215.001",
"Department": "ACCT",
"Title": "Principles of Accounting I",
"Instructors": [{"Username": "chughes", "Name": "Hughes, Carolyn"}],
"Hours": 4,
"Days": "MW",
"StartTime": "2019-05-07T21:30:00Z",
"EndTime": "2019-05-07T23:10:00Z",
"Location": {"FullLocation": "DEL 110", "Building": "DEL", "Room": "110"},
"EnrollmentCurrent": 26,
"EnrollmentMax": 28,
"WaitlistMax": 0,
"WaitlistAvailable": 0,
"TermPart": "1",
"StartDate": "2024-08-19T04:00:00Z",
"EndDate": "2024-12-11T05:00:00Z",
"Classification": {
"DiversityIntensive": False,
"DiversityIntensiveR": False,
"DistanceLearning": False,
"FirstYearSeminar": False,
"Graduate": False,
"Honors": False,
"Arts": False,
"ServiceLearning": False,
"Open": True,
},
"AdditionalMeetings": [],
"InstructionalMethod": "In-Person",
"Async": False,
}
Properties
It should allow setting and getting of the following properties:
-
crn
(str) -
code
(str) -
department
(str) -
title
(str) -
instructors
(list[str]) - array of strings -
hours
(int) -
days
(list[str]) - array of strings -
start_time
(datetime) -
end_time
datetime) -
location
(str) -
enrollment_current
(int) -
enrollment_max
(int) -
term
(int) -
is_di
(bool) -
is_dir
(bool) -
modality
(str): In-Person or Online
Empty values: If no data are available for a particular property, represent “no data” as follows:
* str = "" # empty string
* int = -1
Methods
Your class should also have the following methods:
- A
constructor
method, which takes a dictionary as an argument and sets the properties right away. A stub has been implemented for you. - A
__repr__
method (similar to Java’s toString() method, which returns the code and name of the course. - A
to_row
method that returns a more detailed representation of the course (e.g., name, crn, instructor, location, time / day, etc.) - An
is_full
method that returns true or false, depending on whether the course is full or not. - An
is_morning
method that returns true or false, depending on whether the course starts before 12PM and false otherwise - An
is_afternoon
method that returns true if the course starts between 12PM-5PM and false otherwise. - An
is_evening
method that returns true if the course starts on or after 5PM and false otherwise.
2. Courses
The job of the Courses class (courses.py
) is to make it convenient to print, filter, and download a list of Course objects.
Properties
It should allow setting and getting of the following properties:
-
courses
: Courses (set by the constructor)
Methods
Your class should also have the following methods:
- A
constructor
method, which takes a list of Course objects (list[Course]) as an argument and sets thecourses
property right away. A stub has been implemented for you in the starter code. - A
to_list
method (no required parameters) which returns thecourses
property - A
size
method (no required parameters) which returns the length of thecourses
list - A
display_to_terminal
method (no required parameters) that prints a nice, formatted list of courses (or a message if there are no courses in the list). Use the course’sto_row
method to help you. - A
get_matching_courses
method which takes a UserPreferences object as an argument and return a new Courses object that includes only the courses that match the user’s preferences.- As you iterate through the available courses, use the CourseFiler object (made by one of your classmates to figure out whether each course is a match).
- Consider chaining your filtering methods together using Python’s built-in
filter
method. Some pseudocode is shown below:# Python's `filter` function returns a new list with only results matching the filter function. # Use multiple filter invocations to apply multiple filters: course_matches = filter(some_filter_function, self.courses) course_matches = filter(another_filter_function, course_matches)
3. UserPreferences
The job of the UserPreferences class (user_preferences.py
) is store the user’s search preference information, and to provide methods for modifying the user’s preferences as they browse courses of interest. If you have been assigned this issue, please note that the rest of the app is highly dependent on this class, so the earlier you complete this task, the easier it will be for your teammates.
Properties
It should allow setting and getting of the following properties.
-
days
: list[str] = [] -
department
: str = “” -
di_only
: bool = False -
dir_only
: bool = False -
hours
: int = -1 -
instructor
: str = “” -
open_closed_status
: str = “” (either “open”, “closed”, or “” (no preference)) -
search_term
: str = “” -
time_of_day
: str = “” (either “morning”, “afternoon”, or “evening”)
Empty values: If the filter is unset (i.e., the user didn’t specify a value one way or the other), represent “unset” preferences as follows:
str = "" # empty string
int = -1
list = []
bool = False
Methods
Your class should also have the following methods:
- A
constructor
method that initializes all of the default property values right away. - An
update_search_term
method that prompts the user to enter a search term or clear their currentsearch_term
property. - An
update_department
method that prompts the user to enter a department or clear their currentdepartment
property. - An
update_instructor
method that prompts the user to enter an instructor or clear their currentinstructor
property. - An
update_di
method that asks the user whether they want to only see DI courses. This will set thedi_only
property. - An
update_dir
method that asks the user whether they want to only see DI-Race courses. This will set thedir_only
property. - An
update_open_closed_status
method that asks the user whether they want to only open or only closed courses. This will set theopen_closed_status
property. - An
update_dir
method that asks the user whether they want to only see DI-Race courses. This will set thedir_only
property. - An
update_hours
method that asks the user whether they only want to see courses with 1, 2, 3, or 4 credit hours. This will set thehours
property. - An
update_days
method that asks the user which days they’re looking for classes. This will set thedays
property. - An
update_time_of_day
method that asks the user whether they want to specify morning, afternoon or evening classes. This will set thetime_of_day
property.
A note on the methods
- All of the update methods must validate the user’s input and re-prompt them if they enter an incorrect value (and explain why the value is incorrect).
- All of the input methods should also be able to clear out the properties (unset them to empty).
- Feel free to use as many private helper methods as you want. In python, private methods are named with a leading underscore.
4. Course Filter
The job of the CourseFilter class (course_filter.py
) is to determine whether a course matches a given set of criteria.
Properties
It should allow setting and getting of the following properties:
-
user_prefs
: UserPreferences
Methods
- A
constructor
method, which takes a UserPreferences object as an argument and sets theuser_prefs
property right away. A stub has been implemented for you. -
is_search_term_match
: accepts a Course object as an argument. Returns true if the course’s title matches the user’s preference (from the UserPreferences) property or if the search_term preference is not set. Returns false otherwise. -
is_instructor_match
: accepts a Course object as an argument. Returns true if the course’s instructor partially matches the user’s preference or if the instructor preference is not set. Returns false otherwise. -
is_di_match
: accepts a Course object as an argument. Returns true if the course is a DI course preference or if the di_only preference is not set. Returns false otherwise. -
is_dir_match
: accepts a Course object as an argument. Returns true if the course is a DI-Race course preference or if the dir_only preference is not set. Returns false otherwise. -
is_departmental_match
: accepts a Course object as an argument. Returns true if the course’s department matches the user’s preference or if the department preference is not set. Returns false otherwise. -
is_hours_match
: accepts a Course object as an argument. Returns true if the course’s credit hours matches the user’s preference or if no preference is set. Returns false otherwise. -
is_status_match
: accepts a Course object as an argument. Returns true if the course’sopen_closed_status
matches the course’s current state or if no preference is set. Returns false otherwise. -
is_time_of_day_match
: accepts a Course object as an argument. Returns true if the course’stime_of_day
matches the course’s time of day status or if no preference is set. Returns false otherwise.
Make sure that all filters are case-insensitive and that they return true if no preference is set.
5. User Interface (UI)
The job of the User Interface file (ui.py
) is to allow people to make choices about what they want to do in the app and receive the appropriate response. This is the main driver of the app and is just a series of function and method calls.
Helper Objects
Because ui.py
is the primary way in which users will interact with the app’s functionality, this file will initialize UserPreferences
, Courses
, and Schedule
objects and make use of their public methods. A small portion of this has already been done for you, but you will continue to use methods from these objects based on user input.
Functions
- A
fetch_courses
method that queries for the course data from the following url: https://meteor.unca.edu/registrar/class-schedules/api/v1/courses/2025/spring, converts each result into aCourse
object, and returns aCourses
object (read more about these objects above). - A
generate_menu
method that returns a string representation of all of the menu choices in the system (watch the demo video to get ideas). This menu should also indicate to the user which preferences they have selected. - A
show_menu
function that prints the menu generated (these two functions have been separated so that they’re easier to test). - A
process_menu_choice
function (which can accept as many arguments as needed) to handle the user’s selection. This function will invoke functionality fromUserPreferences
,Courses
, andSchedule
objects that are initialized when the app starts. It will return the string “quit” if the user asks to terminate the program (so that the application loop knows to break). - A
start_app
method that continues showing the user the menu and re-prompting them until they quit the application.
6. Schedule
The job of the Schedule class (schedule.py
) is to allow the user to build a schedule based on the courses the user finds interesting. Eventually, you may want this class to extend the Courses
class (optional) if it makes sense.
Properties
It should allow setting and getting of the following properties:
-
courses
: a list ofCourse
objects (set by the constructor)
Methods
- A
to_list
method (no required parameters) which returns thecourses
property - A
size
method (no required parameters) which returns the length of thecourses
list - A
display_to_terminal
method (no required parameters) that prints a nice, formatted list of courses (or a message if there are no courses in the list). Use the course’sto_row
method to help you. - An
add_courses
method, which takes a list of Course objects (list[Course]) as an argument and appends them tocourses
. - A
remove_courses
method, which takes a list of Course objects (list[Course]) and removes them fromcourses
. - A
save_schedule
method that saves the current schedule as a CSV file (text file). - An
send_email
, which will email the schedule to a selected recipient.
Set Up
I have also made a video walkthrough of the set up if it helps.
1. Set up your repository
Once your team lead has added you to their repo, you will set up the code locally as follows:
- Open the terminal.
- Clone your team’s version of the repository:
git clone git@github.com:csci338/p01-<your-team-number>-fall2024.git project01
- Note that the second argument of the clone command allows you to specify the name of the local folder (
project01
) even if the remote repo name is different. - Please do not clone this repo inside of another directory that is already under version control.
- Make sure that the folder you just created is called
project01
(important for Docker).
- Note that the second argument of the clone command allows you to specify the name of the local folder (
- Navigate into the
project01
directory you just cloned. Create a new branch that describes the feature you will be working on (name it whatever you want):cd project01 git checkout -b yourname-feature
2. Building Your Docker Container
- Make sure that Docker is running
- Then, from within the
project01
directory on your command line, issue the following Docker command:docker compose up
This command will build your image and your container. Once the container has built, you should see some output like this:
Running 2/1 ✔ Network project01_default Created ✔ Container project01-app-1 Created Attaching to project01-app-1 project01-app-1 | root@d863cf880897:/project01#
Keep this terminal open and then open a new shell (for the remaining commands). In other words, you will have two terminals open: one that’s running your Docker container, and one for issuing other system commands (listed below).
- In your new terminal shell (with the other shell running), find your container pid by listing all of your containers as follows:
docker ps -a
- Open the terminal shell on the Docker container you just made:
docker exec -it <pid> bash # note that this command will put you onto Docker's bash
- Install the poetry dependencies from within your Docker container:
poetry install
- Run the tests:
poetry run pytest -v
- Run the app:
poetry run python course_lookup/ui.py
- Now you’re ready to start coding in VS Code.
1. Returning to Docker later
Once you’ve built your container, it should be available to you in the future, even if you close out of Docker or stop the container. To get back to your container:
- Make sure Docker is running
- Find the pid of the container you want to run:
docker ps -a
- Start the stopped container with the pid you just found:
docker start <pid>
- Use container as per usual (and make sure you’re in the
project01
directory when you run the poetry commands).
You could also go to the Docker Dashboard and start your container from the Dashboard.
2 Some useful commands
Command | Description |
---|---|
docker ps -a |
List all of the containers (including stopped containers) |
docker stop <pid> |
List all of the containers (including stopped containers) |
docker start <pid> |
List all of the containers (including stopped containers) |
docker exec -it <pid> bash |
Runs terminal on the container |
docker exec -it <pid> poetry run python course_lookup/driver.py |
Runs the driver.py using poetry. |
docker exec -it <pid> poetry run pytest -v |
Runs tests |
docker exec -it <pid> bash scripts/check.sh |
Runs check script |
docker exec -it <pid> bash scripts/fix.sh |
Runs fix script |
Logistics
Before you begin working on this project, please read the guidelines VERY CAREFULLY.
1. Working with Feature Branches
- You will never edit the
main
branch directly. Considermain
to be the “source of truth” for the app. - All of your work will be committed to your feature branch (which you will create from the
main
branch). -
Please pull down the latest changes from
main
periodically and rebase your local branch withmain
. To do this:- Commit your feature branch work,
- Check out main,
- Pull down the latest changes,
- Check out your feature branch again, and
- Rebase your feature branch with main
2. Testing Tips
Run the tests using the pytest library (installed via poetry). Some sample commands:
poetry run pytest -v # runs all the tests
poetry run pytest tests/test_courses.py -v # runs all the tests in a single file
poetry run pytest test_courses.py::TestCourses::test_one -v # runs a single test (e.g., test_one)
3. Formatting Tips
We are using black
to format our code, isort
to sort our import statements, and flake8
to validate your Python code.
- https://black.readthedocs.io/en/stable/usage_and_configuration/index.html
- https://pypi.org/project/isort/
- https://flake8.pycqa.org/en/latest/
The following commands will be run on every push to make sure that the code styling rules have been followed:
poetry run black . --check # runs the Python formatter
poetry run isort . --check-only # run the Python import sorter
poetry run flake8 # flake8 just checks things -- you have to fix them manually
You can also use the bash/check.sh
script (for convenience):
bash scripts/check.sh
To actually APPLY the formatting (which will make changes to your files), make sure you run these two commands before pushing your branch to GitHub:
poetry run black . # runs the Python formatter
poetry run isort . # run the Python import sorter
You can also use the bash/fix.sh script (for convenience):
bash scripts/fix.sh
If you want flake8 to ignore a particular line, add a # noqa
comment at the end of the line
4. GitHub Workflows & Continuous Integration Notes
The .github/workflows/pr.yml
is a configuration file that GitHub reads in order to run a series of checks everytime you push your branch to GitHub. Before you make a pull request, make sure that you run the bash scripts/run_pr_checks.sh
script. If everything passes locally, it should also pass on GitHub.
Grading and Assessment
1. How to submit work for review
Submissions
You will submit your work by making a pull request (PR) and tagging svanwart
as a reviewer. Before making your PR, make sure that you have:
- Implemented all of properties and methods needed to support the functionality listed above (using private helper methods as needed).
- Written tests for all public methods.
- Ensured all of the formatter and linter checks pass before making a pull request.
Pull Request Details
In the text of your PR, be sure that:
- You reference this issue by using the hash tag followed by the issue number.
- You describe what has been done in the PR as simply and clearly as possible.
- All of the validation checks on GitHub need to pass
- There are no merge conflicts with the
main
branch. You will need to coordinate with your teammmates to figure out how to integrate some of the work.
A few other PR details:
- You may make multiple PRs to implement subsets of functionality. Remember: it’s better to create smaller, incremental PRs than one big PR (no long-running branches).
- I (Sarah) will review all pull requests from the previous day by 10AM the following morning
- Once I approve your pull request, you will be responsible for merging your branch into
main
via GitHub.
Revisions
I may ask you for revisions on your code, which is a normal, expected part of the development process. If this happens:
- Please make the requested changes on your feature branch
- Push your new changes to the remote branch
- Modify the PR text to explain what you changed
- Tag
svanwart
again so that I can review your changes.
2. Rubric
20% | Teamwork and communication | Where you a good team player? Did you talk to your teammates? If someone else was working on a feature that you depended on, did you reach out / coordinate / communicate with them? If someone reached out to you because they depended on a task you were doing, did you respond to them in a timely manner? |
50% | Individual code contribution | Did you complete your tasks? Does your code work as expected? Do all of your functions / methods have tests? Did the code quality checks pass? Was your PR(s) approved? Were your (PRs) approved by the deadline? |
30% | Quality of the final group product | Does your app work? Is it complete? Can users browse for courses, filter according to their preferences, and add them to a schedule? Were everyone’s contributions merged by the deadline (recognizing that the deadline may be a moving target, given the contraints we are operating under)? |
At the end of Project 1, you will be asked to complete a brief, anonymous survey about the contributions of person in your group. Even though we are all remote and operating on different sets of constraints, it is still possible to shoot a quick email or text to give a brief status report. If you are concerned about your ability to participate in this project please talk to Sarah as soon as possible so that the appropriate accommodations can be made.