Commit 24e4b7c0 authored by Mathias Lund Ahrn's avatar Mathias Lund Ahrn
Browse files

Merge branch 'testing' into 'master'

Github version to gitlab

See merge request !1
parents cde19659 b1043400
Pipeline #113891 passed with stages
in 2 minutes and 23 seconds
name: CI/CD
on:
push:
branches: [ master ]
workflow_dispatch:
jobs:
build_backend:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
-
name: Build and push backend
id: docker_build_backend
uses: docker/build-push-action@v2
env:
GROUPID: ${{ secrets.GROUPID }}
DJANGO_SUPERUSER_USERNAME: ${{ secrets.DJANGO_SUPERUSER_USERNAME }}
DJANGO_SUPERUSER_PASSWORD: ${{ secrets.DJANGO_SUPERUSER_PASSWORD }}
DJANGO_SUPERUSER_EMAIL: ${{ secrets.DJANGO_SUPERUSER_EMAIL }}
with:
context: ./backend/secfit
push: true
tags: jonev/secfit:backend
-
name: Backend Image digest
run: echo ${{ steps.docker_build_backend.outputs.digest }}
build_frontend:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
-
name: Build and push frontend
id: docker_build_frontend
uses: docker/build-push-action@v2
env:
GROUPID: ${{ secrets.GROUPID }}
DOMAIN: ${{ secrets.DOMAIN }}
URL_PREFIX: ${{ secrets.URL_PREFIX }}
PORT_PREFIX: ${{ secrets.PORT_PREFIX }}
with:
context: ./frontend
push: true
tags: jonev/secfit:frontend
-
name: Frontend Image digest
run: echo ${{ steps.docker_build_frontend.outputs.digest }}
deploy:
needs: [build_backend, build_frontend]
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Deploy to Docker swarm
uses: appleboy/ssh-action@v0.1.4
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
key: ${{ secrets.DEPLOY_KEY_PRIVATE }}
script: /home/${{ secrets.DEPLOY_USERNAME }}/secfit/update
# This is a basic workflow to help you get started with Actions
name: SSH Deploy Test
# Controls when the action will run.
on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
deploy:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: SSH Remote Commands
uses: appleboy/ssh-action@v0.1.4
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USERNAME }}
key: ${{ secrets.DEPLOY_KEY_PRIVATE }}
script: /home/${{ secrets.DEPLOY_USERNAME }}/secfit/update
name: TEST
on:
push:
branches-ignore:
- master
pull_request:
branches:
- '**'
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Build backend
id: docker_test_backend
uses: docker/build-push-action@v2
env:
GROUPID: ${{ secrets.GROUPID }}
DJANGO_SUPERUSER_USERNAME: ${{ secrets.DJANGO_SUPERUSER_USERNAME }}
DJANGO_SUPERUSER_PASSWORD: ${{ secrets.DJANGO_SUPERUSER_PASSWORD }}
DJANGO_SUPERUSER_EMAIL: ${{ secrets.DJANGO_SUPERUSER_EMAIL }}
with:
context: ./backend/secfit
push: false
tags: jonev/secfit:backend-test
-
name: Backend Image digest
run: echo ${{ steps.docker_build_backend.outputs.digest }}
-
name: Test backend
uses: addnab/docker-run-action@v2
with:
image: jonev/secfit:backend-test
run: echo "hello world"
......@@ -2,3 +2,4 @@ backend/secfit/.vscode/
backend/secfit/*/migrations/__pycache__/
backend/secfit/*/__pycache__/
backend/secfit/db.sqlite3
backend/secfit/media
\ No newline at end of file
# Webserver running nginx
FROM nginx:perl
# Import groupid environment variable
ENV GROUPID=${GROUPID}
ENV PORT_PREFIX=${PORT_PREFIX}
# Copy nginx config to the container
COPY nginx.conf /etc/nginx/nginx.conf
\ No newline at end of file
......@@ -2,39 +2,69 @@
SecFit (Secure Fitness) is a hybrid mobile application for fitness logging.
## Deploy with Docker
## Development in VS code remote container
Prerequisites:
- VS code with remote development extension
- Docker
To develop in remote containers open a separate VS code instance inside both backend and frontend. Use VS code's command (Ctrl + P) ">Remote-Containers: Open Folder In Container..."
Benefits using this development environment:
- Development environment is identical to the production environment (uses the same dockerfile). This leads to fewer surprises when deploying.
- No manual installation steps needed
- No cleanup
- Easy to switch between different projects using different dependencies/technologies
## Deploy (Production) with Docker Compose/Swarm
### Prerequisites:
Docker
Docker Compose/Swarm
### Deploy:
Git
- The deployment uses prebuilt images from docker hub, built with github actions
Windows hosts must use Education or more advanced versions to run Docker \
Download: https://innsida.ntnu.no/wiki/-/wiki/English/Microsoft+Windows+10
### Run with Docker Compose:
### Install:
```
docker-compose up
```
$ git clone https://gitlab.stud.idi.ntnu.no/kyleo/secfit.git \
$ cd secfit/
Hosts the application on http://localhost:4011
### Run:
### Run (or update) with Docker Swarm
$ docker-compose up --build \
Hosts the application on http://localhost:9090 with default settings
Prerequisites:
- Git clone, or copy `db.sqlite3`, `docker-compose.yaml` and `nginx.conf` to the server.
```
docker stack deploy --compose-file docker-compose.yml --with-registry-auth stack-secfit
```
Hosts the application on http://localhost:4011 with default settings
## CI/CD Pipeline
### Master branch
![Pipeline master](./documentation/Pipeline_Master.png "Pipeline Master")
## Technology
- **deployment** Docker
- **web** Nginx
- **database** Postgre SQL
- **proxy** Nginx
- **database** SQLite
- **backend** Django 3 with Django REST framework
- **application**
- **frontend**
- **browser** - HTML5/CSS/JS, Bootstrap v5 (no jQuery dependency)
- **mobile** Apache Cordova (uses same website)
- **authentication** JWT
## Code and structure
.gitlab-ci.yml - gitlab ci
......@@ -62,35 +92,24 @@ package.json - Some node.js requirements, this is needed for cordova
- **manage.py** - entry point for running the project.
- **seed.json** - contains seed data for the project to get it up and running quickly (coming soon)
## Local setup
It's recommended to have a look at: https://www.djangoproject.com/start/
Just as important is the Django REST guide: https://www.django-rest-framework.org/
Create a virtualenv https://docs.python-guide.org/dev/virtualenvs/
### Django
### Django basics
Installation with examples for Ubuntu. Windows and OSX is mostly the same
Fork the project and clone it to your machine.
#### Setup and activation of virtualenv (env that prevents python packages from being installed globaly on the machine)
Naviagate into the project folder, and create your own virtual environment
Naviagate into the project folder, and create your own virtual environment
#### Install python requirements
`pip install -r requirements.txt`
#### Migrate database
`python manage.py migrate`
#### Create superuser
Create a local admin user by entering the following command:
......@@ -99,12 +118,10 @@ Create a local admin user by entering the following command:
Only username and password is required
#### Start the app
`python manage.py runserver`
#### Add initial data
You can add initial data either by going to the url the app is running on locally and adding `/admin` to the url.
......@@ -116,8 +133,10 @@ Or by entering
`python manage.py loaddata seed.json`
### Cordova
Cordova CLI guide: https://cordova.apache.org/docs/en/latest/guide/cli/
If you want to run this as a mobile application
- Navigate to the frontend directory
- For android, do `cordova run android`
- For ios, do `cordova run ios`
......
{
"name": "Python-Django-Dev",
"build": {
"dockerfile": "../secfit/Dockerfile"
},
"extensions": ["ms-python.python"],
"forwardPorts": [8000],
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
},
"containerEnv": {
"DJANGO_SUPERUSER_USERNAME": "admin",
"DJANGO_SUPERUSER_PASSWORD": "Password",
"DJANGO_SUPERUSER_EMAIL": "admin@mail.com"
},
"postAttachCommand": "python secfit/manage.py migrate"
}
{
"python.linting.pylintEnabled": true,
"python.linting.enabled": true
}
\ No newline at end of file
To access Django application from terminal:
`python manage.py shell`
Accessing user objects
```python
from django.contrib.auth import get_user_model
# Retrieve all users
users = get_user_model().objects.all()
# Retrieve specific user
donald = get_user_model().objects.filter(username="donald")
# Print all values
donald.values()
```
\ No newline at end of file
# Development
Start development:
```bash
python secfit/manage.py runserver
```
/media
\ No newline at end of file
......@@ -44,7 +44,7 @@ RUN DJANGO_SUPERUSER_USERNAME=${DJANGO_SUPERUSER_USERNAME} \
&& echo "If you wish to alter the user credentials, then delete the user first."
# Create some exercises from seed data
RUN python manage.py loaddata seed.json
# RUN python manage.py loaddata seed.json
# Run wsgi server with gunicorn
CMD ["gunicorn", "secfit.wsgi", "--log-file", "-", "-b", "0.0.0.0:8000"]
......@@ -43,6 +43,7 @@ ALLOWED_HOSTS = [
"10." + groupid + ".0.6",
"10." + groupid + ".0.4",
"molde.idi.ntnu.no",
"secfit.vassbo.as",
"10.0.2.2",
# ADD HEROKU URL HERE
]
......@@ -61,6 +62,7 @@ INSTALLED_APPS = [
"users.apps.UsersConfig",
"comments.apps.CommentsConfig",
"corsheaders",
"django_extensions"
]
MIDDLEWARE = [
......
# Generated by Django 3.1 on 2021-03-05 23:08
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import workouts.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('workouts', '0003_rememberme'),
]
operations = [
migrations.AddField(
model_name='exercise',
name='owner',
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='exercises', to='users.user'),
preserve_default=False,
),
migrations.CreateModel(
name='ExerciseFile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to=workouts.models.exercise_directory_path)),
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='workouts.exercise')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercise_files', to=settings.AUTH_USER_MODEL)),
],
),
]
......@@ -82,6 +82,9 @@ class Exercise(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
unit = models.CharField(max_length=50)
owner = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="exercises"
)
def __str__(self):
return self.name
......@@ -139,6 +142,13 @@ class WorkoutFile(models.Model):
)
file = models.FileField(upload_to=workout_directory_path)
def exercise_directory_path(instance, filename):
return f"exercises/{instance.exercise.id}/{filename}"
class ExerciseFile(models.Model):
exercise = models.ForeignKey(Exercise, on_delete=models.CASCADE, related_name="files")
owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name="exercise_files")
file = models.FileField(upload_to=exercise_directory_path)
class RememberMe(models.Model):
"""Django model for an remember_me cookie used for remember me functionality.
......
......@@ -28,6 +28,22 @@ class IsOwnerOfWorkout(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.workout.owner == request.user
class IsOwnerOfExercise(permissions.BasePermission):
"""Checks whether the requesting user is also the owner of the new or existing object"""
def has_permission(self, request, view):
if request.method == "POST":
if request.data.get("exercise"):
exercise_id = request.data["exercise"].split("/")[-2]
exercise = exercise.objects.get(pk=exercise_id)
if exercise:
return exercise.owner == request.user
return False
return True
def has_object_permission(self, request, view, obj):
return obj.exercise.owner == request.user
class IsCoachAndVisibleToCoach(permissions.BasePermission):
"""Checks whether the requesting user is the existing object's owner's coach
......
......@@ -2,7 +2,7 @@
"""
from rest_framework import serializers
from rest_framework.serializers import HyperlinkedRelatedField
from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile, RememberMe
from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile, RememberMe, ExerciseFile
class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer):
......@@ -197,6 +197,27 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
"""
return obj.owner.username
class ExerciseFileSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for a ExerciseFile. Hyperlinks are used for relationships by default.
Serialized fields: url, id, owner, file, Exercise
Attributes:
owner: The owner (User) of the ExerciseFile, represented by a username. ReadOnly
Exercise: The associate Exercise for this ExerciseFile, represented by a hyperlink
"""
owner = serializers.ReadOnlyField(source="owner.username")
exercise = HyperlinkedRelatedField(
queryset=Exercise.objects.all(), view_name="exercise-detail", required=False
)
class Meta:
model = ExerciseFile
fields = ["url", "id", "owner", "file", "exercise"]
def create(self, validated_data):
return ExerciseFile.objects.create(**validated_data)
class ExerciseSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for an Exercise. Hyperlinks are used for relationships by default.
......@@ -206,15 +227,59 @@ class ExerciseSerializer(serializers.HyperlinkedModelSerializer):
Attributes:
instances: Associated exercise instances with this Exercise type. Hyperlinks.
"""
owner_username = serializers.SerializerMethodField()
instances = serializers.HyperlinkedRelatedField(
many=True, view_name="exerciseinstance-detail", read_only=True
)
files = ExerciseFileSerializer(many=True, required=False)
class Meta:
model = Exercise
fields = ["url", "id", "name", "description", "unit", "instances"]
fields = ["url", "id", "owner", "owner_username", "name", "description", "unit", "instances", "files"]
extra_kwargs = {"owner": {"read_only": True}}
def get_owner_username(self, obj):
return obj.owner.username
def create(self, validated_data):
files_data = []
if "files" in validated_data:
files_data = validated_data.pop("files")
exercise = Exercise.objects.create(**validated_data)
for file_data in files_data:
ExerciseFile.objects.create(
exercise=exercise, owner=exercise.owner, file=file_data.get("file")
)
return exercise
def update(self, instance, validated_data):
instance.name = validated_data.get("name", instance.name)
instance.description = validated_data.get("description", instance.description)
instance.unit = validated_data.get("unit", instance.unit)
instance.save()
if "files" in validated_data:
files_data = validated_data.pop("files")
files = instance.files
for file, file_data in zip(files.all(), files_data):
file.file = file_data.get("file", file.file)
# If new files have been added, creating new WorkoutFiles
if len(files_data) > len(files.all()):
for i in range(len(files.all()), len(files_data)):
ExerciseFile.objects.create(
exercise=instance,
owner=instance.owner,
file=files_data[i].get("file"),
)
# Else if files have been removed, delete WorkoutFiles
elif len(files_data) < len(files.all()):
for i in range(len(files_data), len(files.all())):
files.all()[i].delete()
return instance
class RememberMeSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for an RememberMe. Hyperlinks are used for relationships by default.
......
......@@ -22,6 +22,11 @@ urlpatterns = format_suffix_patterns(
views.ExerciseDetail.as_view(),
name="exercise-detail",
),
path(
"api/exercise-files/<int:pk>/",
views.ExerciseFileDetail.as_view(),
name="exercisefile-detail",
),
path(
"api/exercise-instances/",
views.ExerciseInstanceList.as_view(),
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment