diff --git a/common/database/database.go b/common/database/database.go index a6832a8c..c78e75fe 100644 --- a/common/database/database.go +++ b/common/database/database.go @@ -13,6 +13,7 @@ type Database interface { RemoveAll(collection_name string, query interface{}) (*ChangeResults, error) Insert(collection_name string, item interface{}) error Upsert(collection_name string, selector interface{}, update interface{}) (*ChangeResults, error) + Patch(collection_name string, selector interface{}, patch interface{}) error Update(collection_name string, selector interface{}, update interface{}) error UpdateAll(collection_name string, selector interface{}, update interface{}) (*ChangeResults, error) DropDatabase() error diff --git a/common/database/mongo_database.go b/common/database/mongo_database.go index 223d9df3..26a1b967 100644 --- a/common/database/mongo_database.go +++ b/common/database/mongo_database.go @@ -8,6 +8,7 @@ import ( "github.com/HackIllinois/api/common/config" "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" ) /* @@ -197,6 +198,17 @@ func (db *MongoDatabase) Upsert(collection_name string, selector interface{}, up return &change_results, convertMgoError(err) } +func (db *MongoDatabase) Patch(collection_name string, selector interface{}, patch interface{}) error { + current_session := db.GetSession() + defer current_session.Close() + + collection := current_session.DB(db.name).C(collection_name) + + err := collection.Update(selector, bson.M{"$set": patch}) + + return convertMgoError(err) +} + /* Finds an item based on the given selector and updates it with the data in update */ diff --git a/common/datastore/validation.go b/common/datastore/validation.go index 4098ac73..17cbe261 100644 --- a/common/datastore/validation.go +++ b/common/datastore/validation.go @@ -2,16 +2,28 @@ package datastore import ( "fmt" + "gopkg.in/go-playground/validator.v9" ) func (datastore *DataStore) Validate() error { validate := validator.New() - return validateField(datastore.Data, datastore.Definition, validate) + return validateField(datastore.Data, datastore.Definition, validate, false) +} + +func (datastore *DataStore) ValidateNonEmpty() error { + validate := validator.New() + + return validateField(datastore.Data, datastore.Definition, validate, true) } -func validateField(data interface{}, definition DataStoreDefinition, validate *validator.Validate) error { +func validateField( + data interface{}, + definition DataStoreDefinition, + validate *validator.Validate, + ignore_empty bool, +) error { err := validate.Var(data, definition.Validations) if err != nil { @@ -26,7 +38,7 @@ func validateField(data interface{}, definition DataStoreDefinition, validate *v return NewErrTypeMismatch(data, "map[string]interface{}") } - return validateFieldArray(mapped_data, definition, validate) + return validateFieldArray(mapped_data, definition, validate, ignore_empty) case "[]object": data_array, ok := data.([]map[string]interface{}) @@ -35,7 +47,7 @@ func validateField(data interface{}, definition DataStoreDefinition, validate *v } for _, mapped_data := range data_array { - err = validateFieldArray(mapped_data, definition, validate) + err = validateFieldArray(mapped_data, definition, validate, ignore_empty) if err != nil { return err @@ -48,9 +60,20 @@ func validateField(data interface{}, definition DataStoreDefinition, validate *v } } -func validateFieldArray(data map[string]interface{}, definition DataStoreDefinition, validate *validator.Validate) error { +func validateFieldArray( + data map[string]interface{}, + definition DataStoreDefinition, + validate *validator.Validate, + ignore_empty bool, +) error { for _, field := range definition.Fields { - err := validateField(data[field.Name], field, validate) + if ignore_empty { + if _, ok := data[field.Name]; !ok { + continue + } + } + + err := validateField(data[field.Name], field, validate, ignore_empty) if err != nil { return err diff --git a/gateway/services/registration.go b/gateway/services/registration.go index 757f8b61..d177ea43 100644 --- a/gateway/services/registration.go +++ b/gateway/services/registration.go @@ -36,6 +36,12 @@ var RegistrationRoutes = arbor.RouteCollection{ "/registration/attendee/", alice.New(middleware.AuthMiddleware([]models.Role{models.ApplicantRole}), middleware.IdentificationMiddleware).ThenFunc(UpdateRegistration).ServeHTTP, }, + arbor.Route{ + "PatchCurrentUserRegistration", + "PATCH", + "/registration/attendee/", + alice.New(middleware.AuthMiddleware([]models.Role{models.ApplicantRole}), middleware.IdentificationMiddleware).ThenFunc(PatchRegistration).ServeHTTP, + }, arbor.Route{ "GetFilteredUserRegistrations", "GET", @@ -60,6 +66,12 @@ var RegistrationRoutes = arbor.RouteCollection{ "/registration/mentor/", alice.New(middleware.AuthMiddleware([]models.Role{models.MentorRole}), middleware.IdentificationMiddleware).ThenFunc(UpdateRegistration).ServeHTTP, }, + arbor.Route{ + "PatchCurrentMentorRegistration", + "PATCH", + "/registration/mentor/", + alice.New(middleware.AuthMiddleware([]models.Role{models.UserRole}), middleware.IdentificationMiddleware).ThenFunc(PatchRegistration).ServeHTTP, + }, arbor.Route{ "GetFilteredMentorRegistrations", "GET", @@ -97,3 +109,7 @@ func CreateRegistration(w http.ResponseWriter, r *http.Request) { func UpdateRegistration(w http.ResponseWriter, r *http.Request) { arbor.PUT(w, config.REGISTRATION_SERVICE+r.URL.String(), RegistrationFormat, "", r) } + +func PatchRegistration(w http.ResponseWriter, r *http.Request) { + arbor.PATCH(w, config.REGISTRATION_SERVICE+r.URL.String(), RegistrationFormat, "", r) +} diff --git a/services/registration/controller/controller.go b/services/registration/controller/controller.go index 11ab2152..8b85a540 100644 --- a/services/registration/controller/controller.go +++ b/services/registration/controller/controller.go @@ -21,11 +21,13 @@ func SetupController(route *mux.Route) { router.HandleFunc("/attendee/", GetCurrentUserRegistration).Methods("GET") router.HandleFunc("/attendee/", CreateCurrentUserRegistration).Methods("POST") router.HandleFunc("/attendee/", UpdateCurrentUserRegistration).Methods("PUT") + router.HandleFunc("/attendee/", PatchCurrentUserRegistration).Methods("PATCH") router.HandleFunc("/attendee/filter/", GetFilteredUserRegistrations).Methods("GET") router.HandleFunc("/mentor/", GetCurrentMentorRegistration).Methods("GET") router.HandleFunc("/mentor/", CreateCurrentMentorRegistration).Methods("POST") router.HandleFunc("/mentor/", UpdateCurrentMentorRegistration).Methods("PUT") + router.HandleFunc("/mentor/", PatchCurrentMentorRegistration).Methods("PATCH") router.HandleFunc("/mentor/filter/", GetFilteredMentorRegistrations).Methods("GET") router.HandleFunc("/{id}/", GetAllRegistrations).Methods("GET") @@ -257,6 +259,63 @@ func UpdateCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(updated_registration) } +/* + Endpoint to patch user registration attributes +*/ +func PatchCurrentUserRegistration(w http.ResponseWriter, r *http.Request) { + id := r.Header.Get("HackIllinois-Identity") + + if id == "" { + errors.WriteError(w, r, errors.MalformedRequestError("Must provide id in request.", "Must provide id in request.")) + return + } + + var patch_data map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&patch_data) + + if err != nil { + errors.WriteError(w, r, errors.InternalError(err.Error(), "Could not decode user registration information. Possible failure in JSON validation, or invalid registration format.")) + return + } + + patch_data["id"] = id + + registration_patch := datastore.NewDataStore(config.REGISTRATION_DEFINITION) + for _, field := range registration_patch.Definition.Fields { + if _, ok := patch_data[field.Name]; !ok { + delete(patch_data, field.Name) + } + } + + registration_patch.Data = patch_data; + + registration_patch.Data["updatedAt"] = time.Now().Unix() + + err = service.PatchUserRegistration(id, registration_patch) + + if err != nil { + errors.WriteError(w, r, errors.InternalError(err.Error(), "Could not update user's registration.")) + return + } + + updated_registration, err := service.GetUserRegistration(id) + + if err != nil { + errors.WriteError(w, r, errors.DatabaseError(err.Error(), "Could not fetch user's updated registration.")) + return + } + + mail_template := "registration_update" + err = service.SendUserMail(id, mail_template) + + if err != nil { + errors.WriteError(w, r, errors.InternalError(err.Error(), "Could not send registration update email.")) + return + } + + json.NewEncoder(w).Encode(updated_registration) +} + /* Endpoint to get user registrations based on filters */ @@ -402,6 +461,63 @@ func UpdateCurrentMentorRegistration(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(updated_registration) } +/* + Endpoint to patch mentor registration attributes +*/ +func PatchCurrentMentorRegistration(w http.ResponseWriter, r *http.Request) { + id := r.Header.Get("HackIllinois-Identity") + + if id == "" { + errors.WriteError(w, r, errors.MalformedRequestError("Must provide id in request.", "Must provide id in request.")) + return + } + + var patch_data map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&patch_data) + + if err != nil { + errors.WriteError(w, r, errors.InternalError(err.Error(), "Could not decode mentor registration information. Possible failure in JSON validation, or invalid registration format.")) + return + } + + patch_data["id"] = id + + mentor_registration_patch := datastore.NewDataStore(config.MENTOR_REGISTRATION_DEFINITION) + for _, field := range mentor_registration_patch.Definition.Fields { + if _, ok := patch_data[field.Name]; !ok { + delete(patch_data, field.Name) + } + } + + mentor_registration_patch.Data = patch_data; + + mentor_registration_patch.Data["updatedAt"] = time.Now().Unix() + + err = service.PatchMentorRegistration(id, mentor_registration_patch) + + if err != nil { + errors.WriteError(w, r, errors.InternalError(err.Error(), "Could not update mentor's registration.")) + return + } + + updated_registration, err := service.GetMentorRegistration(id) + + if err != nil { + errors.WriteError(w, r, errors.DatabaseError(err.Error(), "Could not fetch mentor's updated registration.")) + return + } + + mail_template := "registration_update" + err = service.SendUserMail(id, mail_template) + + if err != nil { + errors.WriteError(w, r, errors.InternalError(err.Error(), "Could not send registration update email.")) + return + } + + json.NewEncoder(w).Encode(updated_registration) +} + /* Endpoint to get mentor registrations based on filters */ diff --git a/services/registration/service/registration_service.go b/services/registration/service/registration_service.go index 0509f13b..459c9174 100644 --- a/services/registration/service/registration_service.go +++ b/services/registration/service/registration_service.go @@ -2,12 +2,13 @@ package service import ( "errors" + "strconv" + "strings" + "github.com/HackIllinois/api/common/database" "github.com/HackIllinois/api/services/registration/config" "github.com/HackIllinois/api/services/registration/models" "gopkg.in/go-playground/validator.v9" - "strconv" - "strings" ) var validate *validator.Validate @@ -89,6 +90,23 @@ func UpdateUserRegistration(id string, user_registration models.UserRegistration return err } +/* + Patches the user registration associated with the given user id +*/ +func PatchUserRegistration(id string, registration_patch models.UserRegistration) error { + err := registration_patch.ValidateNonEmpty() + + if err != nil { + return err + } + + selector := database.QuerySelector{"id": id} + + err = db.Patch("attendees", selector, ®istration_patch) + + return err +} + /* Returns db search query based on given parameters */ @@ -214,6 +232,23 @@ func UpdateMentorRegistration(id string, mentor_registration models.MentorRegist return err } +/* + Patches the mentor registration associated with the given user id +*/ +func PatchMentorRegistration(id string, mentor_registration_patch models.MentorRegistration) error { + err := mentor_registration_patch.ValidateNonEmpty() + + if err != nil { + return err + } + + selector := database.QuerySelector{"id": id} + + err = db.Patch("mentors", selector, &mentor_registration_patch) + + return err +} + /* Returns the mentor registrations associated with the given parameters */ diff --git a/services/registration/tests/registration_test.go b/services/registration/tests/registration_test.go index 3af47484..69383049 100644 --- a/services/registration/tests/registration_test.go +++ b/services/registration/tests/registration_test.go @@ -162,6 +162,44 @@ func TestUpdateUserRegistrationService(t *testing.T) { CleanupTestDB(t) } +/* + Service level test for patching user registration in the db +*/ +func TestPatchUserRegistrationService(t *testing.T) { + SetupTestDB(t) + test_registration := datastore.NewDataStore(config.REGISTRATION_DEFINITION) + json.Unmarshal([]byte(patch_registration_data), &test_registration) + test_data := make (map[string]interface{}) + test_data["firstName"] = "John" + test_data["lastName"] = "Smith" + test_data["email"] = "new_test@gmail.com" + test_registration.Data = test_data + fmt.Println(test_registration) + err := service.PatchUserRegistration("testid", test_registration) + + if err != nil { + t.Fatal(err) + } + + user_registration, err := service.GetUserRegistration("testid") + + if err != nil { + t.Fatal(err) + } + + expected_registration := getBaseUserRegistration() + expected_registration.Data["id"] = "testid" + expected_registration.Data["firstName"] = "John" + expected_registration.Data["lastName"] = "Smith" + expected_registration.Data["email"] = "new_test@gmail.com" + + if !reflect.DeepEqual(user_registration.Data["firstName"], expected_registration.Data["firstName"]) { + t.Errorf("Wrong user info.\nExpected %v\ngot %v\n", expected_registration.Data["firstName"], user_registration.Data["firstName"]) + } + + CleanupTestDB(t) +} + /* Service level test for getting mentor registration from db */ @@ -251,6 +289,44 @@ func TestUpdateMentorRegistrationService(t *testing.T) { CleanupTestDB(t) } +/* + Service level test for patching user registration in the db +*/ +func TestPatchMentorRegistrationService(t *testing.T) { + SetupTestDB(t) + test_registration := datastore.NewDataStore(config.MENTOR_REGISTRATION_DEFINITION) + json.Unmarshal([]byte(patch_registration_data), &test_registration) + test_data := make (map[string]interface{}) + test_data["firstName"] = "John" + test_data["lastName"] = "Smith" + test_data["email"] = "new_test@gmail.com" + test_registration.Data = test_data + fmt.Println(test_registration) + err := service.PatchMentorRegistration("testid", test_registration) + + if err != nil { + t.Fatal(err) + } + + user_registration, err := service.GetMentorRegistration("testid") + + if err != nil { + t.Fatal(err) + } + + expected_registration := getBaseMentorRegistration() + expected_registration.Data["id"] = "testid" + expected_registration.Data["firstName"] = "John" + expected_registration.Data["lastName"] = "Smith" + expected_registration.Data["email"] = "new_test@gmail.com" + + if !reflect.DeepEqual(user_registration.Data["firstName"], expected_registration.Data["firstName"]) { + t.Errorf("Wrong user info.\nExpected %v\ngot %v\n", expected_registration.Data["firstName"], user_registration.Data["firstName"]) + } + + CleanupTestDB(t) +} + /* Service level test for filtering user registrations in the db */ @@ -441,6 +517,15 @@ var user_registration_data string = ` } ` +var patch_registration_data string = ` +{ + "id": "testid", + "firstName": "John", + "lastName": "Smith", + "email": "new_test@gmail.com" +} +` + var mentor_registration_data string = ` { "id": "testid",