Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Required Skills for Task - evaluation logic #41

Merged
merged 6 commits into from
Nov 30, 2024
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
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.py]
indent_size = 4
indent_style = space

[*.{yaml,yml}]
indent_size = 2
indent_style = space
40 changes: 34 additions & 6 deletions comrade/comrade_core/admin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User, Task, Skill
from django.contrib.auth.forms import UserChangeForm

from .models import Skill, Task, User


class UserChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = User


class ComradeUserAdmin(UserAdmin):
form = UserChangeForm
fieldsets = UserAdmin.fieldsets + ((None, {"fields": ("skills",)}),)


class TaskAdmin(admin.ModelAdmin):
list_display = ['name', 'owner', 'state', 'lat', 'lon', 'respawn', 'respawn_time', 'base_value', 'criticality', 'contribution']
list_filter = ['state', 'respawn', 'criticality']
search_fields = ['name', 'description']
list_display = [
"name",
"owner",
"state",
"lat",
"lon",
"respawn",
"respawn_time",
"base_value",
"criticality",
"contribution",
]
list_filter = ["state", "respawn", "criticality"]
search_fields = ["name", "description"]


class SkillInline(admin.StackedInline):
model = Skill


admin.site.register(User, UserAdmin)
admin.site.register(User, ComradeUserAdmin)
admin.site.register(Task, TaskAdmin)
admin.site.register(Skill)
admin.site.register(Skill)
132 changes: 92 additions & 40 deletions comrade/comrade_core/models.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import datetime

from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.timezone import now
from django.core.validators import MaxValueValidator, MinValueValidator
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
def __str__(self) -> str:
return self.username
skills = models.ManyToManyField('Skill', blank=True)
return self.username

skills = models.ManyToManyField("Skill", blank=True)

latitude = models.FloatField(blank=True, default=0)
longitude = models.FloatField(blank=True, default=0)
Expand All @@ -18,12 +20,15 @@ def __str__(self) -> str:

def has_skill(self, skill_name):
return self.skills.filter(name=skill_name).exists()



class Skill(models.Model):
name = models.CharField(max_length=32)

def __str__(self) -> str:
return self.name


class Task(models.Model):
class Criticality(models.IntegerChoices):
LOW = 1
Expand All @@ -37,7 +42,7 @@ class State(models.IntegerChoices):
WAITING = 3
IN_REVIEW = 4
DONE = 5

def __str__(self) -> str:
return self.name

Expand All @@ -46,14 +51,26 @@ def __str__(self) -> str:
description = models.CharField(max_length=200, blank=True)

# permissions
skill_read = models.ManyToManyField(Skill, related_name='read')
skill_write = models.ManyToManyField(Skill, related_name='write')
skill_execute = models.ManyToManyField(Skill, related_name='execute')
skill_read = models.ManyToManyField(Skill, related_name="read")
skill_write = models.ManyToManyField(Skill, related_name="write")
skill_execute = models.ManyToManyField(Skill, related_name="execute")

# task state
state = models.IntegerField(choices=State, default=1, blank=True)
owner = models.ForeignKey('comrade_core.User', null=True, blank=True, on_delete=models.RESTRICT, related_name='owned_tasks')
assignee = models.ForeignKey('comrade_core.User', null=True, blank=True, on_delete=models.RESTRICT, related_name='assigned_tasks')
owner = models.ForeignKey(
"comrade_core.User",
null=True,
blank=True,
on_delete=models.RESTRICT,
related_name="owned_tasks",
)
assignee = models.ForeignKey(
"comrade_core.User",
null=True,
blank=True,
on_delete=models.RESTRICT,
related_name="assigned_tasks",
)

# location
lat = models.FloatField(null=True, blank=True)
Expand All @@ -66,64 +83,99 @@ def __str__(self) -> str:
# values
base_value = models.FloatField(blank=True, null=True)
criticality = models.IntegerField(choices=Criticality, default=1)
contribution = models.FloatField(blank=True, null=True, validators=[MaxValueValidator(1.0), MinValueValidator(0.0)])
contribution = models.FloatField(
blank=True,
null=True,
validators=[MaxValueValidator(1.0), MinValueValidator(0.0)],
)

# time tracking
minutes = models.IntegerField(default=10, validators=[MaxValueValidator(480), MinValueValidator(1)])
minutes = models.IntegerField(
default=10, validators=[MaxValueValidator(480), MinValueValidator(1)]
)
datetime_start = models.DateTimeField(auto_now_add=False, blank=True, null=True)
datetime_finish = models.DateTimeField(auto_now_add=False, blank=True, null=True)
datetime_finish = models.DateTimeField(auto_now_add=False, blank=True, null=True)

