diff --git a/.gitignore b/.gitignore index 9f28e5cb447949081556e27b2b1702613de93bbc..2dcb4d9f42f23b7af576b707bd66f202bed1e826 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ __pycache__ *.db */env *.coverage +htmlcov .pytest_cache \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 72270e125a8b1eb2597597a576cd83192122ca04..22158c9dda3cc8462343d3ce101686335a257da2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,7 +2,7 @@ "version": "2.0.0", "tasks": [ { - "label": "Client", + "label": "Start client", "type": "npm", "script": "start", "path": "client/", @@ -13,7 +13,26 @@ } }, { - "label": "Server", + "label": "Test client", + "type": "npm", + "script": "test:coverage:html", + "path": "client/", + "group": "test", + "problemMatcher": [], + }, + { + "label": "Open client coverage", + "type": "shell", + "group": "build", + "command": "start ./output/coverage/jest/index.html", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/client" + }, + + }, + { + "label": "Start server", "type": "shell", "group": "build", "command": "env/Scripts/python main.py", @@ -26,10 +45,20 @@ } }, { - "label": "Test Server", + "label": "Test server", + "type": "shell", + "group": "build", + "command": "env/Scripts/pytest.exe --cov-report html --cov app tests/", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/server" + }, + }, + { + "label": "Open server coverage", "type": "shell", "group": "build", - "command": "env/Scripts/pytest.exe --cov app tests/", + "command": "start ./htmlcov/index.html", "problemMatcher": [], "options": { "cwd": "${workspaceFolder}/server" @@ -37,11 +66,11 @@ }, { - "label": "Client + Server", + "label": "Start client and server", "group": "build", "dependsOn": [ - "Server", - "Client" + "Start server", + "Start client" ], "problemMatcher": [] } diff --git a/README.md b/README.md index 66ebcfb30963378191d2d4aaea096f5af3efa910..cd540e1f8c23ae68119a1b0a1d89bd7b82f5e316 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@   - # Scoring system for Teknikåttan This is the scoring system for Teknikåttan! @@ -13,6 +12,65 @@ To install the client and server needed to run the application, look in their re ## Using After installing both the client and the server, you are ready to run the application. -This is done in VSCode by pressing `ctrl+shift+b` and running the `Client + Server` task. +This is done in VSCode by pressing `ctrl+shift+b` and running the `Start client and server` task. The terminals for the client and server will now be seen on the right and left, respectively. -After making a change to either the client or the server while they are running, they will auto reload and you will see the changes immediately. +After making a change to either the client or the server while they are running, simply reload the page to see the changes immediately. + +## Workflow + +### Working on an issue + +This following steps describe how you choose an issue and create a branch and merge request from it. + +1. See all issues by going to `Issues->Boards`. +2. The issues no one has started on yet are showed in the `Open` tab. Choose one of these by dragging it into the `In progress` tab and opening it. +3. Add yourself as an asignee (in top right corner). +4. Add the current week as a milestone to the issue (to the right). +5. Press the little green downarrow on the right of the `Create merge request` button and select and press `Create branch`. +6. Open the project in VSCode. +7. Type `git pull`. This will fetch the new branch you just created and you should see it in the log (Example: `* [new branch] 5-add-login-api -> origin/5-add-login-api`) +8. Switch to it by running `git checkout <issue>-<name>`. (Example: `git checkout 5-add-login-api`) + +You are now ready to start working on your issue. + +### Creating a merge request + +When you have solved the issue and are ready to merge them into the `dev` will have to create a merge request. + +1. On GitLab open `Repository->Branches`. +2. Find your branch and press `Merge request`. + +You have now create a merge request for your branch. +The next step is to prepare your branch to be merged. + +1. Open the project in VSCode. +2. Checkout your branch, if you are not already on it (`git checkout <branch>`). +3. Run `git pull origin dev`. This will try to merge the latest changes from `dev` into your branch. This can have a few different results: + - There will be no changes, which is fine. + - There will be no conflicting changes, which is also fine. + - There will be conflicting changes, in which case you will need to merge it manually (see Merge conflicts) before continuing to the next step. +4. Run `git push`. +5. Go to GitLab and press `Merge Requests`, open your merge request and press the green `Mark as ready` button (in the top righ corner). + +The test will then run on your changes in the merge request on GitLab. +You will be allowed to merge once the pipelines have passed and another person has approved your merge request. +When this is done, simply press the `Merge` button. + +### Merge conflicts + +You will need to manually merge if there is a merge conflict between your branch and another. +This is simply done by opening the project in VSCode and going to the Git tab on the left (git symbol). +You will then see som files marked with `C`, which means that there are conflicts in these files. +Open them one by one and choose if you want to keep incoming changes (from `dev`), current changes (from your branch) or both. +The only thing you really need to do is removing the `<<<`, `===` and `>>>` symbols from the document, although you don't have to do it by hand. +A merge typically looks like the following picture in plain (try opening this in VSCode and see how it looks). +Simply solve all the merge conflicts in every file, run the tests to make sure it still works. +When you are done, simply commit and push your changes. + +``` +<<<<<<< file.txt +<Your changes> +======= +<Changes from dev> +>>>>>>> 123456789:file.txt +``` diff --git a/client/.gitignore b/client/.gitignore index 4d29575de80483b005c29bfcac5061cd2f45313e..ef85236eb5861796d67cb5a15157d7e525e2989e 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +output diff --git a/client/README.md b/client/README.md index da908d16ebb6009136be641e69e38cad9a7f0566..23cb20572c166b66b4e46f7ba58059ecab4fd66a 100644 --- a/client/README.md +++ b/client/README.md @@ -24,32 +24,33 @@ npm install ## Using After you have done every step described in setup, you are ready to start the client. -You can either start the client using tasks (recommended) or start it directly in the terminal. +To see the tasks described in the following steps, press `ctrl+shift+b`. -### Tasks +### Starting -You can run the client using Visual Studio Code tasks. -This is done by pressing `ctrl+shift+b` and running the `Client` task. +Start the server by running the `Start client` task. -### Terminal +### Testing + +Run the client tests running the `Test client` task. + +After it has finished, you can view a coverage report. +This is done by running the `Open client coverage` task. + +### Adding and removing new modules -You can also run the client directly from the terminal. All of the following snippets assume you are in the `client` folder. -Running the client: +Installing new module: ```bash -npm run start +npm install <module> ``` -Installing new modules: +Uninstalling module: ```bash -npm install new_module +npm uninstall <module> ``` Whenever a new module is installed, commited and pushed to git, everyone else needs to run `npm install` after pulling to install it as well. - -Author: Victor Löfgren - -Last updated: 11 February 2020 diff --git a/client/package.json b/client/package.json index 707cdc2d1ed3efa51cb8def43c5d8c0fb3471c21..2b67c10305c3bd117c493746990d291fceb5af56 100644 --- a/client/package.json +++ b/client/package.json @@ -4,7 +4,10 @@ "private": true, "dependencies": { "@material-ui/core": "^4.11.3", +<<<<<<< HEAD "@material-ui/icons": "^4.11.2", +======= +>>>>>>> 186d707ace54d27ff8913eff2be29026e64afb43 "@material-ui/lab": "^4.0.0-alpha.57", "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", @@ -45,7 +48,8 @@ "test": "react-scripts test", "eject": "react-scripts eject", "lint": "eslint \"./src/**/*.{js,ts,tsx}\"", - "test:coverage": "react-scripts test --coverage --coverageDirectory=output/coverage/jest" + "test:coverage": "react-scripts test --coverage --coverageDirectory=output/coverage/jest", + "test:coverage:html": "npm test -- --coverage --watchAll=false --coverageDirectory=output/coverage/jest" }, "browserslist": { "production": [ diff --git a/client/src/App.tsx b/client/src/App.tsx index 1e8c051e2d67e0a74f4ef26ab737a7427cf23b52..c26dce58152b871aa48b13f4c333f4fbe461e6ef 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,7 +9,19 @@ const App: React.FC = () => { rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> +<<<<<<< HEAD <Main /> +======= + <h1>Application</h1> + <TestConnection /> + <BrowserRouter> + <Switch> + <Route path="/"> + <LoginForm /> + </Route> + </Switch> + </BrowserRouter> +>>>>>>> 186d707ace54d27ff8913eff2be29026e64afb43 </div> ) } diff --git a/server/README.md b/server/README.md index b52b51c070844778fc4424670c8ab2ec6dc32a84..84d994d85e475930cd49d6a8adb0a6372289d302 100644 --- a/server/README.md +++ b/server/README.md @@ -11,7 +11,7 @@ You will need to do the following things to install the server: 3. Clone this repository if you haven't done so already. 4. Open the project folder in VSCode. 5. Open the integrated terminal by pressing `ctrl+ö`. -6. Type the following commands (or if you are on Windows, simply paste them) into your terminal: +6. Type the following commands into your terminal: ```bash # Install virtualenv package. You may need to open as administrator if you get @@ -29,12 +29,13 @@ py -m venv env # This step is different depending on your operating system. # Windows -# You migt to run the following before activating the virtual environment. Set-ExecutionPolicy Unrestricted -Scope Process ./env/Scripts/activate +# ===== # Linux/Mac -# source env/bin/activate +source env/bin/activate +# ===== # Install all the required packages into your virtual environment. pip install -r requirements.txt @@ -42,35 +43,40 @@ pip install -r requirements.txt ## Using -After you have done every step described in setup, you are ready to run the server. -You can either run the server using tasks (recommended) or run it directly in the terminal. +After you have done every step described in setup, you are ready to start the server. +To see the tasks described in the following steps, press `ctrl+shift+b`. -### Tasks +### Starting -You can run the server using Visual Studio Code tasks. -This is done by pressing `ctrl+shift+b` and running the `Server` task. +Start the server by running the `Start server` task. -### Terminal +### Testing -You can also run the server and tests directly from the terminal. -Before doing anything in the terminal, you need to activate the Python virtual environment (see Setup). -All of the following snippets assume you are in the `server` folder. +Run the client tests running the `Test server` task. -Running the server: +After it has finished, you can view a coverage report. +This is done by running the `Open server coverage` task. + +### Adding and removing new packages + +All of the following snippets assume you are in the `server` folder and have activated the virtual environment (see Setup). + +Installing new package: ```bash -python main.py +pip install <package> ``` -Running the tests: +Uninstalling package: ```bash -python test.py +pip uninstall <package> ``` -Adding new packages: +If you have added or removed a package from the repository, you will also have to run the following before commiting it to git: ```bash -pip install new_package pip freeze > requirements.txt ``` + +Whenever a new package is installed, commited and pushed to git, everyone else needs to run `pip install -r requirements.txt` after pulling to install it as well. diff --git a/server/app/api/users.py b/server/app/api/users.py index db7859f264c3af31d346b2b1f42cebd2f8d4574a..0d92ee52ca97406b99b98939bdc5bea54411c787 100644 --- a/server/app/api/users.py +++ b/server/app/api/users.py @@ -5,6 +5,7 @@ from app.api import api_blueprint from app.database.models import Blacklist, User from app.utils.validator import edit_user_schema, login_schema, register_schema, validateObject from flask import request +from flask.globals import session from flask_jwt_extended import ( create_access_token, create_refresh_token, diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index d755b84e4ed716e143e95b5b50503a52e98f04a9..4a1dc8b75fcd414adf91d242e8367c98d763db55 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -1,13 +1,10 @@ -from flask_sqlalchemy.model import Model import sqlalchemy as sa +from flask_sqlalchemy.model import Model from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.sql import func class Base(Model): - @declared_attr - def __tablename__(self): - return self.__class__.__name__.replace("Model", "s").lower() - + __abstract__ = True created = sa.Column(sa.DateTime(timezone=True), server_default=func.now()) updated = sa.Column(sa.DateTime(timezone=True), onupdate=func.now()) diff --git a/server/app/database/models.py b/server/app/database/models.py index bd486a144d28f46c358ab8979d274ed1b3cd83e7..5452ce3d7e5a1373af783c57dd2453ede892392c 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -1,22 +1,38 @@ -from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method +from app import bcrypt, db from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property -from app import bcrypt, db -from app.database import Base +STRING_SIZE = 254 class Blacklist(db.Model): id = db.Column(db.Integer, primary_key=True) jti = db.Column(db.String, unique=True, nullable=False) - @declared_attr - def __tablename__(self): - return "blacklist" - def __init__(self, jti): self.jti = jti +class Role(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + + users = db.relationship("User", backref="role") + + def __init__(self, name): + self.name = name + + +class City(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + + users = db.relationship("User", backref="city") + + def __init__(self, name): + self.name = name + + class User(db.Model): id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(254), unique=True, nullable=False) @@ -28,9 +44,16 @@ class User(db.Model): twoAuthConfirmed = db.Column(db.Boolean, default=True) # Change to false for Two factor authen twoAuthCode = db.Column(db.String(100), nullable=True) - def __init__(self, email, plaintext_password, name=""): + role_id = db.Column(db.Integer, db.ForeignKey("role.id"), nullable=True) # Change to false + city_id = db.Column(db.Integer, db.ForeignKey("city.id"), nullable=True) # Change to false + + media = db.relationship("Media", backref="upload_by") + + def __init__(self, email, plaintext_password, role_id=None, city_id=None, name=None): self._password = bcrypt.generate_password_hash(plaintext_password) self.email = email + self.role_id = role_id + self.city_id = city_id self.name = name self.authenticated = False @@ -48,3 +71,164 @@ class User(db.Model): @hybrid_method def is_correct_password(self, plaintext_password): return bcrypt.check_password_hash(self._password, plaintext_password) + + +class Media(db.Model): + id = db.Column(db.Integer, primary_key=True) + filename = db.Column(db.String(STRING_SIZE), unique=True) + type = db.Column(db.String(STRING_SIZE), nullable=False) + upload_by_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + + def __init__(self, filename, type, upload_by_id): + self.filename = filename + self.type = type + self.upload_by_id = upload_by_id + + +class Style(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + css = db.Column(db.Text, nullable=True) + bg_image_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=True) + + bg_image = db.relationship("Media", foreign_keys=[bg_image_id], uselist=False) + + def __init__(self, name, css=None, bg_image_id=None): + self.name = name + self.css = css + self.bg_image_id = bg_image_id + + +class Competition(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + style_id = db.Column(db.Integer, db.ForeignKey("style.id"), nullable=False) + city_id = db.Column(db.Integer, db.ForeignKey("city.id"), nullable=False) + + style = db.relationship("Style", foreign_keys=[style_id], uselist=False) + city = db.relationship("City", foreign_keys=[city_id], uselist=False) + + def __init__(self, name, style_id, city_id): + self.name = name + self.style_id = style_id + self.city_id = city_id + + +class Team(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False) + competition = db.relationship("Competition", foreign_keys=[competition_id], uselist=False) + + def __init__(self, name, competition_id): + self.name = name + self.competition_id = competition_id + + +class Slide(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + order = db.Column(db.Integer, nullable=False) + tweak_settings = db.Column(db.Text, nullable=True) + competition_id = db.Column(db.Integer, db.ForeignKey("competition.id"), nullable=False) + + competition = db.relationship("Competition", foreign_keys=[competition_id], uselist=False) + + def __init__(self, name, order, competition_id, tweak_settings=None): + self.name = name + self.order = order + self.competition_id = competition_id + self.tweak_settings = tweak_settings + + +class Question(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(STRING_SIZE), unique=True) + title = db.Column(db.String(STRING_SIZE), nullable=False) + timer = db.Column(db.Integer, nullable=False) + slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) + + slide = db.relationship("Slide", foreign_keys=[slide_id], uselist=False) + + def __init__(self, name, title, timer, slide_id): + self.name = name + self.title = title + self.timer = timer + self.slide_id = slide_id + + +class TrueFalseQuestion(db.Model): + id = db.Column(db.Integer, primary_key=True) + true_false = db.Column(db.Boolean, nullable=False, default=False) + question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) + + question = db.relationship("Question", foreign_keys=[question_id], uselist=False) + + def __init__(self, true_false, question_id): + self.true_false = true_false + self.question_id = question_id + + +class TextQuestion(db.Model): + id = db.Column(db.Integer, primary_key=True) + question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) + question = db.relationship("Question", foreign_keys=[question_id], uselist=False) + + def __init__(self, question_id): + self.question_id = question_id + + +class TextQuestionAlternative(db.Model): + id = db.Column(db.Integer, primary_key=True) + text = db.Column(db.String(STRING_SIZE), nullable=False) + text_question_id = db.Column(db.Integer, db.ForeignKey("text_question.id"), nullable=False) + text_question = db.relationship("TextQuestion", foreign_keys=[text_question_id], uselist=False) + + def __init__(self, text, text_question_id): + self.text = text + self.text_question_id = text_question_id + + +class MCQuestion(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(STRING_SIZE), nullable=False) + timer = db.Column(db.Integer, nullable=False) + question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) + + question = db.relationship("Question", foreign_keys=[question_id], uselist=False) + + def __init__(self, title, timer, slide_id): + self.title = title + self.timer = timer + self.slide_id = slide_id + + +class MCQuestionAlternative(db.Model): + id = db.Column(db.Integer, primary_key=True) + text = db.Column(db.String(STRING_SIZE), nullable=False) + true_false = db.Column(db.Boolean, nullable=False, default=False) + mc_id = db.Column(db.Integer, db.ForeignKey("mc_question.id"), nullable=False) + + mc = db.relationship("MCQuestion", foreign_keys=[mc_id], uselist=False) + + def __init__(self, text, true_false, mc_id): + self.text = text + self.true_false = true_false + self.mc_id = mc_id + + +class AnsweredQuestion(db.Model): + id = db.Column(db.Integer, primary_key=True) + data = db.Column(db.Text, nullable=False) + score = db.Column(db.Integer, nullable=False) + question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) + team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=False) + + question = db.relationship("Question", foreign_keys=[question_id], uselist=False) + team = db.relationship("Team", foreign_keys=[team_id], uselist=False) + + def __init__(self, data, score, question_id, team_id): + self.data = data + self.score = score + self.question_id = question_id + self.team_id = team_id