Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TaskManagerSystem/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ def __call__(self, request):
return HttpResponse('Payload Too Large', status=413)
except (ValueError, TypeError):
pass
return self.get_response(request)
return self.get_response(request)
20 changes: 8 additions & 12 deletions TaskManagerSystem/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
AUTH_USER_MODEL = 'users.User'

# Security settings
SECURE_BROWSER_XSS_FILTER = True # enable X-XSS-Protection
SECURE_BROWSER_XSS_FILTER = True # enable X-XSS-Protection
SECURE_CONTENT_TYPE_NOSNIFF = True # enable X-Content-Type-Options
X_FRAME_OPTIONS = 'DENY' # prevent clickjacking
X_FRAME_OPTIONS = 'DENY' # prevent clickjacking


ADMIN_ROLE_NAMES = ["Admin",]
Expand Down Expand Up @@ -55,7 +55,7 @@
}
ROLE_ORDER = tuple(reversed(ROLE_PERMISSIONS.keys()))

DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024 * 25 # 25 Mb restriction
DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024 * 25 # 25 Mb restriction
API_VERSION = '1.0.0'

DJANGO_APPS = [
Expand All @@ -65,7 +65,7 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.test"
"django.test",
]

THIRD_PARTY_APPS = [
Expand All @@ -76,11 +76,7 @@
"corsheaders",
]

LOCAL_APPS = [
"users",
"tasks",
"projects"
]
LOCAL_APPS = ["users", "tasks", "projects"]

# Application definition
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
Expand Down Expand Up @@ -154,7 +150,7 @@
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
'UNAUTHENTICATED_USER': None,
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle', # not auth request
'rest_framework.throttling.AnonRateThrottle', # not auth request
'rest_framework.throttling.UserRateThrottle'
],
"DEFAULT_THROTTLE_RATES": {"anon": "10000/minute", "user": "10000/minute"},
Expand Down Expand Up @@ -191,8 +187,8 @@
}

# Logging
LOG_DIR = BASE_DIR / 'logs'
LOG_DIR.mkdir(exist_ok=True)
LOG_DIR = BASE_DIR / 'logs'
LOG_DIR.mkdir(exist_ok=True)
LOGGING = {
'version': 1, # version of the logging configuration
'disable_existing_loggers': False, # dont disable existing loggers
Expand Down
2 changes: 1 addition & 1 deletion TaskManagerSystem/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]

urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
5 changes: 3 additions & 2 deletions TaskManagerSystem/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

logger = logging.getLogger(__name__)


def custom_exception_handler(exc, context):
"""
Custom handler for exceptions in the API
Expand All @@ -23,8 +24,8 @@ def custom_exception_handler(exc, context):
else:
logger.error(f"Unhandled exception: {exc}", exc_info=True)
return error_response(
"Internal server error. Please try again later",
status.HTTP_500_INTERNAL_SERVER_ERROR
"Internal server error. Please try again later",
status.HTTP_500_INTERNAL_SERVER_ERROR,
)

return response
2 changes: 1 addition & 1 deletion api/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from django.contrib import admin
# from django.contrib import admin

# Register your models here.
1 change: 1 addition & 0 deletions api/apps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.apps import AppConfig


class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'
22 changes: 13 additions & 9 deletions api/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,28 @@ class UserQuerysetMixin:

def get_queryset(self):
base_queryset = super().get_queryset()
model = getattr(base_queryset, 'model', None)
model = getattr(base_queryset, "model", None)
if not model:
raise ImproperlyConfigured("Queryset has a model for filtering")
if hasattr(model, 'owner'):
if hasattr(model, "owner"):
return base_queryset.filter(owner=self.request.user)
if hasattr(model, 'user'):
if hasattr(model, "user"):
return base_queryset.filter(user=self.request.user)
raise ImproperlyConfigured(f"Model {model.__name__} does not have owner or user fields")
raise ImproperlyConfigured(
f"Model {model.__name__} does not have owner or user fields"
)

def perform_create(self, serializer):
logger.debug(
f"Creating object {serializer.Meta.model.__name__} by user_id={self.request.user.pk}"
)
save_kwargs = {}
if hasattr(serializer.Meta.model, 'owner'):
save_kwargs['owner'] = self.request.user
elif hasattr(serializer.Meta.model, 'user'):
save_kwargs['user'] = self.request.user
if hasattr(serializer.Meta.model, "owner"):
save_kwargs["owner"] = self.request.user
elif hasattr(serializer.Meta.model, "user"):
save_kwargs["user"] = self.request.user
else:
raise ImproperlyConfigured("The model does not support user-based creation")
raise ImproperlyConfigured(
"The model does not support user-based creation"
)
serializer.save(**save_kwargs)
2 changes: 1 addition & 1 deletion api/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from django.db import models
# from django.db import models

# Create your models here.
7 changes: 4 additions & 3 deletions api/tests/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class BaseAPITestCase(APITestCase):
setUp(): Configure test client
api_post(): Make authenticated POST request
"""

