diff --git a/.gitignore b/.gitignore index ffb3de3832416b870bf800ac19d59da9db49bf56..1d40e0537e5f4894bc7867676e6bfd9227d0c563 100644 --- a/.gitignore +++ b/.gitignore @@ -110,4 +110,10 @@ environment.ps1 environment.bat #PyCharm -.idea/ \ No newline at end of file +.idea/ + +#VSCode +.vscode/ + +#media +/media/* \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 19b1c23cf3699519e16058625d22789b55afeb6f..027b673d3aa9b66ab968a2c1feefa874519443b7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ before_script: - python -V # Print out python version for debugging # Uncomment next line if your Django app needs a JS runtime: # - apt-get update -q && apt-get install nodejs -yqq - - pip install -r requirements/development.txt + - pip install -r requirements/production.txt # To get Django tests to work you may need to create a settings file using # the following DATABASES: # diff --git a/.travis.yml b/.travis.yml index af5ebcc2a6397f7ada705a69cd12a91e2f4267a6..75c6e5b0c8f2ea94269adadf1c8c8da18cf74673 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,18 @@ language: python python: - - "3.4" - "3.5" - "3.5-dev" # 3.5 development branch - "3.6" - "3.6-dev" # 3.6 development branch - # - "3.7-dev" # 3.7 development branch + - "3.7" + - "3.7-dev" # 3.7 development branch # - "nightly" services: - postgresql # command to install dependencies install: - pip install -r requirements/development.txt - + before_script: - psql -c 'create database travis_ci_test;' -U postgres env: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..0dd2d90d20d02e6e18f3773fa43b4feeea582145 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# pull official base image +FROM python:3.8.1 + +# set work directory +WORKDIR /usr/src/app + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install dependencies +RUN apt-get -y update +RUN apt-get install -y python python-pip python-dev python-django-extensions postgresql-client netcat +RUN pip install --upgrade pip +COPY ./requirements/production.txt /usr/src/app/requirements.txt +RUN pip install -r requirements.txt + +RUN apt-get -y update && apt-get -y autoremove + +# copy entrypoint.sh +COPY ./src/entrypoint.sh /usr/src/app/entrypoint.sh + +# copy project +COPY ./src /usr/src/app/ + +# run entrypoint.sh +ENTRYPOINT ["/usr/src/app/entrypoint.sh"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000000000000000000000000000000000000..030717cdd793a0a7367cade822888bf852029aff --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,27 @@ +FROM python:3.8.1 + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 +ENV TZ Europe/Budapest + +ENV HOME /home/app +ENV APP_HOME /home/app/kszkepzes-backend +RUN groupadd app && useradd -g app app && \ +mkdir -p /home/app && mkdir $APP_HOME && \ +mkdir $APP_HOME/staticfiles && mkdir $APP_HOME/mediafiles +WORKDIR $APP_HOME + +COPY ./requirements/production.txt requirements.txt + +RUN apt-get -y update && apt-get install -y python python-pip python-dev python-django-extensions postgresql-client netcat +RUN pip install -r requirements.txt + +COPY ./src $APP_HOME + +RUN chown -R app:app $APP_HOME + +USER app + +EXPOSE 8000 + +CMD ["gunicorn", "kszkepzes.wsgi:application", "--bind", "0.0.0.0:8000"] diff --git a/README.md b/README.md index 3084bb5c17ad754726f57ea7e286e5ae7a98e235..b3d8e230e67c037c729c2f6d1d5d824b8247d4aa 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,15 @@ 1. python3 -m venv venv 2. source venv/bin/activate 3. cp environment.sh.example environment.sh -4. Ki kell tölteni a környezeti változókat. +4. Ki kell tölteni a környezeti változókat. +Az authsch-s adatokat az https://auth.sch.bme.hu/ fejlesztői konzol menüpontja alatt lehet legenerálni új kliens hozzáadásával. +Atirányítási cím: `http://127.0.0.1:3000/api/v1/complete/authsch` +```shell script + export SECRET_KEY=<Ide bármi kerülhet> + export AUTHSCH_KEY=<authsch-s Kliens azonosító> + export AUTHSCH_SECRET=<authsch-s Kliens kulcs> + export MEDIA_ROOT=static/ + ``` 5. source environment.sh 6. pip install -r requirements/development.txt 7. python3 src/manage.py runserver diff --git a/requirements/base.in b/requirements/base.in index 84ce1c98213e06963da8ab3b722b9c5c39f83d2a..e319619d85d0f23b7fee16d86e0c3da1a0f0d4c1 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -1,5 +1,5 @@ -Django==2.0.1 -djangorestframework==3.7.7 +Django==2.2.4 +djangorestframework==3.10.2 django-social-authsch==0.1 django-solo==1.1.3 -django-import-export==1.0.0 +django-import-export==1.2.0 diff --git a/requirements/development.in b/requirements/development.in deleted file mode 100644 index d80fc6250c3504682e90f62fdf9abfed415f824c..0000000000000000000000000000000000000000 --- a/requirements/development.in +++ /dev/null @@ -1,5 +0,0 @@ --r base.in -flake8==3.4.1 -pip-tools==1.9.0 -django-extensions==1.9.9 -python-language-server==0.13.0 diff --git a/requirements/development.txt b/requirements/development.txt deleted file mode 100644 index 91118f565509bfebb4e633fd2f19702300e03c0c..0000000000000000000000000000000000000000 --- a/requirements/development.txt +++ /dev/null @@ -1,55 +0,0 @@ -# -# This file is autogenerated by pip-compile -# To update, run: -# -# pip-compile --output-file development.txt development.in -# -certifi==2017.7.27.1 # via requests -chardet==3.0.4 # via requests -click==6.7 # via pip-tools -configparser==3.5.0 # via python-language-server -defusedxml==0.5.0 # via python3-openid, social-auth-core -diff-match-patch==20121119 # via django-import-export -django-extensions==1.9.9 -django-import-export==1.0.0 -django-social-authsch==0.1 -django-solo==1.1.3 -django>=2.0.10 -djangorestframework==3.7.7 -et-xmlfile==1.0.1 # via openpyxl -first==2.0.1 # via pip-tools -flake8==3.4.1 -future==0.16.0 # via python-language-server -idna==2.6 # via requests -jdcal==1.3 # via openpyxl -jedi==0.11.1 # via python-language-server -json-rpc==1.10.8 # via python-language-server -mccabe==0.6.1 # via flake8, python-language-server -oauthlib==2.0.6 # via requests-oauthlib, social-auth-core -odfpy==1.3.6 # via tablib -openpyxl==2.5.0 # via tablib -parso==0.1.1 # via jedi -pip-tools==1.9.0 -pluggy==0.6.0 # via python-language-server -pycodestyle==2.3.1 # via flake8, python-language-server -pydocstyle==2.1.1 # via python-language-server -pyflakes==1.5.0 # via flake8, python-language-server -pyjwt==1.5.3 # via social-auth-core -python-language-server==0.13.0 -python3-openid==3.1.0 # via social-auth-core -pytz==2017.2 # via django -pyyaml>=4.2b1 # via tablib -requests-oauthlib==0.8.0 # via social-auth-core -requests>=2.20.0 # via requests-oauthlib, social-auth-core -rope>=0.11.0 # via python-language-server -six==1.10.0 # via django-extensions, pip-tools, pydocstyle, social-auth-app-django, social-auth-core -snowballstemmer==1.2.1 # via pydocstyle -social-auth-app-django==2.0.0 # via django-social-authsch -social-auth-core==1.5.0 # via django-social-authsch, social-auth-app-django -tablib==0.12.1 # via django-import-export -typing==3.6.4 # via django-extensions -unicodecsv==0.14.1 # via tablib -urllib3>=1.23 # via requests -xlrd==1.1.0 # via tablib -xlwt==1.3.0 # via tablib -yapf==0.20.1 # via python-language-server diff --git a/requirements/production.in b/requirements/production.in index 00a73eea0fce88f6ffa4c793be6b1966a47d95b8..2ef446c78ccb7cf01c48ff5cee36a04d5f6f6abc 100644 --- a/requirements/production.in +++ b/requirements/production.in @@ -1,2 +1,11 @@ -r base.in +psycopg2-binary gunicorn==19.7.1 +flake8==3.7.8 +pip-tools==4.1.0 +django-extensions==2.2.1 +python-language-server==0.28.2 +drf-yasg==1.16.1 +packaging==19.1 +Pillow==7.0.0 +djangorestframework-api-key==1.4.1 \ No newline at end of file diff --git a/requirements/production.txt b/requirements/production.txt index d4403006c713efb94b3f60beeb43b6b2144d0cb3..7cdc80bd945fd1c0c126cfee9d379ae6c4c884f9 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -2,35 +2,69 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file production.txt production.in +# pip-compile --output-file=requirements/production.txt requirements/production.in # +attrs==19.3.0 # via packaging certifi==2018.1.18 # via requests chardet==3.0.4 # via requests +click==7.0 # via pip-tools +coreapi==2.3.3 # via drf-yasg +coreschema==0.0.4 # via coreapi, drf-yasg defusedxml==0.5.0 # via python3-openid, social-auth-core diff-match-patch==20121119 # via django-import-export -django-import-export==1.0.0 +django-extensions==2.2.1 +django-import-export==1.2.0 django-social-authsch==0.1 django-solo==1.1.3 -django>=2.0.10 -djangorestframework==3.7.7 +django==2.2.4 +djangorestframework-api-key==1.4.1 +djangorestframework==3.10.2 +drf-yasg==1.16.1 +entrypoints==0.3 # via flake8 et-xmlfile==1.0.1 # via openpyxl +flake8==3.7.8 +future==0.18.2 # via python-language-server gunicorn==19.7.1 idna==2.6 # via requests +importlib-metadata==1.5.0 # via pluggy +inflection==0.3.1 # via drf-yasg +itypes==1.1.0 # via coreapi jdcal==1.3 # via openpyxl +jedi==0.14.1 # via python-language-server +jinja2==2.11.1 # via coreschema +markupsafe==1.1.1 # via jinja2 +mccabe==0.6.1 # via flake8 oauthlib==2.0.6 # via requests-oauthlib, social-auth-core odfpy==1.3.6 # via tablib openpyxl==2.5.0 # via tablib +packaging==19.1 +parso==0.6.1 # via jedi +pillow==7.0.0 +pip-tools==4.1.0 +pluggy==0.13.1 # via python-language-server +psycopg2-binary==2.8.4 +pycodestyle==2.5.0 # via flake8 +pyflakes==2.1.1 # via flake8 pyjwt==1.5.3 # via social-auth-core +pyparsing==2.4.6 # via packaging +python-jsonrpc-server==0.3.4 # via python-language-server +python-language-server==0.28.2 python3-openid==3.1.0 # via social-auth-core pytz==2017.3 # via django -pyyaml>=4.2b1 # via tablib +pyyaml==5.3 # via tablib requests-oauthlib==0.8.0 # via social-auth-core -requests>=2.20.0 # via requests-oauthlib, social-auth-core -six==1.11.0 # via social-auth-app-django, social-auth-core +requests==2.22.0 # via coreapi, requests-oauthlib, social-auth-core +ruamel.yaml.clib==0.2.0 # via ruamel.yaml +ruamel.yaml==0.16.7 # via drf-yasg +six==1.11.0 # via django-extensions, drf-yasg, packaging, pip-tools, social-auth-app-django, social-auth-core social-auth-app-django==2.1.0 # via django-social-authsch social-auth-core==1.6.0 # via django-social-authsch, social-auth-app-django +sqlparse==0.3.0 # via django tablib==0.12.1 # via django-import-export +ujson==1.35 # via python-jsonrpc-server unicodecsv==0.14.1 # via tablib -urllib3>=1.23 # via requests +uritemplate==3.0.1 # via coreapi, drf-yasg +urllib3==1.25.8 # via requests xlrd==1.1.0 # via tablib xlwt==1.3.0 # via tablib +zipp==2.1.0 # via importlib-metadata diff --git a/src/account/migrations/0001_initial.py b/src/account/migrations/0001_initial.py index 8550f16222d06151f89e7dbf62fb44cc7233e753..10c1415fab7a219189d6dd4338d89ca2ba0c7d5d 100644 --- a/src/account/migrations/0001_initial.py +++ b/src/account/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.1 on 2019-01-17 15:06 +# Generated by Django 2.2.4 on 2020-02-04 14:16 from django.conf import settings from django.db import migrations, models @@ -19,6 +19,8 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('deadline', models.DateTimeField(null=True)), + ('messageBefore', models.TextField(blank=True, default='')), + ('messageAfter', models.TextField(blank=True, default='')), ], options={ 'abstract': False, @@ -42,7 +44,7 @@ class Migration(migrations.Migration): ('motivation_exercise', models.TextField(blank=True, default='')), ('nick', models.CharField(blank=True, default='', max_length=15)), ('signed', models.BooleanField(default=False)), - ('role', models.CharField(choices=[('Staff', 'Staff'), ('Applicant', 'Applicant'), ('Student', 'Student')], default='Applicant', max_length=10)), + ('role', models.CharField(choices=[('Staff', 'Staff'), ('Applicant', 'Applicant'), ('Student', 'Student'), ('Denied', 'Denied')], default='Applicant', max_length=10)), ('groups', models.ManyToManyField(blank=True, related_name='profiles', to='account.GroupChoice')), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), ], diff --git a/src/account/migrations/0002_auto_20190122_1341.py b/src/account/migrations/0002_auto_20190122_1341.py deleted file mode 100644 index e9ee7b662af95a01f09815050e8650ab04603b30..0000000000000000000000000000000000000000 --- a/src/account/migrations/0002_auto_20190122_1341.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.1 on 2019-01-22 12:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='profile', - name='role', - field=models.CharField(choices=[('Staff', 'Staff'), ('Applicant', 'Applicant'), ('Student', 'Student'), ('Denied', 'Denied')], default='Applicant', max_length=10), - ), - ] diff --git a/src/account/models.py b/src/account/models.py index 082a83c98de9be5d956af810734422ca9897c1c7..40a38470f6478f44b733c1f7c1b25f606db00141 100644 --- a/src/account/models.py +++ b/src/account/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.auth.models import User from solo.models import SingletonModel +from django.db.models import Sum class GroupChoice(models.Model): @@ -12,7 +13,11 @@ class GroupChoice(models.Model): ('HAT', 'Hallgatói Tudásbázis'), ('N', 'None'), ) - choice = models.CharField(max_length=10, choices=TEAMS, default='N', unique=True) + choice = models.CharField( + max_length=10, + choices=TEAMS, + default='N', + unique=True) def __str__(self): return self.choice @@ -32,15 +37,27 @@ class Profile(models.Model): related_name='profile', on_delete=models.CASCADE ) - # TODO: Change the default to json render side + motivation_about = models.TextField(blank=True, default='') motivation_profession = models.TextField(blank=True, default='') motivation_exercise = models.TextField(blank=True, default='') nick = models.CharField(max_length=15, blank=True, default='') signed = models.BooleanField(default=False, null=False) - groups = models.ManyToManyField(GroupChoice, related_name='profiles', blank=True) + groups = models.ManyToManyField( + GroupChoice, related_name='profiles', blank=True) role = models.CharField(max_length=10, choices=ROLES, default='Applicant') + @property + def events_visited(self): + return self.events_visitor.all().count() + + @property + def homework_bits(self): + return self.solution.filter(accepted=True) \ + .values('task__bits') \ + .aggregate(total_bits=Sum('task__bits')) \ + .get('total_bits') + @property def full_name(self): return self.user.get_full_name() @@ -51,3 +68,5 @@ class Profile(models.Model): class Deadline(SingletonModel): deadline = models.DateTimeField(null=True) + messageBefore = models.TextField(blank=True, default='') + messageAfter = models.TextField(blank=True, default='') diff --git a/src/account/resources.py b/src/account/resources.py index 4682f638b07cadfff0e0d216c394ce466d980866..1217cad331af7ec6fc291ce0417dc2a020284167 100644 --- a/src/account/resources.py +++ b/src/account/resources.py @@ -5,7 +5,8 @@ from . import models class SignUpResource(resources.ModelResource): groups = fields.Field( attribute='groups', - widget=widgets.ManyToManyWidget(model=models.GroupChoice, separator=' ,', field='choice'), + widget=widgets.ManyToManyWidget( + model=models.GroupChoice, separator=' ,', field='choice'), ) class Meta: diff --git a/src/account/serializers.py b/src/account/serializers.py index e38ad58cb689559fb56cb78a93e94631401e0ecc..895d845a7554129824391e5cc320edc5072dc85c 100644 --- a/src/account/serializers.py +++ b/src/account/serializers.py @@ -1,6 +1,5 @@ from rest_framework import serializers from account import models -from common.middleware import CurrentUserMiddleware from common import email @@ -10,10 +9,14 @@ class ChoiceSerializer(serializers.ModelSerializer): fields = ('choice', 'profile') -class ProfileSerializer(serializers.ModelSerializer): - groups = serializers.SlugRelatedField(many=True, slug_field='choice', queryset=models.GroupChoice.objects.all()) +class ProfileSerializer_User(serializers.ModelSerializer): + groups = serializers.SlugRelatedField( + many=True, + slug_field='choice', + queryset=models.GroupChoice.objects.all()) updated_at = serializers.DateTimeField(read_only=True) full_name = serializers.SerializerMethodField() + bits = serializers.SerializerMethodField() class Meta: model = models.Profile @@ -30,23 +33,25 @@ class ProfileSerializer(serializers.ModelSerializer): 'motivation_exercise', 'full_name', 'role', + 'bits' ) def validate_updated_at(self, value): deadline = models.Deadline.get_solo().deadline if deadline is not None and value > deadline: - raise serializers.ValidationError("You cannot join after the deadline") + raise serializers.ValidationError( + "You cannot join after the deadline") return value def validate_role(self, value): - modifier_role = CurrentUserMiddleware.get_current_user_profile().role - if value != modifier_role and modifier_role != "Staff": - raise serializers.ValidationError("You don't have permission change role") + modifier_role = self.context['request'].user.profile.role + if value != modifier_role: + raise serializers.ValidationError( + "You don't have permission to change role") return value def validate_signed(self, value): - modifier = CurrentUserMiddleware.get_current_user_profile() - if value is False and modifier.role != "Staff": + if value is False: raise serializers.ValidationError("You cannot join without signed") return value @@ -61,3 +66,87 @@ class ProfileSerializer(serializers.ModelSerializer): def get_full_name(self, obj): return obj.full_name + + def get_bits(self, obj): + if obj.homework_bits is None: + return obj.events_visited + return obj.homework_bits + obj.events_visited + + +class ProfileSerializer_Staff(serializers.ModelSerializer): + groups = serializers.SlugRelatedField( + many=True, + slug_field='choice', + queryset=models.GroupChoice.objects.all()) + updated_at = serializers.DateTimeField(read_only=True) + full_name = serializers.SerializerMethodField() + + class Meta: + model = models.Profile + read_only_fields = ('id', 'join_date', 'updated_at', 'full_name', ) + fields = ( + 'id', + 'join_date', + 'updated_at', + 'nick', + 'signed', + 'groups', + 'motivation_about', + 'motivation_profession', + 'motivation_exercise', + 'full_name', + 'role', + 'events_visited', + 'homework_bits' + ) + + def validate_updated_at(self, value): + deadline = models.Deadline.get_solo().deadline + if deadline is not None and value > deadline: + raise serializers.ValidationError( + "You cannot join after the deadline") + return value + + def validate_role(self, value): + return value + + def validate_signed(self, value): + return value + + def update(self, instance, validated_data): + new_role = validated_data.get('role', instance.role) + if instance.role != new_role: + if new_role == 'Student': + email.admitted(instance.user) + if new_role == 'Denied': + email.denied(instance.user) + return super().update(instance, validated_data) + + def get_full_name(self, obj): + return obj.full_name + + +class MonitoringSerializer(serializers.ModelSerializer): + bits = serializers.SerializerMethodField() + full_name = serializers.SerializerMethodField() + + class Meta: + model = models.Profile + read_only_fields = ( + 'id', + 'bits', + 'full_name', + ) + fields = ( + 'id', + 'bits', + 'full_name', + ) + + def get_bits(self, obj): + if obj.homework_bits is None: + return obj.events_visited + return obj.homework_bits + obj.events_visited + + def get_full_name(self, obj): + return obj.full_name diff --git a/src/account/urls.py b/src/account/urls.py index ebf85f3d5bdf9885beb1ad7f28d1bce0e6624575..07e85fddebf48ed4fe6e582f6e2d9f493567af92 100644 --- a/src/account/urls.py +++ b/src/account/urls.py @@ -3,5 +3,15 @@ from . import views router = routers.DefaultRouter() -router.register(r'profiles', views.ProfileViewSet, base_name='profile') +router.register( + r'profiles', + views.ProfileViewSet, + base_name='profile' +) +router.register( + r'monitoring/profiles', + views.MonitorinViewSet, + base_name='monitoring' +) + urlpatterns = router.urls diff --git a/src/account/views.py b/src/account/views.py index c2ad4ec5b61ff4ae7545845c8b1b9149ec42bba5..456f83c27f5d2434794ce769355b3e3e5544b12e 100644 --- a/src/account/views.py +++ b/src/account/views.py @@ -1,17 +1,40 @@ from rest_framework import viewsets from rest_framework import permissions from rest_framework.response import Response -from rest_framework.decorators import list_route +from rest_framework.decorators import action from common.permissions import IsSafeOrPatch +from rest_framework_api_key.permissions import HasAPIKey from . import models from . import serializers +class MonitorinViewSet(viewsets.ModelViewSet): + serializer_class = serializers.MonitoringSerializer + permission_classes = (HasAPIKey,) + + def get_queryset(self): + return models.Profile.objects.filter(role='Student') + + @action(detail=False) + def totalbits(self, request): + profiles = models.Profile.objects.filter(role='Student') + bits = map(lambda item: serializers.MonitoringSerializer(item).data['bits'], profiles) + return Response({ + 'sum': sum(bits) + }) + + class ProfileViewSet(viewsets.ModelViewSet): - serializer_class = serializers.ProfileSerializer + serializer_class = serializers.ProfileSerializer_User permission_classes = (permissions.IsAuthenticated, IsSafeOrPatch) + def get_serializer_class(self): + user = self.request.user + if user.profile.role == 'Staff': + return serializers.ProfileSerializer_Staff + return serializers.ProfileSerializer_User + def get_queryset(self): user = self.request.user if user.profile.role == 'Staff': @@ -21,7 +44,16 @@ class ProfileViewSet(viewsets.ModelViewSet): return models.Profile.objects.all() return models.Profile.objects.filter(pk=user.profile.id) - @list_route(methods=['get']) + @action(detail=False) def me(self, request): serializer = self.serializer_class(request.user.profile) return Response(serializer.data) + + @action(detail=False) + def deadline(self, request): + deadline = models.Deadline.get_solo() + return Response({ + 'deadline': deadline.deadline, + 'messageBefore': deadline.messageBefore, + 'messageAfter': deadline.messageAfter + }) diff --git a/src/common/email.py b/src/common/email.py index 45b8705f994cea140f6746831d916630364d0c0e..ffb32b274836aef944ce8fdeead4ced50ecc8c80 100644 --- a/src/common/email.py +++ b/src/common/email.py @@ -1,8 +1,8 @@ from django.core.mail import send_mail import codecs -sender_email = 'noreply@ujonc.sch.bme.hu' -link = 'https://ujonc.sch.bme.hu/homework' +sender_email = 'noreply@ujonc.kszk.bme.hu' +link = 'https://ujonc.kszk.bme.hu/homework' def read_email(name): @@ -16,6 +16,7 @@ def registration(user): message = read_email('registration.txt') message = str.format(message % {'name': user.get_full_name()}) send_mail(subject, message, sender_email, [user.email, ]) + pass def admitted(user): @@ -23,6 +24,7 @@ def admitted(user): message = read_email('admitted.txt') message = str.format(message % {'name': user.get_full_name()}) send_mail(subject, message, sender_email, [user.email, ]) + pass def denied(user): @@ -30,14 +32,17 @@ def denied(user): message = read_email('denied.txt') message = str.format(message % {'name': user.get_full_name()}) send_mail(subject, message, sender_email, [user.email, ]) + pass def new_homework(user, deadline): deadline = deadline.strftime('%Y-%m-%d %H:%M') subject = "Új házifeladat" message = read_email('new_homework.txt') - message = str.format(message % {'name': user.get_full_name(), 'link': link, 'deadline': deadline}) + message = str.format( + message % {'name': user.get_full_name(), 'link': link, 'deadline': deadline}) send_mail(subject, message, sender_email, [user.email, ]) + pass def homework_corrected(user, title, accepted): @@ -47,5 +52,7 @@ def homework_corrected(user, title, accepted): else: status = 'Hibás' message = read_email('homework_corrected.txt') - message = str.format(message % {'name': user.get_full_name(), 'link': link, 'status': status, 'title': title}) + message = str.format(message % {'name': user.get_full_name( + ), 'link': link, 'status': status, 'title': title}) send_mail(subject, message, sender_email, [user.email, ]) + pass diff --git a/src/common/middleware.py b/src/common/middleware.py deleted file mode 100644 index 82f040c15458facfd7e6603558465825b2dc4c4a..0000000000000000000000000000000000000000 --- a/src/common/middleware.py +++ /dev/null @@ -1,19 +0,0 @@ -from threading import local - -_profile = local() - - -class CurrentUserMiddleware(object): - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - _profile.value = request.user - return self.get_response(request) - - def get_current_user_profile(): - return _profile.value.profile - - def get_current_user(): - return _profile.value diff --git a/src/common/permissions.py b/src/common/permissions.py index 7d2384c7bf9186939272c68ac9d5f61dbfe58e72..71019190dc24d58c05180291c99b3d4f83ed1fcd 100644 --- a/src/common/permissions.py +++ b/src/common/permissions.py @@ -5,13 +5,13 @@ from rest_framework.permissions import SAFE_METHODS class IsStaffOrReadOnly(BasePermission): def has_permission(self, request, view): return request.method in SAFE_METHODS or\ - (request.user.is_authenticated and request.user.profile.role == 'Staff') + (request.user.is_authenticated and request.user.profile.role == 'Staff') class IsStaffOrReadOnlyForAuthenticated(BasePermission): def has_permission(self, request, view): return request.user.is_authenticated and\ - (request.method in SAFE_METHODS or request.user.profile.role == 'Staff') + (request.method in SAFE_METHODS or request.user.profile.role == 'Staff') class IsStaffUser(BasePermission): @@ -27,4 +27,5 @@ class IsSafeOrPatch(BasePermission): class IsStaffOrStudent(BasePermission): def has_permission(self, request, view): return request.user.is_authenticated and\ - (request.user.profile.role == 'Staff' or request.user.profile.role == 'Student') + (request.user.profile.role == + 'Staff' or request.user.profile.role == 'Student') diff --git a/src/document/migrations/0001_initial.py b/src/document/migrations/0001_initial.py index a6c028301e73f9984456b29c47277a90f2ca37ed..f40f0e7e4a8d42373f06d0b01ce63b3a6f730cf8 100644 --- a/src/document/migrations/0001_initial.py +++ b/src/document/migrations/0001_initial.py @@ -1,9 +1,10 @@ -# Generated by Django 2.0.1 on 2019-01-17 15:06 +# Generated by Django 2.2.4 on 2020-02-04 14:16 import common.validators import django.core.validators from django.db import migrations, models import django.db.models.deletion +import document.models class Migration(migrations.Migration): @@ -21,11 +22,11 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('uploaded_at', models.DateTimeField(auto_now_add=True)), - ('name', models.CharField(max_length=150)), + ('name', models.CharField(blank=True, default='', max_length=150)), ('description', models.TextField(blank=True, default='')), - ('file', models.FileField(upload_to='', validators=[django.core.validators.FileExtensionValidator(['png', 'jpeg', 'jpg', 'zip']), common.validators.FileSizeValidator(size_limit=52428800)])), - ('solution', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='files', to='homework.Solution')), - ('uploaded_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='account.Profile')), + ('file', models.FileField(blank=True, null=True, upload_to=document.models.document_file_name, validators=[django.core.validators.FileExtensionValidator(['png', 'jpeg', 'jpg', 'zip']), common.validators.FileSizeValidator(size_limit=52428800)])), + ('solution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='homework.Solution')), + ('uploaded_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='documents', to='account.Profile')), ], ), ] diff --git a/src/document/migrations/0002_auto_20190121_1332.py b/src/document/migrations/0002_auto_20190121_1332.py deleted file mode 100644 index 5cf755b92259e186c711bd5be422791f17294858..0000000000000000000000000000000000000000 --- a/src/document/migrations/0002_auto_20190121_1332.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.0.1 on 2019-01-21 12:32 - -import common.validators -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('document', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='document', - name='file', - field=models.FileField(blank=True, default='', upload_to='', validators=[django.core.validators.FileExtensionValidator(['png', 'jpeg', 'jpg', 'zip']), common.validators.FileSizeValidator(size_limit=52428800)]), - ), - ] diff --git a/src/document/migrations/0003_auto_20190121_1335.py b/src/document/migrations/0003_auto_20190121_1335.py deleted file mode 100644 index fa4d1c22ae825d615e093126a69d4a0a76132ad0..0000000000000000000000000000000000000000 --- a/src/document/migrations/0003_auto_20190121_1335.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.0.1 on 2019-01-21 12:35 - -import common.validators -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('document', '0002_auto_20190121_1332'), - ] - - operations = [ - migrations.AlterField( - model_name='document', - name='file', - field=models.FileField(blank=True, upload_to='', validators=[django.core.validators.FileExtensionValidator(['png', 'jpeg', 'jpg', 'zip']), common.validators.FileSizeValidator(size_limit=52428800)]), - ), - ] diff --git a/src/document/migrations/0004_auto_20190123_1812.py b/src/document/migrations/0004_auto_20190123_1812.py deleted file mode 100644 index 9eb42c6664d6d3fe22abb6ec091b7ea965643db5..0000000000000000000000000000000000000000 --- a/src/document/migrations/0004_auto_20190123_1812.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.0.1 on 2019-01-23 17:12 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('document', '0003_auto_20190121_1335'), - ] - - operations = [ - migrations.AlterField( - model_name='document', - name='solution', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='homework.Solution'), - ), - ] diff --git a/src/document/migrations/0005_auto_20190129_1438.py b/src/document/migrations/0005_auto_20190129_1438.py deleted file mode 100644 index 5c207c4a9776a2776d3d1b5da9d0473d2a283175..0000000000000000000000000000000000000000 --- a/src/document/migrations/0005_auto_20190129_1438.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.0.1 on 2019-01-29 13:38 - -import common.validators -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('document', '0004_auto_20190123_1812'), - ] - - operations = [ - migrations.AlterField( - model_name='document', - name='file', - field=models.FileField(blank=True, null=True, upload_to='', validators=[django.core.validators.FileExtensionValidator(['png', 'jpeg', 'jpg', 'zip']), common.validators.FileSizeValidator(size_limit=52428800)]), - ), - migrations.AlterField( - model_name='document', - name='name', - field=models.CharField(blank=True, default='', max_length=150), - ), - ] diff --git a/src/document/models.py b/src/document/models.py index 8e26ea20ec77461fe475a53e308c9d0194e131c2..f4103dc2c2a9c43cc467fa48c118ec73a5ee3ba9 100644 --- a/src/document/models.py +++ b/src/document/models.py @@ -10,7 +10,7 @@ from common.validators import FileSizeValidator def document_file_name(instance, filename): return '/'.join([ - 'document', + 'public/document', instance.solution.task.title, instance.uploaded_by.full_name, filename @@ -18,7 +18,11 @@ def document_file_name(instance, filename): class Document(models.Model): - uploaded_by = models.ForeignKey(Profile, on_delete=models.DO_NOTHING) + uploaded_by = models.ForeignKey( + Profile, + on_delete=models.DO_NOTHING, + related_name='documents', + ) uploaded_at = models.DateTimeField(auto_now_add=True, editable=False) name = models.CharField(max_length=150, blank=True, default='') description = models.TextField(blank=True, default='') @@ -36,7 +40,8 @@ class Document(models.Model): null=True, upload_to=document_file_name ) - solution = models.ForeignKey(Solution, related_name='files', on_delete=models.CASCADE) + solution = models.ForeignKey( + Solution, related_name='files', on_delete=models.CASCADE) def __str__(self): return self.name diff --git a/src/document/serializers.py b/src/document/serializers.py index f7ad1c31123d1a5e159c07187174d087d5daaf6e..616321803f490246ede36bb5a43285d54ecc6577 100644 --- a/src/document/serializers.py +++ b/src/document/serializers.py @@ -1,7 +1,6 @@ from rest_framework import serializers from common.serializers import CurrentUserProfileDefault from . import models -from common.middleware import CurrentUserMiddleware _max_count = 1 @@ -9,20 +8,51 @@ _max_count = 1 class DocumentSerializer(serializers.ModelSerializer): uploaded_by = serializers.HiddenField(default=CurrentUserProfileDefault()) uploaded_by_name = serializers.SerializerMethodField() + file_url = serializers.SerializerMethodField() class Meta: model = models.Document - fields = ('uploaded_by', 'uploaded_at', 'name', 'description', 'file', 'uploaded_by_name', 'solution', ) + fields = [ + 'uploaded_by', + 'uploaded_at', + 'name', + 'description', + 'file_url', + 'file', + 'uploaded_by_name', + 'solution', + ] + + read_only_fields = [ + 'uploaded_by', + 'uploaded_at', + 'file_url', + 'uploaded_by_name', + ] + + extra_kwargs = { + 'file': {'write_only': True}, + } + + def to_representation(self, instance): + data = super().to_representation(instance) + if not data['file_url']: + data['file_url'] = "" + return data def get_uploaded_by_name(self, obj): return obj.uploaded_by.full_name + def get_file_url(self, obj): + return f"/api/v1/documents/{obj.id}/download/" + def validate_solution(self, value): - profile = CurrentUserMiddleware.get_current_user_profile() + profile = self.context['request'].user.profile if value not in profile.solution.all(): raise serializers.ValidationError('You dont have permission!') - count = models.Document.objects.filter(uploaded_by=profile, solution=value).count() + count = models.Document.objects.filter( + uploaded_by=profile, solution=value).count() if count >= _max_count: - raise serializers.ValidationError('You cant upload more than ' + str(_max_count) + - ' document to one solution!') + raise serializers.ValidationError( + f'You cant upload more than {_max_count} document to one solution!') return value diff --git a/src/document/views.py b/src/document/views.py index 00f53216570810c84504625b4c1f2abdf2896f2d..86e53f1f7643a134731c18c3c52b891d2de4d581 100644 --- a/src/document/views.py +++ b/src/document/views.py @@ -3,6 +3,10 @@ from common import permissions from . import models from . import serializers from rest_framework.parsers import JSONParser, MultiPartParser +from django.http import HttpResponse, Http404 +from rest_framework.decorators import action +import os +import mimetypes class DocumentViewSet(viewsets.ModelViewSet): @@ -23,7 +27,10 @@ class DocumentViewSet(viewsets.ModelViewSet): profile_id = self.request.query_params.get('profileID', None) solution_id = self.request.query_params.get('solutionID', None) if profile_id is not None and solution_id is not None: - return queryset.filter(uploaded_by=profile_id, solution=solution_id) + return queryset.filter( + uploaded_by=profile_id, + solution=solution_id + ) if profile_id is not None: return queryset.filter(uploaded_by=profile_id) if solution_id is not None: @@ -36,3 +43,23 @@ class DocumentViewSet(viewsets.ModelViewSet): if solution_id is not None: return queryset.filter(solution=solution_id) return queryset + + def perform_create(self, serializer): + kwargs = { + 'uploaded_by': self.request.user.profile + } + + serializer.save(**kwargs) + + @action(detail=True, methods=["get"]) + def download(self, request, pk): + document = self.get_object() + with document.file.open() as fh: + response = HttpResponse( + fh.read(), + content_type=mimetypes.guess_type(document.file.name) + ) + response['Content-Disposition'] = \ + 'inline; filename=' + os.path.basename(document.file.name) + return response + raise Http404 diff --git a/src/entrypoint.prod.sh b/src/entrypoint.prod.sh new file mode 100755 index 0000000000000000000000000000000000000000..5236bfaeabe748c12cfce8f387009f568021975a --- /dev/null +++ b/src/entrypoint.prod.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +if [ "$DJANGO_SETTINGS_MODULE" = "kszkepzes.settings.production" ] +then + echo "Waiting for postgres..." + + while ! nc -z $DB_HOST $DB_PORT; do + sleep 0.1 + done + + echo "PostgreSQL started" +fi + +exec "$@" \ No newline at end of file diff --git a/src/entrypoint.sh b/src/entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..e837b4f5bad4448d9d7558f0dc0ebb9de1b2b9bb --- /dev/null +++ b/src/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +if [ "$DJANGO_SETTINGS_MODULE" = "kszkepzes.settings.production" ] +then + echo "Waiting for postgres..." + + while ! nc -z $DB_HOST $DB_PORT; do + sleep 0.1 + done + + echo "PostgreSQL started" +fi + +python manage.py flush --no-input +python manage.py migrate +python manage.py collectstatic --no-input --clear + +exec "$@" \ No newline at end of file diff --git a/src/groups/__init__.py b/src/groups/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/groups/admin.py b/src/groups/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8eefe1fca67e802c1a591c1705104a3bc34f23fc --- /dev/null +++ b/src/groups/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from . import models + +admin.site.register(models.Group) diff --git a/src/groups/apps.py b/src/groups/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..e6984a81f64b2c3bf2ebae54e2d9501333bafa38 --- /dev/null +++ b/src/groups/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class GroupsConfig(AppConfig): + name = 'groups' diff --git a/src/groups/migrations/0001_initial.py b/src/groups/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..643e9bace651b115a74ee0ce5c4dcbc9157b6b82 --- /dev/null +++ b/src/groups/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2020-02-02 14:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.AutoField(auto_created=True, + primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, default='', max_length=50)), + ('description', models.TextField(blank=True, default='')), + ], + ), + ] diff --git a/src/groups/migrations/__init__.py b/src/groups/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/groups/models.py b/src/groups/models.py new file mode 100644 index 0000000000000000000000000000000000000000..2953fdc811b6e9a837cd4e4eb6b1cf8f4f72f9bd --- /dev/null +++ b/src/groups/models.py @@ -0,0 +1,11 @@ +from django.db import models + +# KSZK groups, like Devteam, Sysadmin, ... + + +class Group(models.Model): + name = models.CharField(max_length=50, blank=True, default='') + description = models.TextField(blank=True, default='') + + def __str__(self): + return self.name diff --git a/src/groups/serializers.py b/src/groups/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..2f14fa78374ea5983b9ec38f05d4f50be82b760e --- /dev/null +++ b/src/groups/serializers.py @@ -0,0 +1,16 @@ +from rest_framework import serializers +from . import models + + +class GroupSerializer(serializers.ModelSerializer): + class Meta: + model = models.Group + read_only_fields = ( + 'id', + 'name', + 'description',) + fields = ( + 'id', + 'name', + 'description', + ) diff --git a/src/groups/tests.py b/src/groups/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..a79ca8be565f44aacce95bad20c1ee34d175ed20 --- /dev/null +++ b/src/groups/tests.py @@ -0,0 +1,3 @@ +# from django.test import TestCase + +# Create your tests here. diff --git a/src/groups/urls.py b/src/groups/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..a265b6f79ebda0883deed15fc061f23a3369c88a --- /dev/null +++ b/src/groups/urls.py @@ -0,0 +1,7 @@ +from rest_framework import routers +from . import views + +router = routers.DefaultRouter() + +router.register(r'groups', views.GroupsViewSet, base_name='groups') +urlpatterns = router.urls diff --git a/src/groups/views.py b/src/groups/views.py new file mode 100644 index 0000000000000000000000000000000000000000..2149906966c10423777d5da772f3402579e89904 --- /dev/null +++ b/src/groups/views.py @@ -0,0 +1,9 @@ +from rest_framework import viewsets + +from . import serializers +from . import models + + +class GroupsViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = serializers.GroupSerializer + queryset = models.Group.objects.all() diff --git a/src/homework/migrations/0001_initial.py b/src/homework/migrations/0001_initial.py index 13125342ce3865c45e5e8304a5353c3b5837e371..562d226240a20d19ece2eb84f0586b9fc50976cd 100644 --- a/src/homework/migrations/0001_initial.py +++ b/src/homework/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 2.0.1 on 2019-01-17 15:06 +# Generated by Django 2.2.4 on 2020-02-04 14:16 -import common.middleware from django.db import migrations, models import django.db.models.deletion @@ -15,32 +14,29 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Solution', + name='Task', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('note', models.TextField(blank=True, default='')), - ('accepted', models.BooleanField()), - ('corrected', models.BooleanField()), - ('created_by', models.ForeignKey(default=common.middleware.CurrentUserMiddleware.get_current_user_profile, on_delete=django.db.models.deletion.DO_NOTHING, related_name='solution', to='account.Profile')), + ('title', models.CharField(max_length=150)), + ('text', models.TextField()), + ('deadline', models.DateTimeField()), + ('bits', models.IntegerField(default=1)), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks', to='account.Profile')), ], ), migrations.CreateModel( - name='Task', + name='Solution', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('title', models.CharField(max_length=150)), - ('text', models.TextField()), - ('deadline', models.DateTimeField()), - ('created_by', models.ForeignKey(default=common.middleware.CurrentUserMiddleware.get_current_user_profile, on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks', to='account.Profile')), + ('note', models.TextField(blank=True, default='')), + ('accepted', models.BooleanField(blank=True, default=False)), + ('corrected', models.BooleanField(blank=True, default=False)), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='solution', to='account.Profile')), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='solutions', to='homework.Task')), ], ), - migrations.AddField( - model_name='solution', - name='task', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='solutions', to='homework.Task'), - ), ] diff --git a/src/homework/migrations/0002_auto_20190121_2124.py b/src/homework/migrations/0002_auto_20190121_2124.py deleted file mode 100644 index 3abf5a83e87d8424a882e3c92f55af2f1478d3ee..0000000000000000000000000000000000000000 --- a/src/homework/migrations/0002_auto_20190121_2124.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.0.1 on 2019-01-21 20:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('homework', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='solution', - name='accepted', - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name='solution', - name='corrected', - field=models.BooleanField(default=False), - ), - ] diff --git a/src/homework/models.py b/src/homework/models.py index 4ecb6e958c9a9349ca39a26d75c57463f030c1fc..be416b1182356e443c499faa0650b7e04bb8a7cd 100755 --- a/src/homework/models.py +++ b/src/homework/models.py @@ -1,5 +1,4 @@ from django.db import models -from common.middleware import CurrentUserMiddleware from account.models import Profile @@ -8,25 +7,25 @@ class Task(models.Model): Profile, on_delete=models.DO_NOTHING, related_name='tasks', - default=CurrentUserMiddleware.get_current_user_profile, ) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) title = models.CharField(max_length=150) text = models.TextField() deadline = models.DateTimeField() + bits = models.IntegerField(default=1) def __str__(self): return self.title class Solution(models.Model): - task = models.ForeignKey(Task, related_name='solutions', on_delete=models.CASCADE) + task = models.ForeignKey( + Task, related_name='solutions', on_delete=models.CASCADE) created_by = models.ForeignKey( Profile, related_name='solution', on_delete=models.DO_NOTHING, - default=CurrentUserMiddleware.get_current_user_profile ) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) @@ -34,5 +33,9 @@ class Solution(models.Model): accepted = models.BooleanField(blank=True, default=False) corrected = models.BooleanField(blank=True, default=False) + @property + def my_bits(self): + return 10 + def __str__(self): return "[{}] {}".format(self.created_at, self.created_by.full_name) diff --git a/src/homework/serializers.py b/src/homework/serializers.py index 3ee662e264b2710475a78c9b0538a2ad8b2770cb..e59ed06790824ae30bdaf891699df03960cdca68 100755 --- a/src/homework/serializers.py +++ b/src/homework/serializers.py @@ -3,7 +3,6 @@ from django.utils import timezone from account.models import Profile from . import models from common import email -from common.middleware import CurrentUserMiddleware class TaskSerializer(serializers.ModelSerializer): @@ -14,20 +13,28 @@ class TaskSerializer(serializers.ModelSerializer): def validate(self, data): if timezone.now() >= data['deadline']: - raise serializers.ValidationError('Please, enter appropriate deadline.') + raise serializers.ValidationError( + 'Please, enter appropriate deadline.') return data def create(self, validated_data): - profiles = Profile.objects.filter(role="Student").exclude(user__email='') + profiles = Profile.objects.filter( + role="Student").exclude( + user__email='') for profile in profiles: email.new_homework(profile.user, validated_data.get('deadline')) return self.Meta.model.objects.create(**validated_data) -class SolutionSerializer(serializers.ModelSerializer): +class SolutionSerializer_Student(serializers.ModelSerializer): class Meta: model = models.Solution - read_only_fields = ('created_by', 'created_at', 'updated_at', 'ready', 'files') + read_only_fields = ( + 'created_by', + 'created_at', + 'updated_at', + 'ready', + 'files') fields = ( 'id', 'task', @@ -45,26 +52,59 @@ class SolutionSerializer(serializers.ModelSerializer): raise serializers.ValidationError('You late.') return value - def validate_accepted(self, value): - profile = CurrentUserMiddleware.get_current_user_profile() - if profile.role != 'Staff' and value: - raise serializers.ValidationError("You don't have permission to modify accepted!") + def validate_note(self, value): + if value != '': + raise serializers.ValidationError( + "You don't have permission to create note!") return value - def validate_corrected(self, value): - profile = CurrentUserMiddleware.get_current_user_profile() - if profile.role != 'Staff' and value: - raise serializers.ValidationError("You don't have permission to modify corrected!") - return value + def update(self, instance, validated_data): + if instance.corrected is not True and validated_data.get( + 'corrected', instance.corrected) is True: + email.homework_corrected( + instance.created_by.user, + instance.task.title, + validated_data.get('accepted', instance.accepted) + ) + return super().update(instance, validated_data) - def validate_note(self, value): - profile = CurrentUserMiddleware.get_current_user_profile() - if profile.role != 'Staff' and value != '': - raise serializers.ValidationError("You don't have permission to create note!") + def create(self, validated_data): + profile = self.context['request'].user.profile + models.Solution.objects.filter( + created_by=profile, + task=validated_data['task']).delete() + return super().create(validated_data) + + +class SolutionSerializer_Staff(serializers.ModelSerializer): + class Meta: + model = models.Solution + read_only_fields = ( + 'created_by', + 'created_at', + 'updated_at', + 'ready', + 'files') + fields = ( + 'id', + 'task', + 'created_at', + 'updated_at', + 'accepted', + 'files', + 'created_by', + 'corrected', + 'note', + ) + + def validate_task(self, value): + if timezone.now() > value.deadline: + raise serializers.ValidationError('You late.') return value def update(self, instance, validated_data): - if instance.corrected is not True and validated_data.get('corrected', instance.corrected) is True: + if instance.corrected is not True and validated_data.get( + 'corrected', instance.corrected) is True: email.homework_corrected( instance.created_by.user, instance.task.title, @@ -73,6 +113,8 @@ class SolutionSerializer(serializers.ModelSerializer): return super().update(instance, validated_data) def create(self, validated_data): - profile = CurrentUserMiddleware.get_current_user_profile() - models.Solution.objects.filter(created_by=profile, task=validated_data['task']).delete() + profile = self.context['request'].user.profile + models.Solution.objects.filter( + created_by=profile, + task=validated_data['task']).delete() return super().create(validated_data) diff --git a/src/homework/views.py b/src/homework/views.py index 9a9d88c415e5b6182a6707bdcfb32db355f7d05a..554533838cf23f7fedca98b9d6d8490688d9257e 100755 --- a/src/homework/views.py +++ b/src/homework/views.py @@ -8,13 +8,29 @@ from common import permissions class TasksViewSet(viewsets.ModelViewSet): serializer_class = serializers.TaskSerializer queryset = models.Task.objects.all() - permission_classes = (permissions.IsStaffOrReadOnlyForAuthenticated, permissions.IsStaffOrStudent, ) + permission_classes = ( + permissions.IsStaffOrReadOnlyForAuthenticated, + permissions.IsStaffOrStudent, + ) + + def perform_create(self, serializer): + kwargs = { + 'created_by': self.request.user.profile + } + + serializer.save(**kwargs) class SolutionsViewSet(viewsets.ModelViewSet): - serializer_class = serializers.SolutionSerializer + serializer_class = serializers.SolutionSerializer_Student permission_classes = (permissions.IsStaffOrStudent, ) + def get_serializer_class(self): + user = self.request.user + if user.profile.role == 'Staff': + return serializers.SolutionSerializer_Staff + return serializers.SolutionSerializer_Student + def get_queryset(self): user = self.request.user queryset = models.Solution.objects.filter(created_by=user.profile) @@ -24,3 +40,10 @@ class SolutionsViewSet(viewsets.ModelViewSet): if profile_id is not None: queryset = queryset.filter(created_by=profile_id) return queryset + + def perform_create(self, serializer): + kwargs = { + 'created_by': self.request.user.profile + } + + serializer.save(**kwargs) diff --git a/src/images/__init__.py b/src/images/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/images/admin.py b/src/images/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..01981f71f01bb049f26d2f3e12783ae24650009e --- /dev/null +++ b/src/images/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Image + +admin.site.register(Image) +# Register your models here. diff --git a/src/images/apps.py b/src/images/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..a873d1a4e0f3523f2d9e297c7c895e710098e878 --- /dev/null +++ b/src/images/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ImagesConfig(AppConfig): + name = 'images' diff --git a/src/images/migrations/0001_initial.py b/src/images/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..2319c1469c25d31d08611fb04d3e4a1c94aba949 --- /dev/null +++ b/src/images/migrations/0001_initial.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.4 on 2020-02-07 22:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Images', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(blank=True, null=True, upload_to='images/')), + ], + ), + ] diff --git a/src/images/migrations/0002_auto_20200208_0254.py b/src/images/migrations/0002_auto_20200208_0254.py new file mode 100644 index 0000000000000000000000000000000000000000..045c2a144dfaf0952963f7841f53d9e740bc40e5 --- /dev/null +++ b/src/images/migrations/0002_auto_20200208_0254.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2020-02-08 01:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('images', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='images', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='public/images/'), + ), + ] diff --git a/src/images/migrations/0003_auto_20200208_0402.py b/src/images/migrations/0003_auto_20200208_0402.py new file mode 100644 index 0000000000000000000000000000000000000000..4868a49ca7b2222bd8bce800b0e4344032b3b844 --- /dev/null +++ b/src/images/migrations/0003_auto_20200208_0402.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.4 on 2020-02-08 03:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('images', '0002_auto_20200208_0254'), + ] + + operations = [ + migrations.RenameModel( + old_name='Images', + new_name='Image', + ), + ] diff --git a/src/images/migrations/__init__.py b/src/images/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/images/models.py b/src/images/models.py new file mode 100644 index 0000000000000000000000000000000000000000..cab2442d4eefa7e8d5200145ab567d911d12bffb --- /dev/null +++ b/src/images/models.py @@ -0,0 +1,20 @@ +import os + +from django.db import models +from django.dispatch import receiver + + +class Image(models.Model): + image = models.ImageField( + upload_to='public/images/', null=True, blank=True + ) + + def __str__(self): + return str(self.id) + +# Deletes file from filesystem when File object is deleted. +@receiver(models.signals.post_delete, sender=Image) +def auto_delete_image_on_delete(sender, instance, **kwargs): + if instance.image: + if os.path.isfile(instance.image.path): + os.remove(instance.image.path) diff --git a/src/images/serializers.py b/src/images/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..b318581cc09e83aa77582afb4217893b1b235866 --- /dev/null +++ b/src/images/serializers.py @@ -0,0 +1,17 @@ +from images.models import Image +from rest_framework import serializers + + +class ImageSerializer(serializers.ModelSerializer): + class Meta: + model = Image + fields = ('image',) + + def to_representation(self, instance): + response = super( + ImageSerializer, + self + ).to_representation(instance) + if instance.image: + response['image'] = instance.image.url + return response diff --git a/src/images/tests.py b/src/images/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/images/urls.py b/src/images/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..b16c854441022549d60f67c06b69d15ac63cf1bd --- /dev/null +++ b/src/images/urls.py @@ -0,0 +1,8 @@ +from rest_framework import routers +from images import views + + +router = routers.DefaultRouter() +router.register(r'images', views.ImagesViewSet, base_name='images') + +urlpatterns = router.urls diff --git a/src/images/views.py b/src/images/views.py new file mode 100644 index 0000000000000000000000000000000000000000..097e757f9fe3902c4e02838735e6e3b466591960 --- /dev/null +++ b/src/images/views.py @@ -0,0 +1,10 @@ +from common.permissions import IsStaffOrReadOnly +from rest_framework import viewsets +from images.models import Image +from images.serializers import ImageSerializer + + +class ImagesViewSet(viewsets.ModelViewSet): + serializer_class = ImageSerializer + permission_classes = (IsStaffOrReadOnly, ) + queryset = Image.objects.all() diff --git a/src/kszkepzes/settings/base.py b/src/kszkepzes/settings/base.py index b831a0ec690d3c2ff85248e79972ab90f1b260f2..fa4afc6fa9a812faa4d6ddd0c096c85cf72c7cdb 100644 --- a/src/kszkepzes/settings/base.py +++ b/src/kszkepzes/settings/base.py @@ -13,7 +13,7 @@ https://docs.djangoproject.com/en/1.11/ref/settings/ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # Quick-start development settings - unsuitable for production @@ -40,6 +40,7 @@ INSTALLED_APPS = [ 'django_extensions', 'import_export', 'rest_framework', + "rest_framework_api_key", 'social_django', 'solo', 'authsch', @@ -48,6 +49,10 @@ INSTALLED_APPS = [ 'stats', 'news', 'document', + 'mentors', + 'images', + 'groups', + 'drf_yasg', ] MIDDLEWARE = [ @@ -58,7 +63,6 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'common.middleware.CurrentUserMiddleware', ] ROOT_URLCONF = 'kszkepzes.urls' @@ -154,7 +158,8 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ -STATIC_URL = '/django-static/' +STATIC_URL = '/staticfiles/' +STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") -MEDIA_ROOT = os.getenv('MEDIA_ROOT', '/tmp') -MEDIA_URL = '/media/' +MEDIA_URL = "/mediafiles/" +MEDIA_ROOT = os.path.join(BASE_DIR, "mediafiles") diff --git a/src/kszkepzes/settings/local.py b/src/kszkepzes/settings/local.py index 53eb49a1bd60f3f66631867f420381d3070b85a1..1c4e0b5cb5ea38e79c195fc5e345f921fb5a8849 100644 --- a/src/kszkepzes/settings/local.py +++ b/src/kszkepzes/settings/local.py @@ -1,6 +1,8 @@ from .base import * -SESSION_COOKIE_SECURE=False +DEBUG = int(os.environ.get("DEBUG", default=1)) + +SESSION_COOKIE_SECURE = False EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.gmail.com' EMAIL_USE_TLS = True diff --git a/src/kszkepzes/settings/production.py b/src/kszkepzes/settings/production.py index cbd865978fa81069cdf9c99a29f8360375bb6fd2..f6d364955eb58dbcdd773123c282c597ee0ccc5a 100644 --- a/src/kszkepzes/settings/production.py +++ b/src/kszkepzes/settings/production.py @@ -1,12 +1,12 @@ from .base import * -DEBUG = False +DEBUG = int(os.environ.get("DEBUG", default=0)) ALLOWED_HOSTS = ['*'] DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql', + 'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.postgresql'), 'NAME': os.getenv('DB_NAME', 'kszkepzes'), 'USER': os.getenv('DB_USER'), 'PASSWORD': os.getenv('DB_PASSWORD'), @@ -24,7 +24,6 @@ REST_FRAMEWORK = { ) } -STATIC_ROOT = os.path.join(BASE_DIR, 'static_collected') EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'mail.sch.bme.hu' EMAIL_PORT = 25 diff --git a/src/kszkepzes/urls.py b/src/kszkepzes/urls.py index f5e414edc9604b99a6d4118130f32b1e4b25b724..c07e90ed5b4b50ce44b43077039adb5e8f38e0fd 100644 --- a/src/kszkepzes/urls.py +++ b/src/kszkepzes/urls.py @@ -4,7 +4,30 @@ from django.conf import settings from django.contrib import admin from django.contrib.auth import views as auth_views +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="Snippets API", + default_version='v1', + description="Test description", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="contact@snippets.local"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) + urlpatterns = [ + url(r'^api/v1/swagger(?P<format>\.json|\.yaml)$', + schema_view.without_ui(cache_timeout=0), name='schema-json'), + url(r'^api/v1/swagger/$', schema_view.with_ui('swagger', + cache_timeout=0), name='schema-swagger-ui'), + url(r'^api/v1/redoc/$', schema_view.with_ui('redoc', + cache_timeout=0), name='schema-redoc'), url(r'^admin/', admin.site.urls), url(r'^api/v1/', include('social_django.urls', namespace='social')), url(r'^api/v1/homework/', include('homework.urls')), @@ -12,8 +35,12 @@ urlpatterns = [ url(r'^api/v1/', include('account.urls')), url(r'^api/v1/', include('news.urls')), url(r'^api/v1/', include('document.urls')), + url(r'^api/v1/', include('groups.urls')), + url(r'^api/v1/', include('mentors.urls')), + url(r'^api/v1/', include('images.urls')), url(r'^api/v1/logout/$', auth_views.LogoutView.as_view(), name='logout'), ] if settings.DEBUG: - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) diff --git a/src/kszkepzes/wsgi.py b/src/kszkepzes/wsgi.py index 12023b6a78e7c9bbb5cc7e48d9d4f63b9eba7b3e..f29c7593000fed09e6a5a94b4c631462fb71bc73 100644 --- a/src/kszkepzes/wsgi.py +++ b/src/kszkepzes/wsgi.py @@ -11,6 +11,7 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kszkepzes.settings.production") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", + "kszkepzes.settings.production") application = get_wsgi_application() diff --git a/src/mentors/__init__.py b/src/mentors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/mentors/admin.py b/src/mentors/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..bacbf7e7188cfdcd9b02e7ebae4224e61e575e06 --- /dev/null +++ b/src/mentors/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import Mentor + +admin.site.register(Mentor) +# Register your models here. diff --git a/src/mentors/apps.py b/src/mentors/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..5649da659a30793fa15b89d01037e08c0fdb3275 --- /dev/null +++ b/src/mentors/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MentorsConfig(AppConfig): + name = 'mentors' diff --git a/src/mentors/migrations/0001_initial.py b/src/mentors/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..0939292fa324326b75466f3c55561f92fbe628ad --- /dev/null +++ b/src/mentors/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.4 on 2020-02-04 14:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('account', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Mentor', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('text', models.TextField()), + ('image', models.ImageField(blank=True, null=True, upload_to='mentors/images/')), + ('email', models.EmailField(max_length=254)), + ('mentor', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='mentor', to='account.Profile')), + ], + ), + ] diff --git a/src/mentors/migrations/0002_auto_20200209_1955.py b/src/mentors/migrations/0002_auto_20200209_1955.py new file mode 100644 index 0000000000000000000000000000000000000000..8ffcb72b291e7efe9a1c82f5da1c9d3b827e9d36 --- /dev/null +++ b/src/mentors/migrations/0002_auto_20200209_1955.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2020-02-09 18:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mentors', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='mentor', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='public/mentors/images/'), + ), + ] diff --git a/src/mentors/migrations/0003_remove_mentor_mentor.py b/src/mentors/migrations/0003_remove_mentor_mentor.py new file mode 100644 index 0000000000000000000000000000000000000000..dcd08df7e84fac46c9f69642b3132d7e89955cd8 --- /dev/null +++ b/src/mentors/migrations/0003_remove_mentor_mentor.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.4 on 2020-02-09 22:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mentors', '0002_auto_20200209_1955'), + ] + + operations = [ + migrations.RemoveField( + model_name='mentor', + name='mentor', + ), + ] diff --git a/src/mentors/migrations/__init__.py b/src/mentors/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/mentors/models.py b/src/mentors/models.py new file mode 100644 index 0000000000000000000000000000000000000000..b641319bcfe6aaf07c8754ef13850c4c9e53491c --- /dev/null +++ b/src/mentors/models.py @@ -0,0 +1,22 @@ +import os + +from django.db import models +from django.dispatch import receiver + + +class Mentor(models.Model): + name = models.CharField(null=False, max_length=200) + text = models.TextField() + image = models.ImageField( + upload_to='public/mentors/images/', null=True, blank=True) + email = models.EmailField() + + def __str__(self): + return self.name + +# Deletes file from filesystem when File object is deleted. +@receiver(models.signals.post_delete, sender=Mentor) +def auto_delete_image_on_delete(sender, instance, **kwargs): + if instance.image: + if os.path.isfile(instance.image.path): + os.remove(instance.image.path) diff --git a/src/mentors/serializers.py b/src/mentors/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..c6e84b8565716eae6c90a67019742bfc22ef99f4 --- /dev/null +++ b/src/mentors/serializers.py @@ -0,0 +1,18 @@ +from mentors.models import Mentor +from rest_framework import serializers + + +class MentorSerializer(serializers.ModelSerializer): + + class Meta: + model = Mentor + fields = '__all__' + + def to_representation(self, instance): + response = super( + MentorSerializer, + self + ).to_representation(instance) + if instance.image: + response['image'] = instance.image.url + return response diff --git a/src/mentors/tests.py b/src/mentors/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/mentors/urls.py b/src/mentors/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..027a533ba50729d7ac321e7c6a3068575a59e76c --- /dev/null +++ b/src/mentors/urls.py @@ -0,0 +1,8 @@ +from rest_framework import routers +from mentors import views + + +router = routers.DefaultRouter() +router.register(r'mentors', views.MentorsViewSet, base_name='mentors') + +urlpatterns = router.urls diff --git a/src/mentors/views.py b/src/mentors/views.py new file mode 100644 index 0000000000000000000000000000000000000000..9fd090dc8e5aa8df0a7ae1c08204e023ab335bfe --- /dev/null +++ b/src/mentors/views.py @@ -0,0 +1,14 @@ +from common.permissions import IsStaffOrStudent, \ + IsStaffOrReadOnlyForAuthenticated +from rest_framework import viewsets +from mentors.models import Mentor +from mentors.serializers import MentorSerializer + + +class MentorsViewSet(viewsets.ModelViewSet): + serializer_class = MentorSerializer + permission_classes = ( + IsStaffOrReadOnlyForAuthenticated, + IsStaffOrStudent, + ) + queryset = Mentor.objects.all().order_by('name') diff --git a/src/news/migrations/0001_initial.py b/src/news/migrations/0001_initial.py index a8f40ce6d982860cc36fa0d335132bf70b770d9e..616cde052b50e3c1a7ee94e3d1c817a8d8a54b85 100644 --- a/src/news/migrations/0001_initial.py +++ b/src/news/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 2.0.1 on 2019-01-17 15:06 +# Generated by Django 2.2.4 on 2020-02-04 14:16 -import common.middleware from django.db import migrations, models import django.db.models.deletion @@ -22,7 +21,7 @@ class Migration(migrations.Migration): ('text', models.TextField()), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('author', models.ForeignKey(default=common.middleware.CurrentUserMiddleware.get_current_user_profile, on_delete=django.db.models.deletion.DO_NOTHING, related_name='author', to='account.Profile')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='author', to='account.Profile')), ('updated_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='updater', to='account.Profile')), ], ), diff --git a/src/news/models.py b/src/news/models.py index c216221410bc8e9f04f3b76c90265ed94191b9d6..17837b5d6a2287cc7429c2a7fdb26a4b13e3589e 100644 --- a/src/news/models.py +++ b/src/news/models.py @@ -1,6 +1,5 @@ from django.db import models from account.models import Profile -from common.middleware import CurrentUserMiddleware class Article(models.Model): @@ -8,7 +7,6 @@ class Article(models.Model): Profile, related_name="author", on_delete=models.DO_NOTHING, - default=CurrentUserMiddleware.get_current_user_profile ) title = models.CharField(null=False, max_length=200) text = models.TextField() @@ -17,7 +15,7 @@ class Article(models.Model): updated_by = models.ForeignKey( Profile, related_name="updater", - on_delete=models.DO_NOTHING + on_delete=models.DO_NOTHING, ) def __str__(self): diff --git a/src/news/serializers.py b/src/news/serializers.py index 514c63bd416184b25ca57672ed24a276c72a382d..7d967b59bde22839708b919f0796cfc39d716d9c 100644 --- a/src/news/serializers.py +++ b/src/news/serializers.py @@ -1,16 +1,14 @@ from news.models import Article from rest_framework import serializers -from common.serializers import CurrentUserProfileDefault class ArticleSerializer(serializers.ModelSerializer): - updated_by = serializers.HiddenField(default=CurrentUserProfileDefault()) last_update_by = serializers.SerializerMethodField() author = serializers.SerializerMethodField() class Meta: model = Article - read_only_fields = ('author', 'created_at', 'updated_at', 'updated_by') + read_only_fields = ('author', 'created_at', 'updated_at') fields = '__all__' def get_last_update_by(self, obj): diff --git a/src/news/views.py b/src/news/views.py index 764d0a93c4aeb7f6ff4a493c6db4aed6f24a6c5c..28e9e71fd1f32a36460f4580ccb672e57b684d43 100644 --- a/src/news/views.py +++ b/src/news/views.py @@ -8,3 +8,11 @@ class NewsViewSet(viewsets.ModelViewSet): serializer_class = ArticleSerializer permission_classes = (IsStaffOrReadOnly,) queryset = Article.objects.all().order_by('-created_at') + + def perform_create(self, serializer): + kwargs = { + 'author': self.request.user.profile, + 'updated_by': self.request.user.profile, + } + + serializer.save(**kwargs) diff --git a/src/stats/admin.py b/src/stats/admin.py index fb841605335f9cd12025ed91abe5361dc7f806f5..6c3f3b784e18268a87cbc7f48dd87392cb54c746 100644 --- a/src/stats/admin.py +++ b/src/stats/admin.py @@ -15,7 +15,8 @@ class EventAdmin(ExportMixin, admin.ModelAdmin): @admin.register(models.Note) class NoteAdmin(ExportMixin, admin.ModelAdmin): - list_display = ('profile', 'note', 'event', 'created_by', 'created_at', 'updated_at') + list_display = ('profile', 'note', 'event', + 'created_by', 'created_at', 'updated_at') list_filter = ('profile', 'created_by', 'event') search_fields = ('event__name', 'note') resource_class = resources.NoteResource diff --git a/src/stats/migrations/0001_initial.py b/src/stats/migrations/0001_initial.py index b6e989e8f6f784a782bf6ef238584160ca141329..8b549fd6b7a023d2c93664a43881beb02b4871fb 100644 --- a/src/stats/migrations/0001_initial.py +++ b/src/stats/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 2.0.1 on 2019-01-17 15:06 +# Generated by Django 2.2.4 on 2020-02-04 14:16 -import common.middleware from django.db import migrations, models import django.db.models.deletion @@ -20,10 +19,12 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255)), ('date', models.DateTimeField()), + ('description', models.TextField(blank=True, default='')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('created_by', models.ForeignKey(default=common.middleware.CurrentUserMiddleware.get_current_user_profile, on_delete=django.db.models.deletion.DO_NOTHING, related_name='created_event', to='account.Profile')), - ('visitors', models.ManyToManyField(blank=True, related_name='events', to='account.Profile')), + ('absent', models.ManyToManyField(blank=True, related_name='events_absent', to='account.Profile')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='created_event', to='account.Profile')), + ('visitors', models.ManyToManyField(blank=True, related_name='events_visitor', to='account.Profile')), ], ), migrations.CreateModel( @@ -33,7 +34,7 @@ class Migration(migrations.Migration): ('note', models.TextField()), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('created_by', models.ForeignKey(default=common.middleware.CurrentUserMiddleware.get_current_user_profile, on_delete=django.db.models.deletion.DO_NOTHING, related_name='created_notes', to='account.Profile')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='created_notes', to='account.Profile')), ('event', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='stats.Event')), ('profile', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='account.Profile')), ], diff --git a/src/stats/migrations/0002_event_description.py b/src/stats/migrations/0002_event_description.py deleted file mode 100644 index d7d056636e1bc6c5ad18457d09a92bc56ba30927..0000000000000000000000000000000000000000 --- a/src/stats/migrations/0002_event_description.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.1 on 2019-01-19 14:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('stats', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='event', - name='description', - field=models.TextField(blank=True, default=''), - ), - ] diff --git a/src/stats/migrations/0003_auto_20190122_1449.py b/src/stats/migrations/0003_auto_20190122_1449.py deleted file mode 100644 index a3689e8ee847704324f3821df0508d6b89ffbb0d..0000000000000000000000000000000000000000 --- a/src/stats/migrations/0003_auto_20190122_1449.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0.1 on 2019-01-22 13:49 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0002_auto_20190122_1341'), - ('stats', '0002_event_description'), - ] - - operations = [ - migrations.AddField( - model_name='event', - name='absent', - field=models.ManyToManyField(blank=True, related_name='events_absent', to='account.Profile'), - ), - migrations.AlterField( - model_name='event', - name='visitors', - field=models.ManyToManyField(blank=True, related_name='events_visitor', to='account.Profile'), - ), - ] diff --git a/src/stats/models.py b/src/stats/models.py index be958204fba61174220ddc988100c21405f44241..366c1d4b7aef6aa518521cdae18343b1a970983c 100644 --- a/src/stats/models.py +++ b/src/stats/models.py @@ -1,6 +1,5 @@ from django.db import models from account.models import Profile -from common.middleware import CurrentUserMiddleware class Event(models.Model): @@ -21,7 +20,6 @@ class Event(models.Model): Profile, related_name='created_event', on_delete=models.DO_NOTHING, - default=CurrentUserMiddleware.get_current_user_profile ) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) @@ -31,14 +29,15 @@ class Event(models.Model): class Note(models.Model): - event = models.ForeignKey(Event, related_name='notes', on_delete=models.CASCADE, blank=True, null=True) - profile = models.ForeignKey(Profile, related_name='notes', on_delete=models.CASCADE, blank=True, null=True) + event = models.ForeignKey( + Event, related_name='notes', on_delete=models.CASCADE, blank=True, null=True) + profile = models.ForeignKey( + Profile, related_name='notes', on_delete=models.CASCADE, blank=True, null=True) note = models.TextField() created_by = models.ForeignKey( Profile, related_name='created_notes', on_delete=models.DO_NOTHING, - default=CurrentUserMiddleware.get_current_user_profile ) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) diff --git a/src/stats/resources.py b/src/stats/resources.py index 712c971ae27eb0f4bebfd30be07eb952d8c5fbb7..b2e284c57094b9ec582900772bf7f007bbdd555e 100644 --- a/src/stats/resources.py +++ b/src/stats/resources.py @@ -7,7 +7,8 @@ from . import models class EventResource(resources.ModelResource): visitors = fields.Field( attribute='visitors', - widget=widgets.ManyToManyWidget(model=Profile, separator=' ,', field='full_name'), + widget=widgets.ManyToManyWidget( + model=Profile, separator=' ,', field='full_name'), ) class Meta: diff --git a/src/stats/serializers.py b/src/stats/serializers.py index e09d7b1b86fe537ddee2106ec72753d626696380..975a7d9af573980c82fa3f9aebd7aaa864968baa 100644 --- a/src/stats/serializers.py +++ b/src/stats/serializers.py @@ -22,7 +22,8 @@ class StaffEventSerializer(serializers.ModelSerializer): if 'absent' in data and 'visitors' in data: for i in data['absent']: if i in data['visitors']: - raise serializers.ValidationError('You cant add a student to absent and visitor in the same time.') + raise serializers.ValidationError( + 'You cant add a student to absent and visitor in the same time.') return data @@ -37,6 +38,15 @@ class NoteSerializer(serializers.ModelSerializer): created_by = serializers.HiddenField(default=CurrentUserProfileDefault()) created_by_name = serializers.SerializerMethodField() + def to_representation(self, instance): + data = super().to_representation(instance) + if not data['event']: + data['event'] = "" + if not data['profile']: + data['profile'] = "" + + return data + class Meta: model = models.Note fields = '__all__' @@ -47,5 +57,6 @@ class NoteSerializer(serializers.ModelSerializer): def validate(self, data): if data['profile'] is None and data['event'] is None: - raise serializers.ValidationError('You have to add profile or event') + raise serializers.ValidationError( + 'You have to add profile or event') return data diff --git a/src/stats/urls.py b/src/stats/urls.py index 0e669f8037c98287185212ecbe0c2b43c03bd267..51ed16131f024b07b2401965526d8855ff2cdd35 100644 --- a/src/stats/urls.py +++ b/src/stats/urls.py @@ -2,8 +2,10 @@ from rest_framework import routers from . import views router = routers.DefaultRouter() -router.register(r'staff_events', views.StaffEventViewSet, base_name='staff_events') -router.register(r'student_events', views.StudentEventViewSet, base_name='student_events') +router.register(r'staff_events', views.StaffEventViewSet, + base_name='staff_events') +router.register(r'student_events', views.StudentEventViewSet, + base_name='student_events') router.register(r'notes', views.NoteViewSet, base_name='notes') urlpatterns = router.urls diff --git a/src/stats/views.py b/src/stats/views.py index c917e47dd8b96927fe4f1ec018975c84ed6656b5..f1e5c802775b4728575577f9311e1e533a29488d 100644 --- a/src/stats/views.py +++ b/src/stats/views.py @@ -9,12 +9,26 @@ class StaffEventViewSet(viewsets.ModelViewSet): queryset = models.Event.objects.all().order_by('date') permission_classes = (IsStaffUser, ) + def perform_create(self, serializer): + kwargs = { + 'created_by': self.request.user.profile + } + + serializer.save(**kwargs) + class StudentEventViewSet(viewsets.ModelViewSet): serializer_class = serializers.StudentEventSerializer queryset = models.Event.objects.all().order_by('date') permission_classes = (IsStaffOrStudent, ) + def perform_create(self, serializer): + kwargs = { + 'created_by': self.request.user.profile + } + + serializer.save(**kwargs) + class NoteViewSet(viewsets.ModelViewSet): serializer_class = serializers.NoteSerializer @@ -31,3 +45,10 @@ class NoteViewSet(viewsets.ModelViewSet): if event_id is not None: return queryset.filter(event=event_id) return queryset + + def perform_create(self, serializer): + kwargs = { + 'created_by': self.request.user.profile + } + + serializer.save(**kwargs)