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

#86 Decoupling the scraper from the backend and editing the scraper to make it more versatile with different quarters #92

Open
wants to merge 37 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
84b7ca3
Making new scraper folder and editing docker file to accommodate
CTrando Mar 17, 2019
59321d7
#86: decouple scraper from backend
snowme34 Apr 1, 2019
0d19ed8
#86: Allowing the scraper to work with multiple quarters - backend is…
CTrando Apr 3, 2019
279b401
Fixing comment and pruning quarter insert script
CTrando Apr 4, 2019
c9afcc0
Making webreg upload script create database and quarters table for fu…
CTrando Apr 5, 2019
4e9ebc5
Adding necessary config file
CTrando Apr 5, 2019
db0f27e
add quarter parameter to application
snowme34 Apr 8, 2019
922b633
bug fix, add quarter variable, note frontend still not sending the ne…
snowme34 Apr 8, 2019
4cc3bb1
#86 trying to add default quarter support for backend
snowme34 Apr 9, 2019
490ca06
#86 fix import path
snowme34 Apr 9, 2019
e3f76a9
Fixing chrome version
CTrando Apr 15, 2019
47faaba
Merge branch 'scratch/issue86' of github.com:ucsdscheduleplanner/UCSD…
CTrando Apr 15, 2019
682a549
#86 trying to prune Dockerfile
snowme34 Apr 19, 2019
4452996
#86 working on pruning requirements.txt
snowme34 Apr 19, 2019
3c6a080
#86 add note for possible caching bug
snowme34 Apr 19, 2019
9ad923d
#86 disable WI19, no one cares
snowme34 Apr 19, 2019
498ce1e
#86 add mysql non-root user, based on config.ini
snowme34 Apr 19, 2019
29e49a2
Adding golang routes and adding back department functionality and cou…
CTrando Apr 26, 2019
6862d9a
Merge branch 'scratch/issue86' of github.com:ucsdscheduleplanner/UCSD…
CTrando Apr 26, 2019
2872b6a
Adding other github repos, they will not be submodules so will not ac…
CTrando Apr 26, 2019
03b4504
Finished moving backend to Golang
CTrando May 3, 2019
72ca1a6
Adding more tests and working out compatability issues with frontend
CTrando May 3, 2019
db27989
Creating module for Go backend and restructure the code
snowme34 Jun 2, 2019
b771ba5
Add 2 functions in RoutesCommon to remove repeats; Add systematical w…
snowme34 Jun 3, 2019
1531f72
Add development config
snowme34 Jun 3, 2019
0eb55b0
Update RoutesCommon functions and error processing
snowme34 Jun 5, 2019
a7bde26
Cleanup Routes Code and update comments
snowme34 Jun 5, 2019
d8a97fa
Make a straightforward constructor for db struct and update the exist…
snowme34 Jun 6, 2019
fafe144
Add miltiple constructors for db since lower-case struct fields are n…
snowme34 Jun 6, 2019
513216b
Test using mock db
snowme34 Jun 6, 2019
98c989a
refactor, fix error handling
snowme34 Aug 3, 2019
49ddc45
refacotr more
snowme34 Aug 4, 2019
f6af1f6
refactor, restructure, add a ctx package with env
snowme34 Aug 5, 2019
9eb7de2
change embarrassing package name
snowme34 Aug 6, 2019
06feb39
refactor, add handler factoary
snowme34 Aug 11, 2019
3dd064a
remove python code
snowme34 Aug 11, 2019
ddf6944
add Docker support for go backend
snowme34 Aug 19, 2019
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
13 changes: 11 additions & 2 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@ import (
"github.com/ucsdscheduleplanner/UCSD-Schedule-Planner/backend/routes"
)

// TODO: make this config-able
const port = 8080

// create closure for http handler func
snowme34 marked this conversation as resolved.
Show resolved Hide resolved
func makeHandler(f func(http.ResponseWriter, *http.Request, *db.DatabaseStruct), ds *db.DatabaseStruct) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { f(w, r, ds) }
}