def start(self, user):
self.state = 2
def start(self, user: User):
has_required_skills = user.skills.filter(
id__in=self.skill_execute.all()
).exists()
if not has_required_skills:
raise ValidationError("User does not have required skills")

self.state = Task.State.IN_PROGRESS
self.datetime_start = now()
self.owner = user
self.assignee = user
self.save()

def pause(self):
if self.state != 2: # Check if the task is currently in progress
def pause(self, user: User):
if user != self.owner or user != self.assignee:
raise ValidationError("Only owner and assignee can pause the task")

if self.state != Task.State.IN_PROGRESS:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should work. 😅 I hope it will because it's much more readable.

return False
self.state = 3 # Set state to WAITING
self.state = Task.State.WAITING
self.save()

def resume(self):
if self.state != 3: # Check if the task is currently in WAITING state
def resume(self, user: User):
if user != self.owner or user != self.assignee:
raise ValidationError("Only owner and assignee can resume the task")

if self.state != Task.State.WAITING:
return False
self.state = 2 # Set state back to IN_PROGRESS
self.state = Task.State.IN_PROGRESS
self.save()

def finish(self):
def finish(self, user: User):
if user != self.owner or user != self.assignee:
raise ValidationError("Only owner and assignee can finish the task")

self.datetime_finish = now()
self.save()

def rate(self):
if self.state != 2:
def rate(self, user: User):
if user != self.owner or user != self.assignee:
raise ValidationError("Only owner and assignee can rate the task")

if self.state != Task.State.IN_PROGRESS:
return False
r = Rating()
r.task = self
r.save()
self.state=4
self.state = Task.State.IN_REVIEW
self.save()

def review(self):
if self.state != 4:
def review(self, user: User):
if user != self.owner:
raise ValidationError("Only owner can review the task")

if self.state != Task.State.IN_REVIEW:
return False
r = Review(done=1)
r.task = self
r.save()
self.state=5
self.state = Task.State.DONE
self.save()

class Rating(models.Model):
task = models.ForeignKey('comrade_core.Task', default=None, on_delete=models.RESTRICT, blank=True)
happiness = models.FloatField(default = 1)
time = models.FloatField(default = 1)

class Rating(models.Model):
task = models.ForeignKey(
"comrade_core.Task", default=None, on_delete=models.RESTRICT, blank=True
)
happiness = models.FloatField(default=1)
time = models.FloatField(default=1)

def __str__(self) -> str:
return "Rating of task \""+ self.task + "\""

return 'Rating of task "' + self.task + '"'


class Review(models.Model):
task = models.ForeignKey('comrade_core.Task', default=None, on_delete=models.RESTRICT, blank=True)
done = models.FloatField(validators=[MaxValueValidator(1.0), MinValueValidator(0.0)])
task = models.ForeignKey(
"comrade_core.Task", default=None, on_delete=models.RESTRICT, blank=True
)
done = models.FloatField(
validators=[MaxValueValidator(1.0), MinValueValidator(0.0)]
)

def __str__(self) -> str:
return "Review of task \""+ self.task + "\""
return 'Review of task "' + self.task + '"'
38 changes: 37 additions & 1 deletion comrade/comrade_core/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
from django.core.exceptions import ValidationError
from django.test import TestCase

# Create your tests here.
from comrade_core.models import Skill, Task, User


class TaskTestCase(TestCase):
def test_task_start_throws_error_to_user_with_no_skill(self):
s = Skill.objects.create(name="tasktestcase1")
u = User.objects.create(username="tasktestcase")
t = Task.objects.create()
t.skill_execute.add(s)

self.assertTrue(t.skill_execute.count() == 1)
self.assertTrue(u.skills.count() == 0)

try:
t.start(u)
except ValidationError:
pass
else:
self.fail(
"start should throw an error when the user has not the required skill for execute"
)

def test_task_start_succeeds_when_user_has_required_execute_skills(self):
s = Skill.objects.create(name="tasktestcase1")
u = User.objects.create(username="tasktestcase")
t = Task.objects.create()
t.skill_execute.add(s)
u.skills.add(s)

self.assertTrue(t.skill_execute.count() == 1)
self.assertTrue(u.skills.count() == 1)

try:
t.start(u)
except ValidationError:
self.fail("start should pass when user has at least one required skill")
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: "3.8"
services:
redis:
image: redis:alpine
ports:
- 6379:6379
postgres:
environment:
POSTGRES_USER: comrade
POSTGRES_PASSWORD: comrade
POSTGRES_DB: comrade
image: postgres:alpine
ports:
- 5432:5432
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data: