diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e81d0acc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.pythonPath": "../.env/bin/python", + "python.formatting.provider": "yapf" +} \ No newline at end of file diff --git a/DjangoScheduler/.gitignore b/DjangoScheduler/.gitignore new file mode 100644 index 00000000..75c689e2 --- /dev/null +++ b/DjangoScheduler/.gitignore @@ -0,0 +1,118 @@ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so +main/migrations/ +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ +.vscode +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +.DS_Store +*.sqlite3 +media/ +*.pyc +*.db +*.pid + +# Ignore Django Migrations in Development if you are working on team + +# Only for Development only +# **/migrations/** +# !**/migrations +# !**/migrations/__init__.py +notes.txt +../notes.txt diff --git a/DjangoScheduler/DjangoScheduler/__init__.py b/DjangoScheduler/DjangoScheduler/__init__.py new file mode 100644 index 00000000..d128d39c --- /dev/null +++ b/DjangoScheduler/DjangoScheduler/__init__.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import, unicode_literals + +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ('celery_app',) \ No newline at end of file diff --git a/DjangoScheduler/DjangoScheduler/asgi.py b/DjangoScheduler/DjangoScheduler/asgi.py new file mode 100644 index 00000000..d7155112 --- /dev/null +++ b/DjangoScheduler/DjangoScheduler/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for DjangoScheduler project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoScheduler.settings') + +application = get_asgi_application() diff --git a/DjangoScheduler/DjangoScheduler/celery.py b/DjangoScheduler/DjangoScheduler/celery.py new file mode 100644 index 00000000..583fd49e --- /dev/null +++ b/DjangoScheduler/DjangoScheduler/celery.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import, unicode_literals +import os +from celery import Celery +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoScheduler.settings') + +app = Celery('DjangoScheduler') + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object('django.conf:settings', namespace='CELERY') + +# Load task modules from all registered Django app configs. +app.autodiscover_tasks() + + +@app.task(bind=True) +def debug_task(self): + print('Request: {0!r}'.format(self.request)) diff --git a/DjangoScheduler/DjangoScheduler/controller.py b/DjangoScheduler/DjangoScheduler/controller.py new file mode 100644 index 00000000..8e514068 --- /dev/null +++ b/DjangoScheduler/DjangoScheduler/controller.py @@ -0,0 +1,29 @@ +from scheduler.models import Task as schedulerTask +from main.models import * +from scheduler.tasks import send_email_task + +from celery.result import AsyncResult + + +class Controller(): + + def scheduleTask(self, task): + # scheduling & adding task to db & Queue + result = send_email_task.apply_async( + args=[task.title, + task.description, + task.owner.email], + eta=task.timeToSend) + + id = result.task_id + schedulerTask.objects.create(uuid=id, data=task) + + def updateTask(self, task): + + # revoking & deleting old task + old_task = schedulerTask.objects.get(data__pk=task.pk) + AsyncResult(old_task.uuid).revoke() + old_task.delete() + + # reScheduling task + self.scheduleTask(task) diff --git a/DjangoScheduler/DjangoScheduler/settings.py b/DjangoScheduler/DjangoScheduler/settings.py new file mode 100644 index 00000000..98969e09 --- /dev/null +++ b/DjangoScheduler/DjangoScheduler/settings.py @@ -0,0 +1,180 @@ +""" +Django settings for DjangoScheduler project. + +Generated by 'django-admin startproject' using Django 3.1.6. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.1/ref/settings/ +""" + +from datetime import timedelta +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '^^--akxxwiep=370c_33gl8g1xy&8&*(_rx__7^p1t1-#d+9i%' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'main', + 'scheduler' +] +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.RemoteUserBackend', + 'django.contrib.auth.backends.ModelBackend', +) +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.RemoteUserMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'DjangoScheduler.urls' + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ], +} + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'DjangoScheduler.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +AUTH_USER_MODEL = 'main.CustomUser' + +# Password validation +# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'Asia/Tehran' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=500), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=10), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + 'UPDATE_LAST_LOGIN': False, + + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUDIENCE': None, + 'ISSUER': None, + + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', + + 'JTI_CLAIM': 'jti', + + 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', + 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=500), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=10), +} + + +# celery configuration +CELERY_BROKER_URL = "amqp://localhost" +CELERY_TIMEZONE = "Asia/Tehran" +CELERY_TASK_TRACK_STARTED = True +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_BACKEND = 'rpc://' +CELERY_RESULT_PRESISTANT = False + +# smtp configuration +EMAIL_HOST = 'smtp.fastmail.com' +EMAIL_PORT = 465 +EMAIL_HOST_USER = 'bmhztest@fastmail.com' +EMAIL_HOST_PASSWORD = '' +EMAIL_USE_TLS = False +EMAIL_USE_SSL = True + + +STATIC_URL = '/static/' +APPEND_SLASH = False diff --git a/DjangoScheduler/DjangoScheduler/urls.py b/DjangoScheduler/DjangoScheduler/urls.py new file mode 100644 index 00000000..43104de3 --- /dev/null +++ b/DjangoScheduler/DjangoScheduler/urls.py @@ -0,0 +1,24 @@ +"""DjangoScheduler URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.conf.urls import url +from django.urls import path,include + +urlpatterns = [ + path('admin/', admin.site.urls), + url(r'^api/v1/', include('main.urls')), + +] diff --git a/DjangoScheduler/DjangoScheduler/wsgi.py b/DjangoScheduler/DjangoScheduler/wsgi.py new file mode 100644 index 00000000..c2f8aaa4 --- /dev/null +++ b/DjangoScheduler/DjangoScheduler/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for DjangoScheduler project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoScheduler.settings') + +application = get_wsgi_application() diff --git a/DjangoScheduler/main/__init__.py b/DjangoScheduler/main/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/DjangoScheduler/main/admin.py b/DjangoScheduler/main/admin.py new file mode 100644 index 00000000..7f26cf70 --- /dev/null +++ b/DjangoScheduler/main/admin.py @@ -0,0 +1,43 @@ +from django.contrib import admin +from .models import CustomUser, Task +from django.forms import ModelForm +from django import forms +from .models import CustomUser +from django.contrib.admin.widgets import AdminSplitDateTime + +# Register your models here. + +class TaskFormNormalUser(ModelForm): + class Meta: + model = Task + fields = ('title', 'description', 'timeToSend', 'owner') + widgets = { + 'timeToSend': AdminSplitDateTime(), + } + + +@admin.register(CustomUser) +class CustomUserAdmin(admin.ModelAdmin): + filter_horizontal = ['groups', 'user_permissions'] + list_display = ('username', 'role',) + + +@admin.register(Task) +class TaskAdmin(admin.ModelAdmin): + list_display = ('title', 'owner', 'timeToSend') + + def render_change_form(self, request, context, *args, **kwargs): + if not request.user.is_superuser: + context['adminform'].form.fields['owner'].queryset = CustomUser.objects.filter( + username=request.user.username) + return super(TaskAdmin, self).render_change_form(request, context, *args, **kwargs) + + def get_changeform_initial_data(self, request): + data = super(TaskAdmin, self).get_changeform_initial_data(request) + return data + + def get_queryset(self, request): + qs = super(TaskAdmin, self).get_queryset(request) + if request.user.is_superuser: + return qs + return qs.filter(owner=request.user) diff --git a/DjangoScheduler/main/apps.py b/DjangoScheduler/main/apps.py new file mode 100644 index 00000000..833bff67 --- /dev/null +++ b/DjangoScheduler/main/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MainConfig(AppConfig): + name = 'main' diff --git a/DjangoScheduler/main/helpers.py b/DjangoScheduler/main/helpers.py new file mode 100644 index 00000000..2c92a320 --- /dev/null +++ b/DjangoScheduler/main/helpers.py @@ -0,0 +1,12 @@ +from rest_framework_simplejwt.tokens import RefreshToken + +GROUP_ROLES = {'A': 'admin', 'N': 'normal'} +GROUP_ROLES_REVERSED = {'admin': 'A', 'normal': 'N'} + + +def get_token(CustomUser): + refresh = RefreshToken.for_user(CustomUser) + return { + 'refresh': str(refresh), + 'access': str(refresh.access_token), + } diff --git a/DjangoScheduler/main/models.py b/DjangoScheduler/main/models.py new file mode 100644 index 00000000..83199cc8 --- /dev/null +++ b/DjangoScheduler/main/models.py @@ -0,0 +1,37 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import Group +from DjangoScheduler.controller import Controller + + +class CustomUser(AbstractUser): + roleChoices = (("A", "admin"), ('N', "normal")) + first_name = models.CharField(max_length=10) + last_name = models.CharField(max_length=10) + role = models.CharField(max_length=2, choices=roleChoices, default='N') + password = models.CharField(max_length=128) + email = models.EmailField(unique=True) + + +class Task(models.Model): + title = models.CharField(max_length=30) + description = models.TextField(max_length=500) + owner = models.ForeignKey(CustomUser, on_delete=models.CASCADE) + timeToSend = models.DateTimeField() + + def __str__(self): + return '{}-{}-{}'.format(self.title,self.owner.first_name,self.timeToSend) + + + def save(self, *args, **kwargs): + + controller = Controller() + + print(self.timeToSend) + + if self.pk: + controller.updateTask(self) + super().save(*args, **kwargs) + else: + super().save(*args, **kwargs) + controller.scheduleTask(self) diff --git a/DjangoScheduler/main/serializers.py b/DjangoScheduler/main/serializers.py new file mode 100644 index 00000000..70480f28 --- /dev/null +++ b/DjangoScheduler/main/serializers.py @@ -0,0 +1,57 @@ +from rest_framework import serializers +from .models import CustomUser, Task +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import Group +from .helpers import GROUP_ROLES,GROUP_ROLES_REVERSED +from . import helpers + + +class CreateUser(serializers.ModelSerializer): + token = serializers.SerializerMethodField() + + class Meta: + model = CustomUser + fields = ('first_name', + 'last_name', + 'username', + 'password', + 'email', + 'role', + 'token') + + read_only_fields = ('token',) + + def get_token(self, CustomUser): + return helpers.get_token(CustomUser) + + def create(self, validated_data): + + role = None + + if 'role' in validated_data: + role = validated_data['role'] + else: + role = 'N' + + user = CustomUser.objects.create(first_name=validated_data['first_name'], + last_name=validated_data['last_name'], + username=validated_data['username'], + email=validated_data['email'], + role=role, + is_superuser= (role==GROUP_ROLES_REVERSED['admin']), + is_staff=True, + password=make_password(validated_data['password'])) + + try: + user.groups.add(Group.objects.get(name=GROUP_ROLES[role])) + except Exception as e: + print(e) + + user.save() + return user + + +class GetTasks(serializers.ModelSerializer): + class Meta: + model = Task + fields = '__all__' diff --git a/DjangoScheduler/main/tests/__init__.py b/DjangoScheduler/main/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/DjangoScheduler/main/tests/test_views.py b/DjangoScheduler/main/tests/test_views.py new file mode 100644 index 00000000..f6f76e96 --- /dev/null +++ b/DjangoScheduler/main/tests/test_views.py @@ -0,0 +1,195 @@ +import json +import re +from operator import add +from unittest import mock +from unittest.mock import patch +from django.contrib.auth.models import User, Permission, Group + +from django.contrib.auth import authenticate, login +from django.urls import reverse +from datetime import datetime +from rest_framework import status +from main.models import * +from rest_framework.test import APITestCase +from django.contrib.auth.hashers import make_password + + +class TestViews(APITestCase): + + @classmethod + def setUpTestData(cls): + + cls.username = "dummyUsermane" + cls.password = "dummyPassword" + cls.normalUsername = "nornalUsername" + cls.normalEmail = "normalEmail@mail.com" + cls.firstName = "dummy" + cls.lastName = "dummy" + cls.email = "dummy@dummy.com" + + cls.user1 = CustomUser.objects.create(username=cls.username, password=make_password(cls.password), + email=cls.email, role='A', first_name=cls.firstName, + last_name=cls.lastName) + + cls.user2 = CustomUser.objects.create(username='username2', password=make_password(cls.password), + email='email2@dummy.com', role='A', first_name=cls.firstName, + last_name=cls.lastName) + + cls.normalUser = CustomUser.objects.create(username=cls.normalUsername, password=make_password(cls.password), + email=cls.normalEmail, role='N', first_name=cls.firstName, + last_name=cls.lastName) + + cls.taskUser1 = Task.objects.create(title='title', description='des', + owner=cls.user1, timeToSend=datetime.now()) + + cls.taskUser2 = Task.objects.create(title='title', description='des', + owner=cls.user2, timeToSend=datetime.now()) + + cls.task1NormalUser = Task.objects.create(title='title', description='des', + owner=cls.normalUser, timeToSend=datetime.now()) + + cls.task2NormalUser = Task.objects.create(title='title', description='des', + owner=cls.normalUser, timeToSend=datetime.now()) + + cls.adminGroup = Group.objects.create(name='admin') + cls.adminGroup.permissions.set(Permission.objects.all()) + cls.normalGroup = Group.objects.create(name='normal') + + taskPermissions = Permission.objects.filter(content_type__app_label='main', content_type__model='task') + # permissiosn coresponds to view,change permissions + cls.normalGroup.permissions.set([taskPermissions[1],taskPermissions[3]]) + + def testSmokeTest(self): + self.assertEquals(1, 1) + + def testLoginUserWithInvalidParametersShouldReturn400(self): + response = self.client.post(reverse( + 'login'), content_type="Application/json", data=json.dumps({'someDummy': 'hello'})) + self.assertEqual(response.status_code, 400) + self.assertIsNotNone(response.content) + self.assertFalse('access' in json.loads(response.content)) + self.assertFalse('refresh' in json.loads(response.content)) + + def testLoginUserWithIncorrentCredentialsShouldReturn401(self): + response = self.client.post( + reverse('login'), content_type="Application/json", data=json.dumps({'username': 'hello', 'password': 'password'})) + self.assertEqual(response.status_code, 401) + self.assertIsNotNone(response.content) + self.assertFalse('access' in json.loads(response.content)) + self.assertFalse('refresh' in json.loads(response.content)) + + def testLoginUserWithCorrentCredentialsShouldReturnToken200(self): + response = self.client.post( + reverse('login'), content_type="Application/json", data=json.dumps({"username": self.username, + "password": self.password})) + + self.assertEqual(response.status_code, 200) + self.assertIsNotNone(response.content) + self.assertTrue('access' in json.loads(response.content)) + self.assertTrue('refresh' in json.loads(response.content)) + + def testSignUpUserWithInvalidParametersShouldReturn400(self): + response = self.client.post( + reverse('signUp'), content_type="Application/json", data=json.dumps({"username": self.username, + "password": self.password, + })) + + self.assertEqual(response.status_code, 400) + self.assertIsNotNone(response.content) + + def testSignUpUserWithInvalidEmailShouldReturn400(self): + + response = self.client.post( + reverse('signUp'), content_type="Application/json", data=json.dumps({"username": self.username, + "password": self.password, + "first_name": "first", + "last_name": "last", + "email": "InvalidEmail", + "role": "N"})) + + self.assertEqual(response.status_code, 400) + self.assertIsNotNone(response.content) + + def testSignUpUserWithAlreadyRegisterdUsernameShouldReturn400(self): + + response = self.client.post( + reverse('signUp'), content_type="Application/json", data=json.dumps({"username": self.username, + "password": self.password, + "first_name": "first", + "last_name": "last", + "email": "valid@valid.com", + "role": "N"})) + + self.assertEqual(response.status_code, 400) + self.assertIsNotNone(response.content) + + def testSignUpUserWithAlreadyRegisterdEmailShouldReturn400(self): + + response = self.client.post( + reverse('signUp'), content_type="Application/json", data=json.dumps({"username": self.username, + "password": self.password, + "first_name": "first", + "last_name": "last", + "email": self.email, + "role": "N"})) + + self.assertEqual(response.status_code, 400) + self.assertIsNotNone(response.content) + + def testSignUpUserWithValidCrednetialsShouldCreateNewUser201(self): + + response = self.client.post( + reverse('signUp'), content_type="Application/json", data=json.dumps({"username": "newuser", + "password": self.password, + "first_name": "first", + "last_name": "last", + "email": "self.email@email.com", + "role": "N"})) + + self.assertEqual(response.status_code, 201) + self.assertIsNotNone(response.content) + self.assertTrue('token' in json.loads(response.content)) + + def testViewTaskWithUnAuthenticatedUserShouldReturn401(self): + response = self.client.get( + reverse('tasks'), content_type="Application/json") + + self.assertEqual(response.status_code, 401) + + def testViewTaskWithInvalidTokenShouldReturn401(self): + headers = {"Authorization": "Bearer eyJ0eXAiOidJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjEzMDg4NjUwLCJqdGkiOiJhYmZjYjBlOWFlNTU0OTkzOGI2YzVmMWEwMzQwNTMwMSIsInVzZXJfaWQiOjV9.bGQzh4JO5pRD9W8U1UPJBO2wTPeXogVtTKGmM_UvmyY"} + response = self.client.get( + reverse('tasks'), headers=headers, content_type="Application/json") + + self.assertEqual(response.status_code, 401) + + def testViewTaskWithValidTokenForAdminUserShouldReturn200(self): + tokenResponse = self.client.post( + reverse('login'), content_type="Application/json", data=json.dumps({"username": self.username, "password": self.password})) + token = json.loads(tokenResponse.content)['access'] + headers = {"Authorization": "Bearer "+token} + self.client.credentials( + HTTP_AUTHORIZATION='Bearer {}'.format(token)) + + response = self.client.get( + reverse('tasks'), content_type="Application/json") + + self.assertEqual(response.status_code, 200) + self.assertIsNotNone(response.content) + + def testViewTaskWithValidTokenForNonAdminUserShouldReturn200OnlyOwnTasks(self): + tokenResponse = self.client.post( + reverse('login'), content_type="Application/json", data=json.dumps({"username": self.normalUsername, "password": self.password})) + token = json.loads(tokenResponse.content)['access'] + self.client.credentials( + HTTP_AUTHORIZATION='Bearer {}'.format(token)) + + response = self.client.get( + reverse('tasks'), content_type="Application/json") + self.assertEqual(response.status_code, 200) + + # checking if owner of taks points to only this user + data = json.loads(response.content) + for item in data: + self.assertEquals(item['owner'],self.normalUser.pk) + diff --git a/DjangoScheduler/main/urls.py b/DjangoScheduler/main/urls.py new file mode 100644 index 00000000..a1d6132a --- /dev/null +++ b/DjangoScheduler/main/urls.py @@ -0,0 +1,11 @@ +from main.views import * +from django.conf.urls import url +from rest_framework_simplejwt import views as jwt_views +from django.urls import path + +urlpatterns = [ + url(r'^signUp/$', SignUpUser.as_view(),name='signUp'), + url(r'^login/$', jwt_views.TokenObtainPairView.as_view(), + name='login'), + url(r'^tasks/$', GetTasks.as_view(),name='tasks'), +] diff --git a/DjangoScheduler/main/views.py b/DjangoScheduler/main/views.py new file mode 100644 index 00000000..ee5cc364 --- /dev/null +++ b/DjangoScheduler/main/views.py @@ -0,0 +1,27 @@ +from django.shortcuts import render +from rest_framework.generics import ListAPIView, CreateAPIView +from .serializers import * +from .models import * +from .helpers import GROUP_ROLES, GROUP_ROLES_REVERSED +from rest_framework.permissions import IsAuthenticated +from rest_framework.exceptions import APIException +from django.http import HttpResponse +from DjangoScheduler.celery import app +from datetime import datetime + +class GetTasks(ListAPIView): + permission_classes = (IsAuthenticated,) + serializer_class = GetTasks + + def get_queryset(self): + user = self.request.user + if user.role == GROUP_ROLES_REVERSED['admin']: + return Task.objects.all() + else: + return Task.objects.filter(owner=user) + + + +class SignUpUser(CreateAPIView): + queryset = CustomUser.objects.all() + serializer_class = CreateUser diff --git a/DjangoScheduler/manage.py b/DjangoScheduler/manage.py new file mode 100755 index 00000000..e246ac15 --- /dev/null +++ b/DjangoScheduler/manage.py @@ -0,0 +1,22 @@ +#!/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', 'DjangoScheduler.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() diff --git a/DjangoScheduler/requirements.txt b/DjangoScheduler/requirements.txt new file mode 100644 index 00000000..944fb955 --- /dev/null +++ b/DjangoScheduler/requirements.txt @@ -0,0 +1,26 @@ +amqp==5.0.5 +asgiref==3.3.1 +autopep8==1.5.5 +billiard==3.6.3.0 +celery==5.0.5 +click==7.1.2 +click-didyoumean==0.0.3 +click-plugins==1.1.1 +click-repl==0.1.6 +Django==3.1.6 +django-celery-beat==2.2.0 +django-timezone-field==4.1.1 +djangorestframework==3.12.2 +djangorestframework-simplejwt==4.6.0 +kombu==5.0.2 +prompt-toolkit==3.0.16 +pycodestyle==2.6.0 +PyJWT==2.0.1 +python-crontab==2.5.1 +python-dateutil==2.8.1 +pytz==2021.1 +six==1.15.0 +sqlparse==0.4.1 +toml==0.10.2 +vine==5.0.0 +wcwidth==0.2.5 diff --git a/DjangoScheduler/scheduler/__init__.py b/DjangoScheduler/scheduler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/DjangoScheduler/scheduler/admin.py b/DjangoScheduler/scheduler/admin.py new file mode 100644 index 00000000..b243de96 --- /dev/null +++ b/DjangoScheduler/scheduler/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import Task +# Register your models here. +admin.site.register(Task) \ No newline at end of file diff --git a/DjangoScheduler/scheduler/apps.py b/DjangoScheduler/scheduler/apps.py new file mode 100644 index 00000000..60fb286a --- /dev/null +++ b/DjangoScheduler/scheduler/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SchedulerConfig(AppConfig): + name = 'scheduler' diff --git a/DjangoScheduler/scheduler/migrations/0001_initial.py b/DjangoScheduler/scheduler/migrations/0001_initial.py new file mode 100644 index 00000000..b4e11609 --- /dev/null +++ b/DjangoScheduler/scheduler/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.6 on 2021-02-12 14:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('main', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Task', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.CharField(max_length=36)), + ('data', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.task')), + ], + ), + ] diff --git a/DjangoScheduler/scheduler/migrations/__init__.py b/DjangoScheduler/scheduler/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/DjangoScheduler/scheduler/models.py b/DjangoScheduler/scheduler/models.py new file mode 100644 index 00000000..78dd58eb --- /dev/null +++ b/DjangoScheduler/scheduler/models.py @@ -0,0 +1,11 @@ +from django.db import models + +# Create your models here. + + +class Task(models.Model): + uuid = models.CharField(max_length=36) + data = models.ForeignKey(to='main.Task', on_delete=models.CASCADE) + + def __str__(self): + return '{}'.format(self.data.title) \ No newline at end of file diff --git a/DjangoScheduler/scheduler/tasks.py b/DjangoScheduler/scheduler/tasks.py new file mode 100644 index 00000000..f6b94708 --- /dev/null +++ b/DjangoScheduler/scheduler/tasks.py @@ -0,0 +1,14 @@ +from celery import shared_task + +from django.core.mail import send_mail +from time import sleep + + +@shared_task() +def send_email_task(title, description, email): + send_mail(title, + description, + 'bmhztest@fastmail.com', + [email]) + + return 'DONE' diff --git a/DjangoScheduler/scheduler/tests.py b/DjangoScheduler/scheduler/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/DjangoScheduler/scheduler/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/DjangoScheduler/scheduler/views.py b/DjangoScheduler/scheduler/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/DjangoScheduler/scheduler/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/notes.txt b/notes.txt new file mode 100644 index 00000000..7131ce31 --- /dev/null +++ b/notes.txt @@ -0,0 +1,18 @@ +app for scheduling tasks for users. + +- tasks ---> title, description, owner, timeToSend + +- admin users(abstract user) ----> CURD user tasks --- when admins changes tasks schedules tasks should be updated or created + +- normal users(abstract user) --->{email, username, password, firstName, lastName, Permissions} see filter and add to their own tasks + + +- api authentication (login/signup) + +- api for getting all tasks(according to user permission)/JWT + +- implement unit-testing for authentication api + +-------------------------------------------------- +abstract user models with groups and permissiosn assigned to groups + * make script for fistTime run to craeting groups with permissions