Browse Source

Sorting improved, more general thresholds for warning groups

master
Dirk Alders 3 years ago
parent
commit
63e9af4d65
10 changed files with 196 additions and 47 deletions
  1. 4
    0
      __init__.py
  2. 6
    1
      admin.py
  3. 17
    0
      context.py
  4. 13
    8
      forms.py
  5. 37
    0
      migrations/0013_auto_20210117_1332.py
  6. 60
    36
      models.py
  7. 12
    0
      templates/patt/profile.html
  8. 6
    0
      templates/patt/profile_formdata.html
  9. 1
    0
      urls.py
  10. 40
    2
      views.py

+ 4
- 0
__init__.py View File

@@ -21,6 +21,10 @@ def url_tasklist(request, user_filter_id=None, search_txt=None, common_filter_id
21 21
         return reverse('patt-tasklist')
22 22
 
23 23
 
24
+def url_profile(request):
25
+    return reverse('patt-profile')
26
+
27
+
24 28
 def url_projectlist(request):
25 29
     return reverse('patt-projectlist')
26 30
 

+ 6
- 1
admin.py View File

@@ -1,5 +1,5 @@
1 1
 from django.contrib import admin
2
-from .models import Project, Task, Comment, Search
2
+from .models import Project, Task, Comment, Search, PattUserProfile
3 3
 from simple_history.admin import SimpleHistoryAdmin
4 4
 
5 5
 
@@ -59,7 +59,12 @@ class SearchAdmin(admin.ModelAdmin):
59 59
     )
60 60
 
61 61
 
62
+class PattUserProfileAdmin(admin.ModelAdmin):
63
+    list_display = ('user', 'threshold_very_soon', 'threshold_soon', )
64
+
65
+
62 66
 admin.site.register(Project, ProjectAdmin)
63 67
 admin.site.register(Task, TaskAdmin)
64 68
 admin.site.register(Comment, CommentAdmin)
65 69
 admin.site.register(Search, SearchAdmin)
70
+admin.site.register(PattUserProfile, PattUserProfileAdmin)

+ 17
- 0
context.py View File

@@ -8,6 +8,7 @@ import patt
8 8
 from .search import common_searches
9 9
 from themes import empty_entry_parameters, color_icon_url, gray_icon_url
10 10
 from users.context import menubar as menubar_users
11
+from users.context import PROFILE_ENTRY_UID
11 12
 
12 13
 ATTACHMENT_UID = 'attachment'
13 14
 BACK_UID = 'back'
@@ -37,6 +38,7 @@ def context_adaption(context, request, **kwargs):
37 38
 
38 39
 def menubar(context, request, caller_name, **kwargs):
39 40
     bar = context[context.MENUBAR]
41
+    replace_profile(request, bar)
40 42
     add_help_menu(request, bar)
41 43
     add_tasklist_menu(request, bar)
42 44
     add_filter_submenu(request, bar, VIEW_TASKLIST_UID)
@@ -76,6 +78,21 @@ def finalise_bar(request, bar):
76 78
         bar.append_entry(*empty_entry_parameters(request))
77 79
 
78 80
 
81
+def replace_profile(request, bar):
82
+    try:
83
+        bar.replace_entry(
84
+            PROFILE_ENTRY_UID,
85
+            PROFILE_ENTRY_UID,                          # uid
86
+            request.user.username,                      # name
87
+            color_icon_url(request, 'user.png'),        # icon
88
+            patt.url_profile(request),                  # url
89
+            False,                                      # left
90
+            False                                       # active
91
+        )
92
+    except ValueError:
93
+        pass        # Profile entry does not exist, so exchange is not needed (e.g. no user is logged in)
94
+
95
+
79 96
 def add_help_menu(request, bar):
