From a98f06ba48d1cb10f88de5ed773a328f2007bbe4 Mon Sep 17 00:00:00 2001 From: Cristian-Adrian Frasineanu Date: Tue, 29 Nov 2016 14:53:29 +0200 Subject: [PATCH] Question model and repository. New views and logic for the main dashboard. --- app/Console.cpp | 1 + app/Model.cpp | 8 +- app/Model.h | 1 + app/ModelInterface.h | 1 + app/QuestionModel.cpp | 168 ++++++++++++++++++++++++++++ app/QuestionModel.h | 54 +++++++++ app/QuestionRepository.cpp | 89 +++++++++++++++ app/QuestionRepository.h | 27 +++++ app/UserModel.cpp | 8 +- app/UserModel.h | 3 +- app/View.cpp | 9 +- app/app.vcxproj | 4 + app/app.vcxproj.filters | 12 ++ views/questions/create.view | 14 +++ views/questions/find-or-ask.view | 14 +++ views/questions/search-results.view | 17 +++ 16 files changed, 415 insertions(+), 15 deletions(-) create mode 100644 app/QuestionModel.cpp create mode 100644 app/QuestionModel.h create mode 100644 app/QuestionRepository.cpp create mode 100644 app/QuestionRepository.h create mode 100644 views/questions/create.view create mode 100644 views/questions/find-or-ask.view create mode 100644 views/questions/search-results.view diff --git a/app/Console.cpp b/app/Console.cpp index feefb55..529fee3 100644 --- a/app/Console.cpp +++ b/app/Console.cpp @@ -281,6 +281,7 @@ Console::~Console() // Dump all the records... UserModel::dumpFile(); + QuestionModel::dumpFile(); // Revert to default terminal and display notice. clearScreen(); diff --git a/app/Model.cpp b/app/Model.cpp index 14ee6df..04cfa02 100644 --- a/app/Model.cpp +++ b/app/Model.cpp @@ -12,14 +12,10 @@ void Model::attachEntity(string &model) { this->repository = new UserRepository(); } - /*else if (model == "question") + else if (model == QuestionRepository::getAlias()) { - + this->repository = new QuestionRepository(); } - else if (model == "category") - { - - }*/ } Model::Model() diff --git a/app/Model.h b/app/Model.h index 0c1fd67..dc1548a 100644 --- a/app/Model.h +++ b/app/Model.h @@ -3,6 +3,7 @@ #include "Helpers.h" #include "RepositoryInterface.h" #include "UserRepository.h" +#include "QuestionRepository.h" using namespace std; diff --git a/app/ModelInterface.h b/app/ModelInterface.h index 7c312a2..3f80dea 100644 --- a/app/ModelInterface.h +++ b/app/ModelInterface.h @@ -19,6 +19,7 @@ class ModelInterface { public: virtual void save() = 0; virtual void setAttributes(map &) = 0; + virtual void markAs(const string &, int) = 0; virtual ~ModelInterface(); }; \ No newline at end of file diff --git a/app/QuestionModel.cpp b/app/QuestionModel.cpp new file mode 100644 index 0000000..e56577a --- /dev/null +++ b/app/QuestionModel.cpp @@ -0,0 +1,168 @@ +#include "QuestionModel.h" + +Question QuestionModel::setAfterUserId(int userId) +{ + this->io.seekg(0, this->io.beg); + + while (this->io.read(reinterpret_cast(&this->question), sizeof(question))) + { + if (this->question.user_id == userId) + { + return this->question; + } + } + + throw invalid_argument("Question not found!"); +} + +Question QuestionModel::setAfterId(int id) +{ + this->io.seekg((id - 1) * sizeof(Question), this->io.beg); + this->io.read(reinterpret_cast(&this->question), sizeof(Question)); + + return this->question; +} + +bool QuestionModel::questionTitleExists(string &title) +{ + Question question; + + this->io.seekg(0, this->io.beg); + + while (this->io.read(reinterpret_cast(&question), sizeof(Question))) + { + if (question.title == title) + { + return true; + } + } + + return false; +} + +void QuestionModel::markAnswered(int id) +{ + this->setAfterId(id); + + this->question.hasAnswer = true; + + this->save(); +} + +void QuestionModel::markAs(const string &status, int id) +{ + this->setAfterId(id); + + this->question.active = (status == "active") ? true : false; + + this->save(); +} + +// Serialize the object. +void QuestionModel::save() +{ + this->io.seekp((this->question.id - 1) * sizeof(Question), this->io.beg); + + this->io.clear(); + + if (!this->io.write(reinterpret_cast(&this->question), sizeof(Question))) + { + throw system_error(error_code(3, generic_category()), "Failed persisting data to file!"); + } +} + +void QuestionModel::setAttributes(map &cleanInputs) +{ + for (map::iterator it = cleanInputs.begin(); it != cleanInputs.end(); it++) + { + if (isInVector(this->protectedAttributes, it->first)) + { + continue; + } + else if (it->first == "title") + { + strcpy(this->question.title, it->second.c_str()); + } + else if (it->first == "body") + { + strcpy(this->question.body, it->second.c_str()); + } + } + + // If there's a new user, assign created_at with the current date. + if (cleanInputs.find("action")->second == "create") + { + time_t t = time(nullptr); + strftime(this->question.created_at, sizeof(this->question.created_at), "%c", localtime(&t)); + } + + this->question.id = ++this->lastId; +} + +void QuestionModel::dumpFile() +{ + Question question; + + ifstream db(QuestionModel::pathToFile, ios::in | ios::binary); + ofstream dump((QuestionModel::pathToFile.substr(0, QuestionModel::pathToFile.find(".store")).append(".txt")), ios::out | ios::trunc); + + if (db.is_open() && dump.is_open()) + { + db.seekg(0, db.beg); + + while (db.read(reinterpret_cast(&question), sizeof(Question))) + { + dump << question.id << endl + << question.user_id << endl + << question.category_id << endl + << question.title << endl + << question.body << endl + << question.created_at << endl + << question.deleted_at << endl + << question.hasAnswer << endl; + } + + db.close(); + dump.close(); + } +} + +QuestionModel::~QuestionModel() +{ + this->io.close(); +} + +string QuestionModel::pathToFile = "..\\database\\questions.store"; +void QuestionModel::openIOStream() +{ + this->io.open(QuestionModel::pathToFile, ios::in | ios::out | ios::binary); + this->io.seekp(0, this->io.end); + this->fileSize = this->io.tellp(); + + if (!this->io.is_open()) + { + toast(string("Couldn't open the file stream!"), string("error")); + } +} + +void QuestionModel::setLastId() +{ + if (this->fileSize == 0) + { + this->lastId = 0; + } + else + { + this->io.seekg((this->fileSize / sizeof(Question) - 1) * sizeof(Question), this->io.beg); + this->io.read(reinterpret_cast(&this->question), sizeof(Question)); + + this->lastId = this->question.id; + } +} + +QuestionModel::QuestionModel() +{ + this->openIOStream(); + this->protectedAttributes = { "id", "user_id", "category_id", "created_at", "deleted_at", "votes", "hasAnswer" }; + this->setLastId(); +} diff --git a/app/QuestionModel.h b/app/QuestionModel.h new file mode 100644 index 0000000..4f1f9f4 --- /dev/null +++ b/app/QuestionModel.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +#include "ModelInterface.h" + +using namespace std; + +typedef struct { + int id; + int user_id; + int category_id = 1; + + unsigned votes = 0; + + char title[255] = ""; + char body[1023] = ""; + char created_at[50] = ""; + + // Soft deletes + char deleted_at[50] = ""; + bool hasAnswer = 0; + bool active = 0; +} Question; + +class QuestionModel : public ModelInterface { +private: + static string pathToFile; + + Question question; + + void openIOStream(); + void setLastId(); + + long long fileSize; + int lastId; +public: + static void dumpFile(); + QuestionModel(); + + Question setAfterUserId(int); + Question setAfterId(int); + + bool questionTitleExists(string &); + void markAnswered(int); + void markAs(const string &, int); + void save(); + void setAttributes(map &); + + // Set the active to false on logout + ~QuestionModel(); +}; \ No newline at end of file diff --git a/app/QuestionRepository.cpp b/app/QuestionRepository.cpp new file mode 100644 index 0000000..87fe002 --- /dev/null +++ b/app/QuestionRepository.cpp @@ -0,0 +1,89 @@ +#include "QuestionRepository.h" +#include "Controller.h" + +string QuestionRepository::alias = "question"; + +string &QuestionRepository::getAlias() +{ + return QuestionRepository::alias; +} + +QuestionRepository::QuestionRepository() +{ + this->defineValidation(); +} + +void QuestionRepository::defineValidation() +{ + // Stand back, I am going to try Regex! + this->ValidationRules = { + { "title", "^.*$" }, + { "body", "^.*$" }, + { "keyword", "^.*$" } + }; + + this->ValidationErrors = { + { "title", "..." }, + { "body", "..." }, + { "keyword", "..." } + }; + + Controller::pushError(string("")); +} + +void QuestionRepository::receiveCleanInput(map &cleanInput) +{ + this->model.setAttributes(cleanInput); + + string action = cleanInput.find("action")->second; + + if (action == "search") + { + string keyword = cleanInput.find("keyword")->second; + + toast(string("Search!"), string("notice")); + } + else if (action == "create") + { + toast(string("Create!"), string("notice")); + } + else + { + Controller::pushError(string("Please provide a valid action!")); + } +} + +// Determine what to output via the model. +void QuestionRepository::echo(const string &alias) +{ + if (alias == "") + { + } +} + +void QuestionRepository::validateItems(map &truncatedInput) +{ + for (map::iterator it = truncatedInput.begin(); it != truncatedInput.end(); it++) + { + try + { + if (!regex_match(it->second.c_str(), regex(this->ValidationRules[it->first.c_str()]))) + { + Controller::pushError(this->ValidationErrors[it->first.c_str()]); + } + } + catch (const regex_error &e) + { + toast("\n\n" + string(e.what()) + "\n", string("error")); + } + } + + if (Controller::getErrorBag().empty()) + { + this->receiveCleanInput(truncatedInput); + } +} + +QuestionRepository::~QuestionRepository() +{ +} \ No newline at end of file diff --git a/app/QuestionRepository.h b/app/QuestionRepository.h new file mode 100644 index 0000000..110cdd4 --- /dev/null +++ b/app/QuestionRepository.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "RepositoryInterface.h" +#include "QuestionModel.h" + +using namespace std; + +class QuestionRepository : public RepositoryInterface { +private: + static string alias; + + QuestionModel model; + + void defineValidation(); + void receiveCleanInput(map &); +public: + static string &getAlias(); + + QuestionRepository(); + + void validateItems(map &); + void echo(const string &); + + ~QuestionRepository(); +}; \ No newline at end of file diff --git a/app/UserModel.cpp b/app/UserModel.cpp index 14bec4b..a5ba351 100644 --- a/app/UserModel.cpp +++ b/app/UserModel.cpp @@ -17,8 +17,6 @@ User UserModel::setAfterUser(string &username) } } - this->io.clear(); - throw invalid_argument("Username not found!"); } @@ -62,7 +60,7 @@ bool UserModel::userExists(string &username) return false; } -void UserModel::markAs(string &status, int id) +void UserModel::markAs(const string &status, int id) { this->setAfterId(id); @@ -188,6 +186,8 @@ void UserModel::setLastId() UserModel::UserModel() { this->openIOStream(); - this->protectedAttributes = { "created_at", "deleted_at", "role", "active", "banned" }; + + // Non mass-assignable fields. + this->protectedAttributes = { "id", "created_at", "deleted_at", "role", "active", "banned" }; this->setLastId(); } diff --git a/app/UserModel.h b/app/UserModel.h index 390c0cc..dcb2ede 100644 --- a/app/UserModel.h +++ b/app/UserModel.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -47,7 +46,7 @@ class UserModel : public ModelInterface { char *getFullName(); bool userExists(string &); - void markAs(string &, int); + void markAs(const string &, int); void save(); void setAttributes(map &); diff --git a/app/View.cpp b/app/View.cpp index 7d6adf3..b651726 100644 --- a/app/View.cpp +++ b/app/View.cpp @@ -53,7 +53,8 @@ void View::loadViewsOptions() faqView = "faq.view", helpView = "help.view", dashboardView = "dashboard.view", - logoutView = "logout.view"; + logoutView = "logout.view", + findOrAsk = "search-or-ask.view"; View::ViewsOptions = { { homeView, { { '1', "login.view" }, { '2', "signup.view" }, { '3', "browse-index.view" }, @@ -65,13 +66,15 @@ void View::loadViewsOptions() { signupView, { { 'c', "dashboard.view" } } }, - { dashboardView, { { '4', "logout.view" } } }, + { dashboardView, { { '1', "find-or-ask.view" }, { '4', "logout.view" } } }, { logoutView, { { 'y', "confirm" }, { 'b', "back" } } }, { faqView, { { 'q', "quit" }, { 'b', "back" } } }, - { helpView, { { 'q', "quit" }, { 'b', "back" } } } + { helpView, { { 'q', "quit" }, { 'b', "back" } } }, + + { findOrAsk, { { 'c', "search-results.view" } } } }; } diff --git a/app/app.vcxproj b/app/app.vcxproj index 38a7b65..da7d748 100644 --- a/app/app.vcxproj +++ b/app/app.vcxproj @@ -151,6 +151,8 @@ + + @@ -160,6 +162,8 @@ + + diff --git a/app/app.vcxproj.filters b/app/app.vcxproj.filters index d1bfd22..d37465f 100644 --- a/app/app.vcxproj.filters +++ b/app/app.vcxproj.filters @@ -45,6 +45,12 @@ Source Files + + Source Files + + + Source Files + @@ -74,5 +80,11 @@ Header Files + + Header Files + + + Header Files + \ No newline at end of file diff --git a/views/questions/create.view b/views/questions/create.view new file mode 100644 index 0000000..bcd85e9 --- /dev/null +++ b/views/questions/create.view @@ -0,0 +1,14 @@ + __ _ _ ___ _ ___ _ +/ _\ |_ __ _ ___| | __ / _ \ |_ _ ___ / _ \ |_ _ ___ +\ \| __/ _` |/ __| |/ // /_)/ | | | / __| / /_)/ | | | / __| +_\ \ || (_| | (__| @input-question-keyword +@action-seach + +=========================================================== +# (c) 2016 Cristian-Adrian Frasineanu | GNU GPLv3 licence # +=========================================================== diff --git a/views/questions/find-or-ask.view b/views/questions/find-or-ask.view new file mode 100644 index 0000000..bcd85e9 --- /dev/null +++ b/views/questions/find-or-ask.view @@ -0,0 +1,14 @@ + __ _ _ ___ _ ___ _ +/ _\ |_ __ _ ___| | __ / _ \ |_ _ ___ / _ \ |_ _ ___ +\ \| __/ _` |/ __| |/ // /_)/ | | | / __| / /_)/ | | | / __| +_\ \ || (_| | (__| @input-question-keyword +@action-seach + +=========================================================== +# (c) 2016 Cristian-Adrian Frasineanu | GNU GPLv3 licence # +=========================================================== diff --git a/views/questions/search-results.view b/views/questions/search-results.view new file mode 100644 index 0000000..26a7861 --- /dev/null +++ b/views/questions/search-results.view @@ -0,0 +1,17 @@ + __ _ _ ___ _ ___ _ +/ _\ |_ __ _ ___| | __ / _ \ |_ _ ___ / _ \ |_ _ ___ +\ \| __/ _` |/ __| |/ // /_)/ | | | / __| / /_)/ | | | / __| +_\ \ || (_| | (__|