Sorting improved, more general thresholds for warning groups
This commit is contained in:
parent
8d5ce7ccae
commit
63e9af4d65
@ -21,6 +21,10 @@ def url_tasklist(request, user_filter_id=None, search_txt=None, common_filter_id
|
||||
return reverse('patt-tasklist')
|
||||
|
||||
|
||||
def url_profile(request):
|
||||
return reverse('patt-profile')
|
||||
|
||||
|
||||
def url_projectlist(request):
|
||||
return reverse('patt-projectlist')
|
||||
|
||||
|
7
admin.py
7
admin.py
@ -1,5 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from .models import Project, Task, Comment, Search
|
||||
from .models import Project, Task, Comment, Search, PattUserProfile
|
||||
from simple_history.admin import SimpleHistoryAdmin
|
||||
|
||||
|
||||
@ -59,7 +59,12 @@ class SearchAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
|
||||
class PattUserProfileAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'threshold_very_soon', 'threshold_soon', )
|
||||
|
||||
|
||||
admin.site.register(Project, ProjectAdmin)
|
||||
admin.site.register(Task, TaskAdmin)
|
||||
admin.site.register(Comment, CommentAdmin)
|
||||
admin.site.register(Search, SearchAdmin)
|
||||
admin.site.register(PattUserProfile, PattUserProfileAdmin)
|
||||
|
17
context.py
17
context.py
@ -8,6 +8,7 @@ import patt
|
||||
from .search import common_searches
|
||||
from themes import empty_entry_parameters, color_icon_url, gray_icon_url
|
||||
from users.context import menubar as menubar_users
|
||||
from users.context import PROFILE_ENTRY_UID
|
||||
|
||||
ATTACHMENT_UID = 'attachment'
|
||||
BACK_UID = 'back'
|
||||
@ -37,6 +38,7 @@ def context_adaption(context, request, **kwargs):
|
||||
|
||||
def menubar(context, request, caller_name, **kwargs):
|
||||
bar = context[context.MENUBAR]
|
||||
replace_profile(request, bar)
|
||||
add_help_menu(request, bar)
|
||||
add_tasklist_menu(request, bar)
|
||||
add_filter_submenu(request, bar, VIEW_TASKLIST_UID)
|
||||
@ -76,6 +78,21 @@ def finalise_bar(request, bar):
|
||||
bar.append_entry(*empty_entry_parameters(request))
|
||||
|
||||
|
||||
def replace_profile(request, bar):
|
||||
try:
|
||||
bar.replace_entry(
|
||||
PROFILE_ENTRY_UID,
|
||||
PROFILE_ENTRY_UID, # uid
|
||||
request.user.username, # name
|
||||
color_icon_url(request, 'user.png'), # icon
|
||||
patt.url_profile(request), # url
|
||||
False, # left
|
||||
False # active
|
||||
)
|
||||
except ValueError:
|
||||
pass # Profile entry does not exist, so exchange is not needed (e.g. no user is logged in)
|
||||
|
||||
|
||||
def add_help_menu(request, bar):
|
||||
bar.append_entry(
|
||||
HELP_UID, # uid
|
||||
|
21
forms.py
21
forms.py
@ -2,10 +2,20 @@ from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
from .models import Task, Project, Comment, Search, TASKSTATE_CHOICES, PROJECTSTATE_OPEN, ModelList
|
||||
from .models import Task, Project, Comment, Search, TASKSTATE_CHOICES, PROJECTSTATE_OPEN, ModelList, PattUserProfile
|
||||
from .search import INDEX_STATES
|
||||
|
||||
|
||||
class PattUserProfileForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = PattUserProfile
|
||||
fields = ['threshold_very_soon', 'threshold_soon']
|
||||
labels = {
|
||||
'threshold_very_soon': _('Period to targetdate for warning Group "very soon"'),
|
||||
'threshold_soon': _('Period to targetdate for warning Group "soon"'),
|
||||
}
|
||||
|
||||
|
||||
class TaskForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -56,7 +66,7 @@ class TaskForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Task
|
||||
fields = ['project', 'assigned_user', 'name', 'state', 'priority', 'targetdate', 'progress', 'description']
|
||||
fields = ['name', 'project', 'assigned_user', 'targetdate', 'priority', 'state', 'progress', 'description']
|
||||
widgets = {
|
||||
'assigned_user': forms.Select(attrs={'required': True}),
|
||||
'project': forms.Select(attrs={'onchange': 'submit()'}),
|
||||
@ -84,15 +94,10 @@ class TaskFormLimited(forms.ModelForm):
|
||||
class ProjectForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ['name', 'state', 'role_leader', 'role_member', 'role_visitor', 'description', 'days_late', 'days_very_soon', 'days_soon']
|
||||
fields = ['name', 'state', 'role_leader', 'role_member', 'role_visitor', 'description']
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'rows': 5}),
|
||||
}
|
||||
labels = {
|
||||
'days_late': _('Days to deadline (late)'),
|
||||
'days_very_soon': _('Days to deadline (very soon)'),
|
||||
'days_soon': _('Days to deadline (soon)'),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProjectForm, self).__init__(*args, **kwargs)
|
||||
|
37
migrations/0013_auto_20210117_1332.py
Normal file
37
migrations/0013_auto_20210117_1332.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Generated by Django 3.1.5 on 2021-01-17 13:32
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('patt', '0012_auto_20200114_1035'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='days_late',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='days_soon',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='days_very_soon',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PattUserProfile',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('threshold_very_soon', models.IntegerField(choices=[(1, '1 day'), (2, '2 days'), (3, '3 days'), (4, '4 days'), (5, '5 days'), (7, '7 days'), (9, '9 days'), (12, '12 days')], default=4)),
|
||||
('threshold_soon', models.IntegerField(choices=[(1, '1 day'), (2, '2 days'), (3, '3 days'), (5, '5 days'), (7, '7 days'), (10, '10 days'), (14, '14 days'), (21, ' days')], default=10)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
96
models.py
96
models.py
@ -1,10 +1,14 @@
|
||||
import datetime
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils.dateformat import format
|
||||
from django.utils.translation import gettext as _
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
|
||||
THRESHOLD_VERY_SOON_CHOICES = ((1, '1 day'), (2, '2 days'), (3, '3 days'), (4, '4 days'), (5, '5 days'), (7, '7 days'), (9, '9 days'), (12, '12 days'), ) # REQ-38
|
||||
THRESHOLD_SOON_CHOICES = ((1, '1 day'), (2, '2 days'), (3, '3 days'), (5, '5 days'), (7, '7 days'), (10, '10 days'), (14, '14 days'), (21, ' days'), ) # REQ-39
|
||||
|
||||
|
||||
# PROJECTSTATE (REQ-??)
|
||||
#
|
||||
PROJECTSTATE_OPEN = 0
|
||||
@ -75,6 +79,26 @@ COMMENTTYPE_CHOICES = (
|
||||
|
||||
# GENERAL Methods and Classes
|
||||
#
|
||||
def get_pattuserprofile(user):
|
||||
if user is None: # return a default profile if no (assigned_)user exist
|
||||
profile = PattUserProfile()
|
||||
else:
|
||||
try:
|
||||
profile = user.pattuserprofile
|
||||
except PattUserProfile.DoesNotExist:
|
||||
profile = PattUserProfile(user=user)
|
||||
profile.save()
|
||||
return profile
|
||||
|
||||
|
||||
# USERPROFILE Model
|
||||
#
|
||||
class PattUserProfile(models.Model):
|
||||
user = models.OneToOneField(User, unique=True, on_delete=models.CASCADE)
|
||||
threshold_very_soon = models.IntegerField(default=4, choices=THRESHOLD_VERY_SOON_CHOICES) # REQ-38
|
||||
threshold_soon = models.IntegerField(default=10, choices=THRESHOLD_SOON_CHOICES) # REQ-39
|
||||
|
||||
|
||||
class ModelList(list):
|
||||
def __init__(self, lst, chk=None, user=None):
|
||||
if chk is None:
|
||||
@ -100,9 +124,6 @@ class Project(models.Model):
|
||||
role_member = models.ManyToManyField(User, related_name='role_member', blank=True) # REQ-12
|
||||
role_visitor = models.ManyToManyField(User, related_name='role_visitor', blank=True) # REQ-33
|
||||
creation_date = models.DateTimeField(auto_now_add=True)
|
||||
days_late = models.IntegerField(default=0, choices=((-1, _('Off')), (0, 0), ))
|
||||
days_very_soon = models.IntegerField(default=3, choices=((-1, _('Off')), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (7, 7), (9, 9), ))
|
||||
days_soon = models.IntegerField(default=7, choices=((-1, _('Off')), (1, 1), (2, 2), (3, 3), (5, 5), (7, 7), (10, 10), (14, 14), ))
|
||||
|
||||
@property
|
||||
def attachment_target_path(self):
|
||||
@ -142,11 +163,11 @@ class Project(models.Model):
|
||||
# TASK Model
|
||||
#
|
||||
class Task(models.Model):
|
||||
FUSION_STATE_FINISHED = 0
|
||||
FUSION_STATE_NORMAL = 1
|
||||
FUSION_STATE_SOON = 2
|
||||
FUSION_STATE_VERY_SOON = 3
|
||||
FUSION_STATE_LATE = 4
|
||||
WARNING_GROUP_OVERDUE = 0 # REQ-40
|
||||
WARNING_GROUP_VERY_SOON = 1 # REQ-40
|
||||
WARNING_GROUP_SOON = 2 # REQ-40
|
||||
WARNING_GROUP_DEFAULT = 3 # REQ-40
|
||||
WARNING_GROUP_DONE = 4 # REQ-40
|
||||
#
|
||||
SAVE_ON_CHANGE_FIELDS = ['state', 'priority', 'targetdate', 'progress', 'name', 'description', 'assigned_user', 'project']
|
||||
#
|
||||
@ -194,39 +215,42 @@ class Task(models.Model):
|
||||
rv.sort(reverse=True)
|
||||
return rv
|
||||
|
||||
def datafusion_state(self):
|
||||
if self.state in [TASKSTATE_CANCELED, TASKSTATE_CLOSED, TASKSTATE_FINISHED]:
|
||||
return self.FUSION_STATE_FINISHED
|
||||
def days_before_targetdate(self): # REQ-40, REQ-43
|
||||
if self.targetdate is None:
|
||||
return float('Inf')
|
||||
else:
|
||||
if self.targetdate is not None:
|
||||
if type(self.targetdate) == datetime.date:
|
||||
targetdate = self.targetdate
|
||||
else:
|
||||
targetdate = datetime.datetime.strptime(self.targetdate, '%Y-%m-%d').date()
|
||||
if targetdate - datetime.date.today() <= datetime.timedelta(self.project.days_late) and self.project.days_late >= 0:
|
||||
return self.FUSION_STATE_LATE
|
||||
elif targetdate - datetime.date.today() <= datetime.timedelta(self.project.days_very_soon) and self.project.days_very_soon >= 0:
|
||||
return self.FUSION_STATE_VERY_SOON
|
||||
elif targetdate - datetime.date.today() <= datetime.timedelta(self.project.days_soon) and self.project.days_soon >= 0:
|
||||
return self.FUSION_STATE_SOON
|
||||
return self.FUSION_STATE_NORMAL
|
||||
if type(self.targetdate) == datetime.date:
|
||||
targetdate = self.targetdate
|
||||
else:
|
||||
targetdate = datetime.datetime.strptime(self.targetdate, '%Y-%m-%d').date()
|
||||
rv = targetdate - datetime.datetime.now().date()
|
||||
return rv.days
|
||||
|
||||
def warning_group(self): # REQ-40
|
||||
if self.state in [TASKSTATE_CANCELED, TASKSTATE_CLOSED, TASKSTATE_FINISHED]:
|
||||
return self.WARNING_GROUP_DONE
|
||||
else:
|
||||
dbt = self.days_before_targetdate()
|
||||
if dbt <= 0:
|
||||
return self.WARNING_GROUP_OVERDUE
|
||||
if dbt <= get_pattuserprofile(self.assigned_user).threshold_very_soon:
|
||||
return self.WARNING_GROUP_VERY_SOON
|
||||
if dbt <= get_pattuserprofile(self.assigned_user).threshold_soon:
|
||||
return self.WARNING_GROUP_SOON
|
||||
return self.WARNING_GROUP_DEFAULT
|
||||
|
||||
@property
|
||||
def class_by_state(self):
|
||||
return {
|
||||
self.FUSION_STATE_FINISHED: 'task-finished',
|
||||
self.FUSION_STATE_NORMAL: 'task-normal',
|
||||
self.FUSION_STATE_VERY_SOON: 'task-very-soon',
|
||||
self.FUSION_STATE_SOON: 'task-soon',
|
||||
self.FUSION_STATE_LATE: 'task-late',
|
||||
}.get(self.datafusion_state())
|
||||
self.WARNING_GROUP_DONE: 'task-finished',
|
||||
self.WARNING_GROUP_DEFAULT: 'task-normal',
|
||||
self.WARNING_GROUP_SOON: 'task-soon',
|
||||
self.WARNING_GROUP_VERY_SOON: 'task-very-soon',
|
||||
self.WARNING_GROUP_OVERDUE: 'task-late',
|
||||
}.get(self.warning_group())
|
||||
|
||||
def sort_string(self):
|
||||
if self.targetdate:
|
||||
td = int(format(self.targetdate, 'U'))
|
||||
else:
|
||||
td = 999999999999
|
||||
return (100 - self.datafusion_state(), self.state, self.priority, td, self.progress, self.name)
|
||||
def sort_string(self): # REQ-41
|
||||
return (self.warning_group(), self.priority, self.days_before_targetdate(), self.progress, self.name)
|
||||
|
||||
def __str__(self):
|
||||
return 'Task #%d: %s' % (self.id, self.name)
|
||||
|
12
templates/patt/profile.html
Normal file
12
templates/patt/profile.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'users/profile_formdata.html' %}
|
||||
{% include 'patt/profile_formdata.html' %}
|
||||
<input type="submit" value="{% trans "Save" %}" class="button" />
|
||||
</form>
|
||||
|
||||
{% endblock content %}
|
6
templates/patt/profile_formdata.html
Normal file
6
templates/patt/profile_formdata.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<h1>{% trans "Warning Group Thresholds" %}</h1>
|
||||
{{ form_pattprofile.as_p }}
|
||||
</select>
|
1
urls.py
1
urls.py
@ -6,6 +6,7 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', lambda request: redirect(reverse('patt-commonfilter', kwargs={'common_filter_id': SEARCH_MY_OPEN_TASKS}), permanent=False)),
|
||||
path('profile', views.patt_profile, name='patt-profile'),
|
||||
#
|
||||
# views and urls
|
||||
#
|
||||
|
42
views.py
42
views.py
@ -7,12 +7,14 @@ from django.shortcuts import render, redirect
|
||||
from django.urls.base import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from .forms import TaskForm, TaskFormLimited, ProjectForm, CommentForm, TaskCommentForm, SearchForm, EasySearchForm
|
||||
from .forms import TaskForm, TaskFormLimited, ProjectForm, CommentForm, TaskCommentForm, SearchForm, EasySearchForm, PattUserProfileForm
|
||||
from .help import help_pages
|
||||
from .models import Task, ModelList, Comment, TASKSTATE_CHOICES, PRIO_CHOICES, TASKS_IN_WORK
|
||||
from .models import Project, Search
|
||||
from .models import Project, Search, get_pattuserprofile
|
||||
import patt
|
||||
from .search import load_index, search, mk_search_pattern, get_project_ids_from_search_pattern, common_searches
|
||||
from users.forms import UserProfileForm
|
||||
from users.models import get_userprofile
|
||||
from themes import Context
|
||||
|
||||
|
||||
@ -142,6 +144,42 @@ def get_next(request):
|
||||
return request.POST.get('next', '/')
|
||||
|
||||
|
||||
def profile_post_actions(request, context):
|
||||
if request.POST:
|
||||
form = context.get('form_userprofile')
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
form = context.get('form_pattprofile')
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(request.GET.get('next') or '/')
|
||||
|
||||
|
||||
def profile_pre_actions(request, context, form_to_be_used=UserProfileForm):
|
||||
user_profile = get_userprofile(request.user)
|
||||
patt_user_profile = get_pattuserprofile(request.user)
|
||||
if request.POST:
|
||||
userprofile_form = UserProfileForm(request.POST, instance=user_profile)
|
||||
pattprofile_form = PattUserProfileForm(request.POST, instance=patt_user_profile)
|
||||
else:
|
||||
userprofile_form = UserProfileForm(instance=user_profile)
|
||||
pattprofile_form = PattUserProfileForm(instance=patt_user_profile)
|
||||
context['form_userprofile'] = userprofile_form
|
||||
context['form_pattprofile'] = pattprofile_form
|
||||
|
||||
|
||||
@login_required
|
||||
def patt_profile(request):
|
||||
context = Context(request) # needs to be executed first because of time mesurement
|
||||
profile_pre_actions(request, context)
|
||||
response = profile_post_actions(request, context)
|
||||
if response is not None:
|
||||
return response
|
||||
else:
|
||||
context_adaption(context, request, title=_('Profile for %(username)s') % {'username': request.user.username})
|
||||
return render(request, 'patt/profile.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
def patt_tasklist(request, user_filter_id=None, common_filter_id=None):
|
||||
context = Context(request) # needs to be executed first because of time mesurement
|
||||
|
Loading…
x
Reference in New Issue
Block a user