80 97
     bar.append_entry(
81 98
         HELP_UID,                                   # uid

+ 13
- 8
forms.py View File

@@ -2,10 +2,20 @@ from django import forms
2 2
 from django.contrib.auth.models import User
3 3
 from django.db.models import Q
4 4
 from django.utils.translation import gettext as _
5
-from .models import Task, Project, Comment, Search, TASKSTATE_CHOICES, PROJECTSTATE_OPEN, ModelList
5
+from .models import Task, Project, Comment, Search, TASKSTATE_CHOICES, PROJECTSTATE_OPEN, ModelList, PattUserProfile
6 6
 from .search import INDEX_STATES
7 7
 
8 8
 
9
+class PattUserProfileForm(forms.ModelForm):
10
+    class Meta:
11
+        model = PattUserProfile
12
+        fields = ['threshold_very_soon', 'threshold_soon']
13
+        labels = {
14
+            'threshold_very_soon': _('Period to targetdate for warning Group "very soon"'),
15
+            'threshold_soon': _('Period to targetdate for warning Group "soon"'),
16
+        }
17
+
18
+
9 19
 class TaskForm(forms.ModelForm):
10 20
 
11 21
     def __init__(self, *args, **kwargs):
@@ -56,7 +66,7 @@ class TaskForm(forms.ModelForm):
56 66
 
57 67
     class Meta:
58 68
         model = Task
59
-        fields = ['project', 'assigned_user', 'name', 'state', 'priority', 'targetdate', 'progress', 'description']
69
+        fields = ['name', 'project', 'assigned_user', 'targetdate', 'priority', 'state', 'progress', 'description']
60 70
         widgets = {
61 71
             'assigned_user': forms.Select(attrs={'required': True}),
62 72
             'project': forms.Select(attrs={'onchange': 'submit()'}),
@@ -84,15 +94,10 @@ class TaskFormLimited(forms.ModelForm):
84 94
 class ProjectForm(forms.ModelForm):
85 95
     class Meta:
86 96
         model = Project
87
-        fields = ['name', 'state', 'role_leader', 'role_member', 'role_visitor', 'description', 'days_late', 'days_very_soon', 'days_soon']
97
+        fields = ['name', 'state', 'role_leader', 'role_member', 'role_visitor', 'description']
88 98
         widgets = {
89 99
             'description': forms.Textarea(attrs={'rows': 5}),
90 100
         }
91
-        labels = {
92
-            'days_late': _('Days to deadline (late)'),
93
-            'days_very_soon': _('Days to deadline (very soon)'),
94
-            'days_soon': _('Days to deadline (soon)'),
95
-        }
96 101
 
97 102
     def __init__(self, *args, **kwargs):
98 103
         super(ProjectForm, self).__init__(*args, **kwargs)

+ 37
- 0
migrations/0013_auto_20210117_1332.py View File

@@ -0,0 +1,37 @@
1
+# Generated by Django 3.1.5 on 2021-01-17 13:32
2
+
3
+from django.conf import settings
4
+from django.db import migrations, models
5
+import django.db.models.deletion
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    dependencies = [
11
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12
+        ('patt', '0012_auto_20200114_1035'),
13
+    ]
14
+
15
+    operations = [
16
+        migrations.RemoveField(
17
+            model_name='project',
18
+            name='days_late',
19
+        ),
20
+        migrations.RemoveField(
21
+            model_name='project',
22
+            name='days_soon',
23
+        ),
24
+        migrations.RemoveField(
25
+            model_name='project',
26
+            name='days_very_soon',
27
+        ),
28
+        migrations.CreateModel(
29
+            name='PattUserProfile',
30
+            fields=[
31
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
32
+                ('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)),
33
+                ('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)),
34
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
35
+            ],
36
+        ),
37
+    ]

+ 60
- 36
models.py View File

@@ -1,10 +1,14 @@
1 1
 import datetime
2 2
 from django.contrib.auth.models import User
3 3
 from django.db import models
4
-from django.utils.dateformat import format
5 4
 from django.utils.translation import gettext as _
6 5
 from simple_history.models import HistoricalRecords
7 6
 
7
+
8
+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
9
+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
10
+
11
+
8 12
 # PROJECTSTATE (REQ-??)
9 13
 #
10 14
 PROJECTSTATE_OPEN = 0
@@ -75,6 +79,26 @@ COMMENTTYPE_CHOICES = (
75 79
 
76 80
 # GENERAL Methods and Classes
77 81
 #
82
+def get_pattuserprofile(user):
83
+    if user is None:    # return a default profile if no (assigned_)user exist
84
+        profile = PattUserProfile()
85
+    else:
86
+        try:
87
+            profile = user.pattuserprofile
88
+        except PattUserProfile.DoesNotExist:
89
+            profile = PattUserProfile(user=user)
90
+            profile.save()
91
+    return profile
92
+
93
+
94
+# USERPROFILE Model
95
+#
96
+class PattUserProfile(models.Model):
97
+    user = models.OneToOneField(User, unique=True, on_delete=models.CASCADE)
98
+    threshold_very_soon = models.IntegerField(default=4, choices=THRESHOLD_VERY_SOON_CHOICES)   # REQ-38
99
+    threshold_soon = models.IntegerField(default=10, choices=THRESHOLD_SOON_CHOICES)            # REQ-39
100
+
101
+
78 102
 class ModelList(list):
79 103
     def __init__(self, lst, chk=None, user=None):
80 104
         if chk is None:
@@ -100,9 +124,6 @@ class Project(models.Model):
100 124
     role_member = models.ManyToManyField(User, related_name='role_member', blank=True)      # REQ-12
101 125
     role_visitor = models.ManyToManyField(User, related_name='role_visitor', blank=True)    # REQ-33
102 126
     creation_date = models.DateTimeField(auto_now_add=True)
103
-    days_late = models.IntegerField(default=0, choices=((-1, _('Off')), (0, 0), ))
104
-    days_very_soon = models.IntegerField(default=3, choices=((-1, _('Off')), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (7, 7), (9, 9), ))
105
-    days_soon = models.IntegerField(default=7, choices=((-1, _('Off')), (1, 1), (2, 2), (3, 3), (5, 5), (7, 7), (10, 10), (14, 14), ))
106 127
 
107 128
     @property
108 129
     def attachment_target_path(self):
@@ -142,11 +163,11 @@ class Project(models.Model):
142 163
 # TASK Model
143 164
 #
144 165
 class Task(models.Model):
145
-    FUSION_STATE_FINISHED = 0
146
-    FUSION_STATE_NORMAL = 1
147
-    FUSION_STATE_SOON = 2
148
-    FUSION_STATE_VERY_SOON = 3
149
-    FUSION_STATE_LATE = 4
166
+    WARNING_GROUP_OVERDUE = 0           # REQ-40
167
+    WARNING_GROUP_VERY_SOON = 1         # REQ-40
168
+    WARNING_GROUP_SOON = 2              # REQ-40
169
+    WARNING_GROUP_DEFAULT = 3           # REQ-40
170
+    WARNING_GROUP_DONE = 4              # REQ-40
150 171
     #
151 172
     SAVE_ON_CHANGE_FIELDS = ['state', 'priority', 'targetdate', 'progress', 'name', 'description', 'assigned_user', 'project']
152 173
     #
@@ -194,39 +215,42 @@ class Task(models.Model):
194 215
         rv.sort(reverse=True)
195 216
         return rv
196 217
 
197
-    def datafusion_state(self):
218
+    def days_before_targetdate(self):   # REQ-40, REQ-43
219
+        if self.targetdate is None:
220
+            return float('Inf')
221
+        else:
222
+            if type(self.targetdate) == datetime.date:
223
+                targetdate = self.targetdate
224
+            else:
225
+                targetdate = datetime.datetime.strptime(self.targetdate, '%Y-%m-%d').date()
226
+            rv = targetdate - datetime.datetime.now().date()
227
+            return rv.days
228
+
229
+    def warning_group(self):            # REQ-40
198 230
         if self.state in [TASKSTATE_CANCELED, TASKSTATE_CLOSED, TASKSTATE_FINISHED]:
199
-            return self.FUSION_STATE_FINISHED
231
+            return self.WARNING_GROUP_DONE
200 232
         else:
201
-            if self.targetdate is not None:
202
-                if type(self.targetdate) == datetime.date:
203
-                    targetdate = self.targetdate
204
-                else:
205
-                    targetdate = datetime.datetime.strptime(self.targetdate, '%Y-%m-%d').date()
206
-                if targetdate - datetime.date.today() <= datetime.timedelta(self.project.days_late) and self.project.days_late >= 0:
207
-                    return self.FUSION_STATE_LATE
208
-                elif targetdate - datetime.date.today() <= datetime.timedelta(self.project.days_very_soon) and self.project.days_very_soon >= 0:
209
-                    return self.FUSION_STATE_VERY_SOON
210
-                elif targetdate - datetime.date.today() <= datetime.timedelta(self.project.days_soon) and self.project.days_soon >= 0:
211
-                    return self.FUSION_STATE_SOON
212
-        return self.FUSION_STATE_NORMAL
233
+            dbt = self.days_before_targetdate()
234
+            if dbt <= 0:
235
+                return self.WARNING_GROUP_OVERDUE
236
+            if dbt <= get_pattuserprofile(self.assigned_user).threshold_very_soon:
237
+                return self.WARNING_GROUP_VERY_SOON
238
+            if dbt <= get_pattuserprofile(self.assigned_user).threshold_soon:
239
+                return self.WARNING_GROUP_SOON
240
+            return self.WARNING_GROUP_DEFAULT
213 241
 
214 242
     @property
215 243
     def class_by_state(self):
216 244
         return {
217
-            self.FUSION_STATE_FINISHED: 'task-finished',
218
-            self.FUSION_STATE_NORMAL: 'task-normal',
219
-            self.FUSION_STATE_VERY_SOON: 'task-very-soon',
220
-            self.FUSION_STATE_SOON: 'task-soon',
221
-            self.FUSION_STATE_LATE: 'task-late',
222
-        }.get(self.datafusion_state())
223
-
224
-    def sort_string(self):
225
-        if self.targetdate:
226
-            td = int(format(self.targetdate, 'U'))
227
-        else:
228
-            td = 999999999999
229
-        return (100 - self.datafusion_state(), self.state, self.priority, td, self.progress, self.name)
245
+            self.WARNING_GROUP_DONE: 'task-finished',
246
+            self.WARNING_GROUP_DEFAULT: 'task-normal',
247
+            self.WARNING_GROUP_SOON: 'task-soon',
248
+            self.WARNING_GROUP_VERY_SOON: 'task-very-soon',
249
+            self.WARNING_GROUP_OVERDUE: 'task-late',
250
+        }.get(self.warning_group())
251
+
252
+    def sort_string(self):      # REQ-41
253
+        return (self.warning_group(), self.priority, self.days_before_targetdate(), self.progress, self.name)
230 254
 
231 255
     def __str__(self):
232 256
         return 'Task #%d: %s' % (self.id, self.name)

+ 12
- 0
templates/patt/profile.html View File

@@ -0,0 +1,12 @@
1
+{% extends "themes/"|add:settings.page_theme|add:"/base.html" %}
2
+{% load i18n %}
3
+
4
+{% block content %}
5
+  <form action="" method="post">
6
+    {% csrf_token %}
7
+    {% include 'users/profile_formdata.html' %}
8
+    {% include 'patt/profile_formdata.html' %}
9
+    <input type="submit" value="{% trans "Save" %}" class="button" />
10
+  </form>
11
+
12
+{% endblock content %}

+ 6
- 0
templates/patt/profile_formdata.html View File

@@ -0,0 +1,6 @@
1
+{% load i18n %}
2
+
3
+{% get_current_language as LANGUAGE_CODE %}
4
+  <h1>{% trans "Warning Group Thresholds" %}</h1>
5
+  {{ form_pattprofile.as_p }}
6
+  </select>

+ 1
- 0
urls.py View File

@@ -6,6 +6,7 @@ from . import views
6 6
 
7 7
 urlpatterns = [
8 8
     path('', lambda request: redirect(reverse('patt-commonfilter', kwargs={'common_filter_id': SEARCH_MY_OPEN_TASKS}), permanent=False)),
9
+    path('profile', views.patt_profile, name='patt-profile'),
9 10
     #
10 11
     # views and urls
11 12
     #

+ 40
- 2
views.py View File

@@ -7,12 +7,14 @@ from django.shortcuts import render, redirect
7 7
 from django.urls.base import reverse
8 8
 from django.utils import timezone
9 9
 from django.utils.translation import gettext as _
10
-from .forms import TaskForm, TaskFormLimited, ProjectForm, CommentForm, TaskCommentForm, SearchForm, EasySearchForm
10
+from .forms import TaskForm, TaskFormLimited, ProjectForm, CommentForm, TaskCommentForm, SearchForm, EasySearchForm, PattUserProfileForm
11 11
 from .help import help_pages
12 12
 from .models import Task, ModelList, Comment, TASKSTATE_CHOICES, PRIO_CHOICES, TASKS_IN_WORK
13
-from .models import Project, Search
13
+from .models import Project, Search, get_pattuserprofile
14 14
 import patt
15 15
 from .search import load_index, search, mk_search_pattern, get_project_ids_from_search_pattern, common_searches
16
+from users.forms import UserProfileForm
17
+from users.models import get_userprofile
16 18
 from themes import Context
17 19
 
18 20
 
@@ -142,6 +144,42 @@ def get_next(request):
142 144
         return request.POST.get('next', '/')
143 145
 
144 146
 
147
+def profile_post_actions(request, context):
148
+    if request.POST:
149
+        form = context.get('form_userprofile')
150
+        if form.is_valid():
151
+            form.save()
152
+        form = context.get('form_pattprofile')
153
+        if form.is_valid():
154
+            form.save()
155
+        return redirect(request.GET.get('next') or '/')
156
+
157
+
158
+def profile_pre_actions(request, context, form_to_be_used=UserProfileForm):
159
+    user_profile = get_userprofile(request.user)
160
+    patt_user_profile = get_pattuserprofile(request.user)
161
+    if request.POST:
162
+        userprofile_form = UserProfileForm(request.POST, instance=user_profile)
163
+        pattprofile_form = PattUserProfileForm(request.POST, instance=patt_user_profile)
164
+    else:
165
+        userprofile_form = UserProfileForm(instance=user_profile)
166
+        pattprofile_form = PattUserProfileForm(instance=patt_user_profile)
167
+    context['form_userprofile'] = userprofile_form
168
+    context['form_pattprofile'] = pattprofile_form
169
+
170
+
171
+@login_required
172
+def patt_profile(request):
173
+    context = Context(request)      # needs to be executed first because of time mesurement
174
+    profile_pre_actions(request, context)
175
+    response = profile_post_actions(request, context)
176
+    if response is not None:
177
+        return response
178
+    else:
179
+        context_adaption(context, request, title=_('Profile for %(username)s') % {'username': request.user.username})
180
+        return render(request, 'patt/profile.html', context=context)
181
+
182
+
145 183
 @login_required
146 184
 def patt_tasklist(request, user_filter_id=None, common_filter_id=None):
147 185
     context = Context(request)      # needs to be executed first because of time mesurement

Loading…
Cancel
Save