func main() {

var config, err = ini.Load(filepath.Join(".", "config", "config.example.ini"))

// var config, err = ini.Load(filepath.Join(".", "config", "config.dev.ini"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODEL: Temporary Development Code


if err != nil {
panic("Error reading config file: " + err.Error())
}
Expand All @@ -35,6 +40,7 @@ func main() {
panic("Failed to init db: " + err.Error())
}

// only for the completeness of the code
defer ds.Close()
snowme34 marked this conversation as resolved.
Show resolved Hide resolved

log.Printf("Starting server on port: %v\n", port)
Expand All @@ -45,6 +51,9 @@ func main() {
http.HandleFunc("/api_instructors", makeHandler(routes.GetInstructors, ds))
http.HandleFunc("/api_types", makeHandler(routes.GetTypes, ds))

// close db no matter what
log.Panic(http.ListenAndServe(":"+strconv.Itoa(port), nil))
// not fatal to close db (defer above)
err = http.ListenAndServe(":"+strconv.Itoa(port), nil)
if err != nil {
log.Panic(err)
}
}
20 changes: 19 additions & 1 deletion backend/db/db.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Package db provides a simple way to communicate with database
package db

// TODO: if necessary, covert the db struct to a Env struct
// that stores environment of the whole server, e.g. *DB, port, hostname, config, etc.
// Env will be constructed in main() and passed to all other handlers

import (
"database/sql"
"encoding/json"
Expand Down Expand Up @@ -36,7 +40,17 @@ func New(config *ini.File) (*DatabaseStruct, error) {

tableNames = append(tableNames, "DEPARTMENT")

db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@/%s%s", username, password, endpoint, databaseName))
// TODEL: must delete, for back compatibility temporarily
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODEL: Temporary Development Code

tableNames = append(tableNames, "CLASS_DATA")

db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s", username, password, endpoint, databaseName))
// db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@/%s%s", username, password, endpoint, databaseName))
snowme34 marked this conversation as resolved.
Show resolved Hide resolved

if err != nil {
return nil, err
}

err = db.Ping() // validate the connection

if err != nil {
return nil, err
Expand All @@ -61,14 +75,18 @@ func (ds *DatabaseStruct) Close() {

// Query using the input SQL query
func (ds *DatabaseStruct) Query(tableName string, sqlQuery string, params ...interface{}) (*sql.Rows, error) {

if !ds.isValidTable(tableName) {
return nil, fmt.Errorf("Table name '%s' is not valid, cannot continue query", tableName)
}

// Query will creates a connection and automatically release it
// ref: https://golang.org/src/database/sql/sql.go?s=40984:41081#L1522
results, err := ds.db.Query(sqlQuery, params...)

if err != nil {
return nil, errors.New(err.Error())
snowme34 marked this conversation as resolved.
Show resolved Hide resolved
}

return results, nil
}
36 changes: 13 additions & 23 deletions backend/routes/ClassData.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"

"github.com/ucsdscheduleplanner/UCSD-Schedule-Planner/backend/db"
snowme34 marked this conversation as resolved.
Show resolved Hide resolved
)

// TODO create some error checking functions to handle duplicated error checking codes
const logTagClassData = "[ClassData]"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea what this variable does, is it like a log id?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this, the logs will look like:

[ClassData] Http Error, not found...
[Type] Http Error, not found...
...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there is better way to do it but this is what I learned from school :(

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I say I don't know what a variable does, it means it is not immediately clear when reading the variable what purpose it serves.

You should have another file with a list of consts that have intuitive ids. Remember part of how you program is so that future devs can understand what you write.


// TODO make this graceful
// TODO: make this graceful, maybe an Env var or config
snowme34 marked this conversation as resolved.
Show resolved Hide resolved
// set default quarter to SP19
const defaultQuarter = "SP19"

Expand Down Expand Up @@ -58,32 +56,31 @@ type Subclass struct {
Description string `json:"description"`
}

// GetClassData is a pre-http.HandlerFunc for class data route ready to be a closure with *DatabaseStruct
// GetClassData is a pre-http.HandlerFunc for class data route and will become a closure with *DatabaseStruct
func GetClassData(writer http.ResponseWriter, request *http.Request, ds *db.DatabaseStruct) {

if request.Method != "POST" {
http.Error(writer, "Unsupported request type", http.StatusMethodNotAllowed)
errInvalidMethod(logTagClassData, writer, request)
return
}

body, err := ioutil.ReadAll(request.Body)

if err != nil {
http.Error(writer, "Error handling POST request", http.StatusBadRequest)
log.Printf("%s Failed to read request to %q from %q: %s", logTagClassData, request.RequestURI, request.RemoteAddr, err.Error())
errRequestBodyPost(logTagClassData, err, writer, request)
return
}

var classesToQuery []ClassDataRequest
err = json.Unmarshal(body, &classesToQuery)

if err != nil {
http.Error(writer, "Failed to parse request", http.StatusInternalServerError)
log.Printf("%s Failed to parse request to %q from %q: %s", logTagClassData, request.RequestURI, request.RemoteAddr, err.Error())
errRequestBodyParse(logTagClassData, err, writer, request)
return
}

// TODO: reduce to one query?

ret := make(map[string][]Subclass)
for i := 0; i < len(classesToQuery); i++ {
snowme34 marked this conversation as resolved.
Show resolved Hide resolved
currentClass := classesToQuery[i]
Expand All @@ -92,16 +89,12 @@ func GetClassData(writer http.ResponseWriter, request *http.Request, ds *db.Data
rows, err := ds.Query(currentClass.quarter, query, currentClass.department, currentClass.courseNumber)

if err != nil {
http.Error(writer, "Error querying for class data", http.StatusInternalServerError)
// TODO think about better ways to log the query, and fix all the other such logs in other files
log.Printf("%s Failed to query data with %q from %q: %s", logTagClassData, query+" "+currentClass.department+" "+currentClass.courseNumber, request.RemoteAddr, err.Error())
errQuery(logTagClassData, err, writer, request, query, currentClass.department, currentClass.courseNumber)
return
}

// TODO this might not be an error, better error message?
if rows == nil {
http.Error(writer, "Error querying for class data", http.StatusInternalServerError)
log.Printf("%s Empty query data with %q from %q: %s", logTagClassData, query+" "+currentClass.department+" "+currentClass.courseNumber, request.RemoteAddr, err.Error())
errEmptyQuery(logTagClassData, writer, request, query, currentClass.department, currentClass.courseNumber)
return
}

Expand All @@ -122,8 +115,7 @@ func GetClassData(writer http.ResponseWriter, request *http.Request, ds *db.Data
&subClass.Description)

if err != nil {
http.Error(writer, "Error retrieving data", http.StatusInternalServerError)
log.Printf("%s Failed to query data with %q from %q: %s", logTagClassData, query+" "+currentClass.department+" "+currentClass.courseNumber, request.RemoteAddr, err.Error())
errParseQueryResult(logTagClassData, err, writer, request)
return
}

Expand All @@ -133,18 +125,16 @@ func GetClassData(writer http.ResponseWriter, request *http.Request, ds *db.Data

retJSON, err := json.Marshal(ret)

// TODO use a function to deal with same error checks for the routes
if err != nil {
http.Error(writer, "Error retrieving data", http.StatusInternalServerError)
log.Printf("%s Failed to read data from ret in response to %q: %s", logTagClassData, request.RemoteAddr, err.Error())
errCreateResponse(logTagClassData, err, writer, request)
return
}

_, err = writer.Write(retJSON)
status, err := writer.Write(retJSON)

if err != nil {
http.Error(writer, "Failed to send response", http.StatusInternalServerError)
log.Printf("%s Failed to write data in response to %q: %s", logTagClassData, request.RemoteAddr, err.Error()) // log JSON?
errWriteResponse(logTagClassData, err, writer, request, status)
return
}

}
57 changes: 10 additions & 47 deletions backend/routes/Department.go
Original file line number Diff line number Diff line change
@@ -1,65 +1,28 @@
package routes

import (
"encoding/json"
"log"
"database/sql"
"net/http"

"github.com/ucsdscheduleplanner/UCSD-Schedule-Planner/backend/db"
)

const logTagDepartment = "[Department]"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Department is not a great message, how about GetDepartment so we know the method

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using the exact file name for all of them. Tell me if you think using GetDepartment (or the route names for all) is better


// GetDepartments is a pre-http.HandlerFunc for department route ready to be a closure with *DatabaseStruct
// GetDepartments is a pre-http.HandlerFunc for department route and will become a closure with *DatabaseStruct
func GetDepartments(writer http.ResponseWriter, request *http.Request, ds *db.DatabaseStruct) {

if request.Method != "GET" {
http.Error(writer, "Method not allowed", http.StatusMethodNotAllowed)
errInvalidMethod(logTagDepartment, writer, request)
return
}

rows, err := ds.Query("DEPARTMENT", "SELECT DISTINCT DEPT_CODE FROM DEPARTMENT")
queryAndResponse(ds, logTagDepartment, writer, request,
func(rows *sql.Rows) (interface{}, error) {
var val string
err := rows.Scan(&val)
return val, err
},
"SELECT DISTINCT DEPT_CODE FROM DEPARTMENT", "DEPARTMENT")

if err != nil {
http.Error(writer, "Error querying departments", http.StatusInternalServerError)
log.Printf("%s Failed to query data with %q from %q: %s", logTagDepartment, "SELECT DISTINCT DEPT_CODE FROM DEPARTMENT", request.RemoteAddr, err.Error())
return
}

if rows == nil {
http.Error(writer, "Error querying departments", http.StatusInternalServerError)
log.Printf("%s Empty query data with %q from %q: %s", logTagDepartment, "SELECT DISTINCT DEPT_CODE FROM DEPARTMENT", request.RemoteAddr, err.Error())
return
}

var departments []string
for rows.Next() {
var (
department string
)
err := rows.Scan(&department)
if err != nil {
http.Error(writer, "Error parsing departments", http.StatusInternalServerError)
log.Printf("%s Failed to parse departments: %s", logTagDepartment, err.Error())
return
}

departments = append(departments, department)
}

departmentJSON, err := json.Marshal(departments)

if err != nil {
http.Error(writer, "Error fetching departments", http.StatusInternalServerError)
log.Printf("%s Failed to read data from departments in response to %q: %s", logTagDepartment, request.RemoteAddr, err.Error())
return
}

_, err = writer.Write(departmentJSON)

if err != nil {
http.Error(writer, "Failed to send response", http.StatusInternalServerError)
log.Printf("%s Failed to write data in response to %q: %s", logTagDepartment, request.RemoteAddr, err.Error()) // log JSON?
return
}
}
75 changes: 15 additions & 60 deletions backend/routes/DepartmentSummary.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package routes

import (
"encoding/json"
"log"
"database/sql"
"net/http"

"github.com/ucsdscheduleplanner/UCSD-Schedule-Planner/backend/db"
Expand All @@ -17,74 +16,30 @@ type DepartmentSummary struct {
Description string `json:"description"`
}

// GetCourseNums is a pre-http.HandlerFunc for course num route ready to be a closure with *DatabaseStruct
// GetCourseNums is a pre-http.HandlerFunc for course num route ready and will become a closure with *DatabaseStruct
func GetCourseNums(writer http.ResponseWriter, request *http.Request, ds *db.DatabaseStruct) {
if request.Method != "GET" {
http.Error(writer, "Invalid method type", http.StatusMethodNotAllowed)
return
}

// TODO create a function for query value check
keys, ok := request.URL.Query()["department"]
department := keys[0]

if !ok {
http.Error(writer, "Request does not contain department!", http.StatusBadRequest)
// not logging invalid input yet
if request.Method != "GET" {
errInvalidMethod(logTagDepartmentSummary, writer, request)
return
}

keys, ok = request.URL.Query()["quarter"]
quarter := keys[0]
// NOTE: hack way to re-use function
department, _, quarter, missing := readDeptCourseNumQuarter(request)

if !ok {
http.Error(writer, "Request does not contain department!", http.StatusBadRequest)
// not logging invalid input yet
if missing != "courseNum" {
errMissingInput(logTagInstructors, writer, request, missing)
return
}

query := "SELECT DISTINCT DEPARTMENT, COURSE_NUM, DESCRIPTION FROM " + quarter + " WHERE DEPARTMENT=?"
rows, err := ds.Query(quarter, query, department)

if err != nil {
http.Error(writer, "Error querying department summary", http.StatusInternalServerError)
log.Printf("%s Failed to query data with %q from %q: %s", logTagDepartmentSummary, query, request.RemoteAddr, err.Error())
return
}

if rows == nil {
http.Error(writer, "Could not query correctly.", http.StatusInternalServerError)
log.Printf("%s Failed to query data with %q from %q: %s", logTagDepartmentSummary, query, request.RemoteAddr, err.Error())
return
}

var ret []DepartmentSummary
queryAndResponse(ds, logTagDepartmentSummary, writer, request,
func(rows *sql.Rows) (interface{}, error) {
val := DepartmentSummary{}
err := rows.Scan(&val.Department, &val.CourseNum, &val.Description)
return val, err
},
query, quarter, department)

for rows.Next() {
departmentSummary := DepartmentSummary{}
err := rows.Scan(&departmentSummary.Department, &departmentSummary.CourseNum, &departmentSummary.Description)

if err != nil {
http.Error(writer, "Could not scan data into struct", http.StatusInternalServerError)
log.Printf("%s Failed to parse department summary: %s", logTagDepartmentSummary, err.Error())
return
}

ret = append(ret, departmentSummary)
}

retJSON, err := json.Marshal(ret)

if err != nil {
http.Error(writer, "Error converting data", http.StatusInternalServerError)
log.Printf("%s Failed to read data from departments in response to %q: %s", logTagDepartmentSummary, request.RemoteAddr, err.Error())
return
}

_, err = writer.Write(retJSON)
if err != nil {
http.Error(writer, "Failed to send response", http.StatusInternalServerError)
log.Printf("%s Failed to write data in response to %q: %s", logTagDepartmentSummary, request.RemoteAddr, err.Error()) // log JSON?
return
}
}
Loading