Commit 141ef533 authored by asmundh's avatar asmundh
Browse files

Initial commit

parents
GROUPID=90
DOMAIN=localhost
URL_PREFIX=http://
PORT_PREFIX=90
DJANGO_SUPERUSER_PASSWORD=Password
DJANGO_SUPERUSER_USERNAME=admin
DJANGO_SUPERUSER_EMAIL=admin@mail.com
backend/secfit/.vscode/
backend/secfit/*/migrations/__pycache__/
backend/secfit/*/__pycache__/
backend/secfit/db.sqlite3
# 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
# SecFit
SecFit (Secure Fitness) is a hybrid mobile application for fitness logging.
## Deploy with Docker
### Prerequisites:
Docker
Git
Windows hosts must use Education or more advanced versions to run Docker \
Download: https://innsida.ntnu.no/wiki/-/wiki/English/Microsoft+Windows+10
### Install:
$ git clone https://gitlab.stud.idi.ntnu.no/kyleo/secfit.git \
$ cd secfit/
### Run:
$ docker-compose up --build \
Hosts the application on http://localhost:9090 with default settings
## Technology
- **deployment** Docker
- **web** Nginx
- **database** Postgre SQL
- **backend** Django 3 with Django REST framework
- **application**
- **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
requirements.txt - Python requirements
package.json - Some node.js requirements, this is needed for cordova
- **secfit/** django project folder containing the project modules
- **<application_name>/** - generic structure of a django application
- **admins.py** - file contaning definitions to connect models to the django admin panel
- **urls.py** - contains mapping between urls and views
- **models.py** - contains data models
- **permissions.py** - contains custom permissions that govern access
- **serializers.py** - contains serializer definitions for sending data between backend and frontend
- **parsers.py** - contains custom parsers for parsing the body of HTTP requests
- **tests/** - contains tests for the module. [View Testing in Django](https://docs.djangoproject.com/en/2.1/topics/testing/) for more.
- **views.py** - Controller in MVC. Methods for rendering and accepting user data
- **forms.py** - definitions of forms. Used to render html forms and verify user input
- **settings.py** - Contains important settings at the application and/or project level
- **Procfile** - Procfile for backend heroku deployment
- **media/** - directory for file uploads (need to commit it for heroku)
- **comments/** - application handling user comments and reactions
- **secfit/** - The projects main module containing project-level settings.
- **users/** - application handling users and requests
- **workouts/** - application handling exercises and workouts
- **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
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
#### 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:
`python manage.py createsuperuser`
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.
Add some categories and you should be all set.
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`
- For browser, do `cordova run browser`
It's possible you will need to add the platforms you want to run and build.
The following documentation can be used to run the application in an Android emulator: \
https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html
# Use the official Python image from the Docker Hub
FROM python:3.8.5-slim
# These two environment variables prevent __pycache__/ files.
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
# Make a new directory to put our code in.
RUN mkdir /code
# Change the working directory.
# Every command after this will be run from the /code directory.
WORKDIR /code
# Copy the of the code.
COPY . /code/
# Add sqlite3 to enable dbshell command for managing the database
RUN apt-get update -y && apt-get install sqlite3 -y
# Upgrade pip
RUN pip install --upgrade pip
# Install the requirements.
RUN pip install -r requirements.txt
# Import groupid environment variable
ENV GROUPID=${GROUPID}
# Initialize Django
RUN python manage.py migrate
# Import credential variables
ARG DJANGO_SUPERUSER_USERNAME
ARG DJANGO_SUPERUSER_PASSWORD
ARG DJANGO_SUPERUSER_EMAIL
# Create superuser
RUN DJANGO_SUPERUSER_USERNAME=${DJANGO_SUPERUSER_USERNAME} \
DJANGO_SUPERUSER_PASSWORD=${DJANGO_SUPERUSER_PASSWORD} \
DJANGO_SUPERUSER_EMAIL=${DJANGO_SUPERUSER_EMAIL} \
python manage.py createsuperuser --noinput || \
echo "WARNING: This error is ignored as it most likely is 'That username is already taken.'" \
&& 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 wsgi server with gunicorn
CMD ["gunicorn", "secfit.wsgi", "--log-file", "-", "-b", "0.0.0.0:8000"]
from django.contrib import admin
# Register your models here.
from .models import Comment
admin.site.register(Comment)
from django.apps import AppConfig
class CommentsConfig(AppConfig):
name = "comments"
# Generated by Django 3.1 on 2020-08-21 03:42
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
("workouts", "__first__"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Comment",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("content", models.TextField()),
("timestamp", models.DateTimeField(auto_now_add=True)),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="comments",
to=settings.AUTH_USER_MODEL,
),
),
(
"workout",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="comments",
to="workouts.workout",
),
),
],
options={
"ordering": ["-timestamp"],
},
),
migrations.CreateModel(
name="Like",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("timestamp", models.DateTimeField(auto_now_add=True)),
(
"comment",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="likes",
to="comments.comment",
),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="likes",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
from django.db import models
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.db import models
from django.contrib.auth import get_user_model
from workouts.models import Workout
# Create your models here.
class Comment(models.Model):
"""Django model for a comment left on a workout.
Attributes:
owner: Who posted the comment
workout: The workout this comment was left on.
content: The content of the comment.
timestamp: When the comment was created.
"""
owner = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="comments"
)
workout = models.ForeignKey(
Workout, on_delete=models.CASCADE, related_name="comments"
)
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-timestamp"]
class Like(models.Model):
"""Django model for a reaction to a comment.
Attributes:
owner: Who liked the comment
comment: The comment that was liked
timestamp: When the like occurred.
"""
owner = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="likes"
)
comment = models.ForeignKey(Comment, on_delete=models.CASCADE, related_name="likes")
timestamp = models.DateTimeField(auto_now_add=True)
from rest_framework import permissions
class IsCommentVisibleToUser(permissions.BasePermission):
"""
Custom permission to only allow a comment to be viewed
if one of the following holds:
- The comment is on a public visibility workout
- The comment was written by the user
- The comment is on a coach visibility workout and the user is the workout owner's coach
- The comment is on a workout owned by the user
"""
def has_object_permission(self, request, view, obj):
# Write permissions are only allowed to the owner.
return (
obj.workout.visibility == "PU"
or obj.owner == request.user
or (obj.workout.visibility == "CO" and obj.owner.coach == request.user)
or obj.workout.owner == request.user
)
from rest_framework import serializers
from rest_framework.serializers import HyperlinkedRelatedField
from comments.models import Comment, Like
from workouts.models import Workout
class CommentSerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.ReadOnlyField(source="owner.username")
workout = HyperlinkedRelatedField(
queryset=Workout.objects.all(), view_name="workout-detail"
)
class Meta:
model = Comment
fields = ["url", "id", "owner", "workout", "content", "timestamp"]
class LikeSerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.ReadOnlyField(source="owner.username")
comment = HyperlinkedRelatedField(
queryset=Comment.objects.all(), view_name="comment-detail"
)
class Meta:
model = Like
fields = ["url", "id", "owner", "comment", "timestamp"]
from django.test import TestCase
# Create your tests here.
from django.urls import path, include
from comments.models import Comment, Like
from comments.views import CommentList, CommentDetail, LikeList, LikeDetail
from rest_framework.urlpatterns import format_suffix_patterns
urlpatterns = [
path("api/comments/", CommentList.as_view(), name="comment-list"),
path("api/comments/<int:pk>/", CommentDetail.as_view(), name="comment-detail"),
path("api/likes/", LikeList.as_view(), name="like-list"),
path("api/likes/<int:pk>/", LikeDetail.as_view(), name="like-detail"),
]
from django.shortcuts import render
from rest_framework import generics, mixins
from comments.models import Comment, Like
from rest_framework import permissions
from comments.permissions import IsCommentVisibleToUser
from workouts.permissions import IsOwner, IsReadOnly
from comments.serializers import CommentSerializer, LikeSerializer
from django.db.models import Q
from rest_framework.filters import OrderingFilter
# Create your views here.
class CommentList(
mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView
):
# queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [OrderingFilter]
ordering_fields = ["timestamp"]
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def get_queryset(self):
workout_pk = self.kwargs.get("pk")
qs = Comment.objects.none()
if workout_pk:
qs = Comment.objects.filter(workout=workout_pk)
elif self.request.user:
"""A comment should be visible to the requesting user if any of the following hold:
- The comment is on a public visibility workout
- The comment was written by the user
- The comment is on a coach visibility workout and the user is the workout owner's coach
- The comment is on a workout owned by the user
"""
# The code below is kind of duplicate of the one in ./permissions.py
# We should replace it with a better solution.
# Or maybe not.
qs = Comment.objects.filter(
Q(workout__visibility="PU")
| Q(owner=self.request.user)
| (
Q(workout__visibility="CO")
& Q(workout__owner__coach=self.request.user)
)
| Q(workout__owner=self.request.user)
).distinct()
return qs
# Details of comment
class CommentDetail(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView,
):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = [
permissions.IsAuthenticated & IsCommentVisibleToUser & (IsOwner | IsReadOnly)
]
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
# List of likes
class LikeList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView):
serializer_class = LikeSerializer
permission_classes = [permissions.IsAuthenticated]
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def get_queryset(self):
return Like.objects.filter(owner=self.request.user)
# Details of like
class LikeDetail(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView,
):
queryset = Like.objects.all()
serializer_class = LikeSerializer
permission_classes = [permissions.IsAuthenticated]
_Detail = []
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "secfit.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
python-3.8.6
\ No newline at end of file
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