@classmethod
def setUpTestData(cls):
cls.user, cls.token, cls.refresh = TestHelper.create_test_user_via_orm()
Expand All @@ -33,12 +34,12 @@ def setUpTestData(cls):
cls.user_update_profile_ep = reverse("user-update-profile")
cls.project_list_ep = reverse("project-list")
cls.role_list_ep = reverse("role-list")

def setUp(self):
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.token}")

def api_post(self, endpoint: str, data: dict, token: Optional[str] = None):
return self.client.post(
endpoint, data, format="json",
HTTP_AUTHORIZATION=f"Bearer {token or getattr(self, 'token', '')}"
)
)
8 changes: 4 additions & 4 deletions api/tests/test_tasks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime, timedelta
from datetime import timedelta
from django.urls import reverse
from django.utils.timezone import now, make_aware
from django.utils.timezone import now
from rest_framework import status

from projects.models import Project
Expand Down Expand Up @@ -406,10 +406,10 @@ def test_filter_by_today(self):

self.future_task.due_date = now() + timedelta(days=1)
self.future_task.save()

self.today_task.due_date = now() + timedelta(days=2)
self.today_task.save()

response = self.client.get(self.task_list_ep, {"today": "true"})

self.assertEqual(
Expand Down
38 changes: 25 additions & 13 deletions api/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,24 @@

User = get_user_model()


class TokenService:
"""
Utility class for generating tokens

Methods:
generate_tokens_for: Generates access and refresh tokens for the specified user
"""

@staticmethod
def generate_tokens_for(user):
refresh_obj = RefreshToken.for_user(user)
return {
'access': str(refresh_obj.access_token),
'refresh': str(refresh_obj)
"access": str(refresh_obj.access_token),
"refresh": str(refresh_obj),
}


class TestHelper:
@staticmethod
def create_test_user(client, email="testuser@example.com", password="testpassword123"):
Expand All @@ -30,22 +33,27 @@ def create_test_user(client, email="testuser@example.com", password="testpasswor

Args:
client: The test client instance.
email (str): The email address of the test user. Defaults to "testuser@example.com"
password (str): The password of the test user. Defaults to "testpassword123"
email (str): The email address of the test user.
Defaults to "testuser@example.com"
password (str): The password of the test user.
Defaults to "testpassword123"

Returns:
tuple: A tuple containing the created user instance, access token, and refresh token
tuple: A tuple containing the created user instance, access token, and
refresh token

Raises:
AssertionError: If user registration or login fails
"""
user_data = {"email": email, "password": password}
response = client.post(reverse("auth-register"), user_data)
assert response.status_code == status.HTTP_201_CREATED, f"User registration failed: {response.data}"
if response.status_code != status.HTTP_201_CREATED:
raise AssertionError(f"User registration failed: {response.data}")

user = User.objects.get(email=email)
response = client.post(reverse("auth-login"), user_data)
assert response.status_code == status.HTTP_200_OK, f"User login failed: {response.data}"
if response.status_code != status.HTTP_200_OK:
raise AssertionError(f"User login failed: {response.data}")

token = response.data.get("access")
refresh = response.data.get("refresh")
Expand All @@ -58,18 +66,21 @@ def create_test_user_via_orm(email="testuser@example.com", password="testpasswor
Creates a test user via ORM

Args:
email (str): The email address of the test user. Defaults to "testuser@example.com"
password (str): The password of the test user. Defaults to "testpassword123"
email (str): The email address of the test user.
Defaults to "testuser@example.com"
password (str): The password of the test user.
Defaults to "testpassword123"

Returns:
tuple: A tuple containing the created user instance, access token, and refresh token
tuple: A tuple containing the created user instance, access token,
and refresh token
"""
user = User.objects.create_user(
username=email.split("@")[0], email=email, password=password
)
tokens = TokenService.generate_tokens_for(user)
return user, tokens['access'], tokens['refresh']

return user, tokens["access"], tokens["refresh"]

@staticmethod
def get_valid_due_date(days: int = 14):
Expand All @@ -80,7 +91,8 @@ def get_valid_due_date(days: int = 14):
days (int): The number of days from the current date. Defaults to 14

Returns:
str: A future date in ISO format (YYYY-MM-DDTHH:MM:SSZ) with microseconds set to 0
str: A future date in ISO format (YYYY-MM-DDTHH:MM:SSZ)
with microseconds set to 0
"""
future_date = timezone.now() + timedelta(days=days)
return future_date.replace(microsecond=0).isoformat() + "Z"
4 changes: 2 additions & 2 deletions api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
path("status/", api_status),
path("account/", include("users.urls")),
path("tasks/", include("tasks.urls")),
path("projects/", include("projects.urls"))
]
path("projects/", include("projects.urls")),
]
17 changes: 10 additions & 7 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
from rest_framework.permissions import AllowAny
from rest_framework.response import Response

@api_view(['GET'])

@api_view(["GET"])
@permission_classes([AllowAny])
def api_status(request):
return Response({
"status": "ok",
"version": getattr(settings, 'API_VERSION', 'dev'),
"date": datetime.now(),
"message": "TaskManager API is up and running"
})
return Response(
{
"status": "ok",
"version": getattr(settings, "API_VERSION", "dev"),
"date": datetime.now(),
"message": "TaskManager API is up and running",
}
)
2 changes: 1 addition & 1 deletion projects/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.contrib import admin
from .models import Role, Project, ProjectMembership, ProjectShareLink

admin.site.register([Role, Project, ProjectMembership, ProjectShareLink])
admin.site.register([Role, Project, ProjectMembership, ProjectShareLink])
4 changes: 2 additions & 2 deletions projects/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ def ready(self):
def create_roles_and_permissions(self, **kwargs):
from django.contrib.auth.models import Permission
from .models import Role

for role_name, perm_codenames in settings.ROLE_PERMISSIONS.items():
role, created = Role.objects.get_or_create(name=role_name)
role, _ = Role.objects.get_or_create(name=role_name)
perms = Permission.objects.filter(codename__in=perm_codenames)
role.permissions.set(perms)
role.save()
10 changes: 7 additions & 3 deletions projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

class Project(models.Model):
"""Project with name, description, and owner"""

name = models.CharField(max_length=128, validators=[TEXT_FIELD_VALIDATOR])
description = models.TextField(blank=True)
owner = models.ForeignKey(
Expand All @@ -30,16 +31,17 @@ def __str__(self):

class Role(models.Model):
"""Static project roles; prohibition to create custom ones"""

name = models.CharField(max_length=64, unique=True)
permissions = models.ManyToManyField(Permission, blank=True)

def clean(self):
fixed = settings.ROLE_ORDER
if self.name not in fixed:
raise ValidationError(
f"Custom roles are not allowed. Use one of: {', '.join(fixed)}"
)

def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
Expand All @@ -49,7 +51,8 @@ def __str__(self):


class ProjectMembership(models.Model):
"""The relationship 'user --> project --> role' """
"""The relationship 'user --> project --> role'"""

user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
Expand All @@ -71,6 +74,7 @@ def __str__(self):

class ProjectShareLink(models.Model):
"""Token for invitation to the project with a limits"""

token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
project = models.ForeignKey(
Project, on_delete=models.CASCADE, related_name="share_links"
Expand Down
Loading