wbtasks 2.2.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wbtasks/__init__.py +2 -0
- wbtasks/admin.py +9 -0
- wbtasks/apps.py +5 -0
- wbtasks/factories.py +28 -0
- wbtasks/filters.py +54 -0
- wbtasks/migrations/0001_initial_squashed_squashed_0006_alter_task_assigned_to_alter_task_in_charge_and_more.py +114 -0
- wbtasks/migrations/__init__.py +0 -0
- wbtasks/models.py +221 -0
- wbtasks/serializers.py +159 -0
- wbtasks/tasks.py +42 -0
- wbtasks/tests/__init__.py +0 -0
- wbtasks/tests/conftest.py +11 -0
- wbtasks/tests/test_filters.py +35 -0
- wbtasks/tests/test_models.py +16 -0
- wbtasks/tests/test_serializers.py +6 -0
- wbtasks/tests/test_tasks.py +6 -0
- wbtasks/tests/tests.py +12 -0
- wbtasks/urls.py +11 -0
- wbtasks/viewsets/__init__.py +3 -0
- wbtasks/viewsets/buttons.py +11 -0
- wbtasks/viewsets/display.py +78 -0
- wbtasks/viewsets/menu.py +11 -0
- wbtasks/viewsets/titles.py +12 -0
- wbtasks/viewsets/viewsets.py +36 -0
- wbtasks-2.2.1.dist-info/METADATA +5 -0
- wbtasks-2.2.1.dist-info/RECORD +27 -0
- wbtasks-2.2.1.dist-info/WHEEL +5 -0
wbtasks/__init__.py
ADDED
wbtasks/admin.py
ADDED
wbtasks/apps.py
ADDED
wbtasks/factories.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import factory
|
|
2
|
+
import pytz
|
|
3
|
+
from wbcore.contrib.authentication.factories import AuthenticatedPersonFactory
|
|
4
|
+
from wbtasks.models import Task
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TaskFactory(factory.django.DjangoModelFactory):
|
|
8
|
+
class Meta:
|
|
9
|
+
model = Task
|
|
10
|
+
|
|
11
|
+
title = factory.Faker("text", max_nb_chars=64)
|
|
12
|
+
due_date = factory.Faker("date_time", tzinfo=pytz.utc)
|
|
13
|
+
creation_date = factory.Faker("date_time_between", start_date="+2d", end_date="+3d", tzinfo=pytz.utc)
|
|
14
|
+
starting_date = factory.Faker("date_time_between", start_date="+2d", end_date="+3d", tzinfo=pytz.utc)
|
|
15
|
+
completion_date = factory.Faker("date_time_between", start_date="+4d", end_date="+5d", tzinfo=pytz.utc)
|
|
16
|
+
requester = factory.SubFactory(AuthenticatedPersonFactory)
|
|
17
|
+
in_charge = factory.SubFactory(AuthenticatedPersonFactory)
|
|
18
|
+
widget_endpoint = factory.Faker("text", max_nb_chars=64)
|
|
19
|
+
description = factory.Faker("paragraph")
|
|
20
|
+
comment = factory.Faker("paragraph")
|
|
21
|
+
|
|
22
|
+
@factory.post_generation
|
|
23
|
+
def assigned_to(self, create, extracted, **kwargs):
|
|
24
|
+
if not create:
|
|
25
|
+
return
|
|
26
|
+
if extracted:
|
|
27
|
+
for participant in extracted:
|
|
28
|
+
self.assigned_to.add(participant)
|
wbtasks/filters.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from wbcore import filters as wb_filters
|
|
2
|
+
from wbcore.contrib.directory.models import Entry, Person
|
|
3
|
+
from wbtasks.models import Task
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_current_user_id(field, request, view):
|
|
7
|
+
return request.user.profile.id
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskFilter(wb_filters.FilterSet):
|
|
11
|
+
in_charge = wb_filters.ModelChoiceFilter(
|
|
12
|
+
label="Assigned to",
|
|
13
|
+
queryset=Person.objects.all(),
|
|
14
|
+
endpoint=Person.get_representation_endpoint(),
|
|
15
|
+
value_key=Person.get_representation_value_key(),
|
|
16
|
+
label_key=Person.get_representation_label_key(),
|
|
17
|
+
filter_params={"only_internal_users": True},
|
|
18
|
+
# default=get_current_user_id,
|
|
19
|
+
method="filter_in_charge",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
participants = wb_filters.ModelMultipleChoiceFilter(
|
|
23
|
+
label="Participants",
|
|
24
|
+
queryset=Entry.objects.all(),
|
|
25
|
+
endpoint=Entry.get_representation_endpoint(),
|
|
26
|
+
value_key=Entry.get_representation_value_key(),
|
|
27
|
+
label_key=Entry.get_representation_label_key(),
|
|
28
|
+
# default=get_current_user_id,
|
|
29
|
+
method="filter_participants",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def filter_in_charge(self, queryset, name, value):
|
|
33
|
+
if value:
|
|
34
|
+
return queryset.filter(in_charge=value)
|
|
35
|
+
return queryset
|
|
36
|
+
|
|
37
|
+
def filter_participants(self, queryset, name, value):
|
|
38
|
+
if value:
|
|
39
|
+
return queryset.filter(assigned_to__in=value).distinct()
|
|
40
|
+
return queryset
|
|
41
|
+
|
|
42
|
+
class Meta:
|
|
43
|
+
model = Task
|
|
44
|
+
fields = {
|
|
45
|
+
"starting_date": ["gte", "exact", "lte"],
|
|
46
|
+
"completion_date": ["gte", "exact", "lte"],
|
|
47
|
+
"creation_date": ["gte", "exact", "lte"],
|
|
48
|
+
"due_date": ["gte", "exact", "lte"],
|
|
49
|
+
"requester": ["exact"],
|
|
50
|
+
# "in_charge": ["exact"],
|
|
51
|
+
"assigned_to": ["exact"],
|
|
52
|
+
"status": ["exact"],
|
|
53
|
+
"priority": ["exact"],
|
|
54
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Generated by Django 4.1.8 on 2023-04-17 11:19
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import django_fsm
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("wbcore", "0001_initial_squashed_squashed_0010_preset_appliedpreset"),
|
|
13
|
+
("directory", "0001_initial"),
|
|
14
|
+
("tags", "0001_initial"),
|
|
15
|
+
("agenda", "0002_initial"),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
operations = [
|
|
19
|
+
migrations.CreateModel(
|
|
20
|
+
name="Task",
|
|
21
|
+
fields=[
|
|
22
|
+
("tag_detail_endpoint", models.CharField(blank=True, max_length=255, null=True)),
|
|
23
|
+
("tag_representation", models.CharField(blank=True, max_length=512, null=True)),
|
|
24
|
+
("due_date", models.DateTimeField(blank=True, null=True, verbose_name="Due Date")),
|
|
25
|
+
("creation_date", models.DateTimeField(auto_now_add=True, verbose_name="Creation Date")),
|
|
26
|
+
("starting_date", models.DateTimeField(blank=True, null=True, verbose_name="Started at")),
|
|
27
|
+
("completion_date", models.DateTimeField(blank=True, null=True, verbose_name="Completed at")),
|
|
28
|
+
(
|
|
29
|
+
"status",
|
|
30
|
+
django_fsm.FSMField(
|
|
31
|
+
choices=[("UNSCHEDULED", "Unscheduled"), ("STARTED", "Started"), ("COMPLETED", "Completed")],
|
|
32
|
+
default="UNSCHEDULED",
|
|
33
|
+
max_length=50,
|
|
34
|
+
verbose_name="Status",
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
("description", models.TextField(default="", verbose_name="Description")),
|
|
38
|
+
(
|
|
39
|
+
"priority",
|
|
40
|
+
models.CharField(
|
|
41
|
+
choices=[("LOW", "Low"), ("MEDIUM", "Medium"), ("HIGH", "High")],
|
|
42
|
+
default="LOW",
|
|
43
|
+
max_length=16,
|
|
44
|
+
verbose_name="Priority level",
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
("comment", models.TextField(default="", verbose_name="Comment")),
|
|
48
|
+
("widget_endpoint", models.CharField(default="", max_length=256, verbose_name="Widget Endpoint")),
|
|
49
|
+
(
|
|
50
|
+
"tags",
|
|
51
|
+
models.ManyToManyField(blank=True, related_name="%(app_label)s_%(class)s_items", to="tags.tag"),
|
|
52
|
+
),
|
|
53
|
+
(
|
|
54
|
+
"assigned_to",
|
|
55
|
+
models.ManyToManyField(
|
|
56
|
+
blank=True,
|
|
57
|
+
help_text="The list of participants",
|
|
58
|
+
related_name="participates_tasks",
|
|
59
|
+
to="directory.person",
|
|
60
|
+
verbose_name="Participants",
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
(
|
|
64
|
+
"calendaritem_ptr",
|
|
65
|
+
models.OneToOneField(
|
|
66
|
+
auto_created=True,
|
|
67
|
+
default=None,
|
|
68
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
69
|
+
parent_link=True,
|
|
70
|
+
primary_key=True,
|
|
71
|
+
serialize=False,
|
|
72
|
+
to="agenda.calendaritem",
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
(
|
|
76
|
+
"in_charge",
|
|
77
|
+
models.ForeignKey(
|
|
78
|
+
blank=True,
|
|
79
|
+
null=True,
|
|
80
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
81
|
+
related_name="in_charge_of_tasks",
|
|
82
|
+
to="directory.person",
|
|
83
|
+
verbose_name="In charge",
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
(
|
|
87
|
+
"requester",
|
|
88
|
+
models.ForeignKey(
|
|
89
|
+
blank=True,
|
|
90
|
+
null=True,
|
|
91
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
92
|
+
related_name="created_tasks",
|
|
93
|
+
to="directory.person",
|
|
94
|
+
verbose_name="Requester",
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
],
|
|
98
|
+
options={
|
|
99
|
+
"abstract": False,
|
|
100
|
+
},
|
|
101
|
+
),
|
|
102
|
+
migrations.AlterField(
|
|
103
|
+
model_name="task",
|
|
104
|
+
name="calendaritem_ptr",
|
|
105
|
+
field=models.OneToOneField(
|
|
106
|
+
auto_created=True,
|
|
107
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
108
|
+
parent_link=True,
|
|
109
|
+
primary_key=True,
|
|
110
|
+
serialize=False,
|
|
111
|
+
to="agenda.calendaritem",
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
]
|
|
File without changes
|
wbtasks/models.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from django.db.models.signals import post_save
|
|
5
|
+
from django.dispatch import receiver
|
|
6
|
+
from django.utils import timezone
|
|
7
|
+
from django_fsm import FSMField, transition
|
|
8
|
+
from psycopg.types.range import TimestamptzRange
|
|
9
|
+
from rest_framework.reverse import reverse
|
|
10
|
+
from wbcore.contrib.agenda.models import CalendarItem
|
|
11
|
+
from wbcore.contrib.agenda.signals import draggable_calendar_item_ids
|
|
12
|
+
from wbcore.contrib.icons import WBIcon
|
|
13
|
+
from wbcore.contrib.notifications.dispatch import send_notification
|
|
14
|
+
from wbcore.contrib.notifications.utils import create_notification_type
|
|
15
|
+
from wbcore.contrib.tags.models import TagModelMixin
|
|
16
|
+
from wbcore.enums import RequestType
|
|
17
|
+
from wbcore.metadata.configs.buttons import ActionButton, ButtonDefaultColor
|
|
18
|
+
from wbcore.metadata.configs.display.instance_display.shortcuts import (
|
|
19
|
+
create_simple_display,
|
|
20
|
+
)
|
|
21
|
+
from wbcore.shares.signals import handle_widget_sharing
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def can_modify_status(instance, user):
|
|
25
|
+
return hasattr(user, "profile") and user.profile in [instance.requester, instance.in_charge]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Task(TagModelMixin, CalendarItem):
|
|
29
|
+
class Status(models.TextChoices):
|
|
30
|
+
UNSCHEDULED = "UNSCHEDULED", "Unscheduled"
|
|
31
|
+
STARTED = "STARTED", "Started"
|
|
32
|
+
COMPLETED = "COMPLETED", "Completed"
|
|
33
|
+
|
|
34
|
+
class Priority(models.TextChoices):
|
|
35
|
+
LOW = "LOW", "Low"
|
|
36
|
+
MEDIUM = "MEDIUM", "Medium"
|
|
37
|
+
HIGH = "HIGH", "High"
|
|
38
|
+
|
|
39
|
+
fsm_base_button_parameters = {
|
|
40
|
+
"method": RequestType.PATCH,
|
|
41
|
+
"identifiers": ("wbtasks:task",),
|
|
42
|
+
"description_fields": "<p>{{ title }}</p>",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
start_button = ActionButton(
|
|
46
|
+
key="start",
|
|
47
|
+
label="Start",
|
|
48
|
+
action_label="start",
|
|
49
|
+
color=ButtonDefaultColor.WARNING,
|
|
50
|
+
icon=WBIcon.SEND.icon,
|
|
51
|
+
**fsm_base_button_parameters,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
complete_button = ActionButton(
|
|
55
|
+
key="complete",
|
|
56
|
+
label="Complete",
|
|
57
|
+
action_label="complete",
|
|
58
|
+
color=ButtonDefaultColor.SUCCESS,
|
|
59
|
+
instance_display=create_simple_display([["comment"]]),
|
|
60
|
+
icon=WBIcon.CONFIRM.icon,
|
|
61
|
+
**fsm_base_button_parameters,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
due_date = models.DateTimeField(verbose_name="Due Date", null=True, blank=True)
|
|
65
|
+
creation_date = models.DateTimeField(auto_now_add=True, verbose_name="Creation Date")
|
|
66
|
+
starting_date = models.DateTimeField(blank=True, null=True, verbose_name="Started at")
|
|
67
|
+
completion_date = models.DateTimeField(blank=True, null=True, verbose_name="Completed at")
|
|
68
|
+
|
|
69
|
+
requester = models.ForeignKey(
|
|
70
|
+
"directory.Person",
|
|
71
|
+
verbose_name="Requester",
|
|
72
|
+
related_name="created_tasks",
|
|
73
|
+
null=True,
|
|
74
|
+
blank=True,
|
|
75
|
+
on_delete=models.CASCADE,
|
|
76
|
+
)
|
|
77
|
+
in_charge = models.ForeignKey(
|
|
78
|
+
"directory.Person",
|
|
79
|
+
verbose_name="In charge",
|
|
80
|
+
related_name="in_charge_of_tasks",
|
|
81
|
+
on_delete=models.CASCADE,
|
|
82
|
+
blank=True,
|
|
83
|
+
null=True,
|
|
84
|
+
)
|
|
85
|
+
assigned_to = models.ManyToManyField(
|
|
86
|
+
"directory.Person",
|
|
87
|
+
related_name="participates_tasks",
|
|
88
|
+
blank=True,
|
|
89
|
+
verbose_name="Participants",
|
|
90
|
+
help_text="The list of participants",
|
|
91
|
+
)
|
|
92
|
+
status = FSMField(default=Status.UNSCHEDULED, choices=Status.choices, verbose_name="Status")
|
|
93
|
+
|
|
94
|
+
description = models.TextField(default="", verbose_name="Description")
|
|
95
|
+
comment = models.TextField(default="", verbose_name="Comment")
|
|
96
|
+
priority = models.CharField(
|
|
97
|
+
default=Priority.LOW, choices=Priority.choices, verbose_name="Priority level", max_length=16
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
widget_endpoint = models.CharField(max_length=256, verbose_name="Widget Endpoint", default="")
|
|
101
|
+
|
|
102
|
+
class Meta:
|
|
103
|
+
notification_types = [
|
|
104
|
+
create_notification_type(
|
|
105
|
+
"wbtasks.task.notify", "Task Notification", "Sends a notification when a task is due."
|
|
106
|
+
),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
@transition(
|
|
110
|
+
status,
|
|
111
|
+
source=[Status.UNSCHEDULED],
|
|
112
|
+
target=Status.STARTED,
|
|
113
|
+
permission=can_modify_status,
|
|
114
|
+
custom={"_transition_button": start_button},
|
|
115
|
+
)
|
|
116
|
+
def start(self, by=None, description=None, **kwargs):
|
|
117
|
+
self.starting_date = timezone.now()
|
|
118
|
+
|
|
119
|
+
@transition(
|
|
120
|
+
status,
|
|
121
|
+
source=[Status.STARTED],
|
|
122
|
+
target=Status.COMPLETED,
|
|
123
|
+
permission=can_modify_status,
|
|
124
|
+
custom={"_transition_button": complete_button},
|
|
125
|
+
)
|
|
126
|
+
def complete(self, by=None, description=None, **kwargs):
|
|
127
|
+
self.completion_date = timezone.now()
|
|
128
|
+
|
|
129
|
+
def get_tag_detail_endpoint(self):
|
|
130
|
+
return reverse("wbtasks:task-detail", [self.id])
|
|
131
|
+
|
|
132
|
+
def get_tag_representation(self):
|
|
133
|
+
return self.title
|
|
134
|
+
|
|
135
|
+
def get_color(self) -> str:
|
|
136
|
+
return "#f48474" # light red
|
|
137
|
+
|
|
138
|
+
def get_icon(self) -> str:
|
|
139
|
+
return WBIcon.WARNING.icon
|
|
140
|
+
|
|
141
|
+
def save(self, *args, **kwargs):
|
|
142
|
+
if self.due_date and not (self.starting_date and self.completion_date):
|
|
143
|
+
start = self.due_date
|
|
144
|
+
end = self.due_date + timedelta(seconds=1)
|
|
145
|
+
self.period = TimestamptzRange(start, end)
|
|
146
|
+
elif self.starting_date and self.completion_date:
|
|
147
|
+
self.period = TimestamptzRange(self.starting_date, self.completion_date)
|
|
148
|
+
|
|
149
|
+
return super().save(*args, **kwargs)
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def get_endpoint_basename(cls):
|
|
153
|
+
return "wbtasks:task"
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def get_representation_endpoint(cls):
|
|
157
|
+
return "wbtasks:taskrepresentation-list"
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def get_representation_value_key(cls):
|
|
161
|
+
return "id"
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def get_representation_label_key(cls):
|
|
165
|
+
return "title"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@receiver(post_save, sender=Task)
|
|
169
|
+
def post_save_task(sender, instance: Task, created: bool, **kwargs):
|
|
170
|
+
"""
|
|
171
|
+
Post save signal.
|
|
172
|
+
* Notifies assigned_to person that a task has been assigned to him
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
if (
|
|
176
|
+
created
|
|
177
|
+
and instance.requester != instance.in_charge
|
|
178
|
+
and instance.in_charge
|
|
179
|
+
and hasattr(instance.in_charge, "user_account")
|
|
180
|
+
):
|
|
181
|
+
send_notification(
|
|
182
|
+
code="wbtasks.task.notify",
|
|
183
|
+
title="A new task has been assigned to you",
|
|
184
|
+
body=f"The task {instance.title} was requested by {str(instance.in_charge)} and is due {instance.due_date:%d.%m.%Y}, check it out!",
|
|
185
|
+
user=instance.in_charge.user_account,
|
|
186
|
+
reverse_name="wbtasks:task-detail",
|
|
187
|
+
reverse_args=[instance.id],
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
instance.entities.set(instance.assigned_to.values_list("id", flat=True))
|
|
191
|
+
if in_charge := instance.in_charge:
|
|
192
|
+
instance.entities.add(in_charge)
|
|
193
|
+
if requester := instance.requester:
|
|
194
|
+
instance.entities.add(requester)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@receiver(draggable_calendar_item_ids, sender="agenda.CalendarItem")
|
|
198
|
+
def tasks_draggable_calendar_item_ids(sender, request, **kwargs) -> models.QuerySet[CalendarItem]:
|
|
199
|
+
return Task.objects.filter(in_charge=request.user.profile, status=Task.Status.UNSCHEDULED).values("id")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@receiver(handle_widget_sharing)
|
|
203
|
+
def received_handle_share(
|
|
204
|
+
request,
|
|
205
|
+
widget_relative_endpoint,
|
|
206
|
+
task_share=None,
|
|
207
|
+
task_title=None,
|
|
208
|
+
task_description=None,
|
|
209
|
+
task_due_date=None,
|
|
210
|
+
task_in_charge=None,
|
|
211
|
+
**kwargs,
|
|
212
|
+
):
|
|
213
|
+
if task_share and task_in_charge:
|
|
214
|
+
Task.objects.create(
|
|
215
|
+
requester=request.user.profile,
|
|
216
|
+
in_charge=task_in_charge,
|
|
217
|
+
title=task_title if task_title else "Share widget Task",
|
|
218
|
+
description=task_description if task_description else "",
|
|
219
|
+
widget_endpoint=widget_relative_endpoint,
|
|
220
|
+
due_date=task_due_date,
|
|
221
|
+
)
|
wbtasks/serializers.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from datetime import date, timedelta
|
|
2
|
+
|
|
3
|
+
from django.utils.translation import gettext_lazy as _
|
|
4
|
+
from wbcore import serializers as wb_serializers
|
|
5
|
+
from wbcore import shares
|
|
6
|
+
from wbcore.contrib.directory.models import Person
|
|
7
|
+
from wbcore.contrib.directory.serializers import (
|
|
8
|
+
InternalUserProfileRepresentationSerializer,
|
|
9
|
+
PersonRepresentationSerializer,
|
|
10
|
+
)
|
|
11
|
+
from wbcore.contrib.tags.serializers import TagSerializerMixin
|
|
12
|
+
from wbcore.metadata.configs.display.instance_display import (
|
|
13
|
+
create_simple_section,
|
|
14
|
+
repeat_field,
|
|
15
|
+
)
|
|
16
|
+
from wbtasks.models import Task
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TaskRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
20
|
+
class Meta:
|
|
21
|
+
model = Task
|
|
22
|
+
fields = ("id", "title")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TaskModelSerializer(TagSerializerMixin, wb_serializers.ModelSerializer):
|
|
26
|
+
_requester = PersonRepresentationSerializer(source="requester")
|
|
27
|
+
_in_charge = InternalUserProfileRepresentationSerializer(source="in_charge")
|
|
28
|
+
_assigned_to = PersonRepresentationSerializer(source="assigned_to", many=True)
|
|
29
|
+
|
|
30
|
+
@wb_serializers.register_resource()
|
|
31
|
+
def ressource(self, instance, request, user):
|
|
32
|
+
ressource = {}
|
|
33
|
+
if instance.widget_endpoint:
|
|
34
|
+
ressource["widget"] = instance.widget_endpoint
|
|
35
|
+
return ressource
|
|
36
|
+
|
|
37
|
+
def create(self, validated_data):
|
|
38
|
+
if request := self.context.get("request"):
|
|
39
|
+
validated_data["requester"] = request.user.profile
|
|
40
|
+
return super().create(validated_data)
|
|
41
|
+
|
|
42
|
+
def validate(self, attrs):
|
|
43
|
+
if self.instance:
|
|
44
|
+
assigned_to = attrs.get("assigned_to", self.instance.assigned_to)
|
|
45
|
+
in_charge = attrs.get("in_charge", self.instance.in_charge)
|
|
46
|
+
else:
|
|
47
|
+
assigned_to = attrs.get("assigned_to", [])
|
|
48
|
+
in_charge = attrs.get("in_charge", None)
|
|
49
|
+
|
|
50
|
+
if assigned_to or in_charge:
|
|
51
|
+
list_of_ids = []
|
|
52
|
+
if in_charge:
|
|
53
|
+
list_of_ids.append(in_charge.id)
|
|
54
|
+
if assigned_to:
|
|
55
|
+
if isinstance(assigned_to, list):
|
|
56
|
+
for person in assigned_to:
|
|
57
|
+
list_of_ids.append(person.id)
|
|
58
|
+
else:
|
|
59
|
+
list_of_ids += assigned_to.values_list("id", flat=True)
|
|
60
|
+
|
|
61
|
+
attrs["entities"] = Person.objects.filter(id__in=list_of_ids).distinct()
|
|
62
|
+
|
|
63
|
+
return super().validate(attrs)
|
|
64
|
+
|
|
65
|
+
class Meta:
|
|
66
|
+
model = Task
|
|
67
|
+
required_fields = ("title", "requester")
|
|
68
|
+
read_only_fields = (
|
|
69
|
+
"starting_date",
|
|
70
|
+
"completion_date",
|
|
71
|
+
"creation_date",
|
|
72
|
+
)
|
|
73
|
+
fields = (
|
|
74
|
+
"id",
|
|
75
|
+
"title",
|
|
76
|
+
"starting_date",
|
|
77
|
+
"completion_date",
|
|
78
|
+
"creation_date",
|
|
79
|
+
"due_date",
|
|
80
|
+
"requester",
|
|
81
|
+
"_requester",
|
|
82
|
+
"in_charge",
|
|
83
|
+
"_in_charge",
|
|
84
|
+
"assigned_to",
|
|
85
|
+
"_assigned_to",
|
|
86
|
+
"status",
|
|
87
|
+
"description",
|
|
88
|
+
"comment",
|
|
89
|
+
"priority",
|
|
90
|
+
"tags",
|
|
91
|
+
"_tags",
|
|
92
|
+
"_additional_resources",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TaskToActivitySerializer(TaskModelSerializer):
|
|
97
|
+
title = wb_serializers.CharField(label="Title", required=False)
|
|
98
|
+
starting_date = wb_serializers.DateTimeField(label="Start")
|
|
99
|
+
completion_date = wb_serializers.DateTimeField(label="End")
|
|
100
|
+
requester = wb_serializers.PrimaryKeyRelatedField(queryset=Person.objects.all(), label="Creator")
|
|
101
|
+
in_charge = wb_serializers.PrimaryKeyRelatedField(queryset=Person.objects.all(), label="In charge to")
|
|
102
|
+
description = wb_serializers.TextField(label="Description")
|
|
103
|
+
comment = wb_serializers.TextField(label="Review")
|
|
104
|
+
|
|
105
|
+
class Meta:
|
|
106
|
+
model = Task
|
|
107
|
+
fields = (
|
|
108
|
+
"id",
|
|
109
|
+
"title",
|
|
110
|
+
"starting_date",
|
|
111
|
+
"completion_date",
|
|
112
|
+
"requester",
|
|
113
|
+
"_requester",
|
|
114
|
+
"in_charge",
|
|
115
|
+
"_in_charge",
|
|
116
|
+
"assigned_to",
|
|
117
|
+
"_assigned_to",
|
|
118
|
+
"description",
|
|
119
|
+
"comment",
|
|
120
|
+
"_requester",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@shares.register(
|
|
125
|
+
section=create_simple_section(
|
|
126
|
+
"task_section",
|
|
127
|
+
_("Share as Task"),
|
|
128
|
+
[
|
|
129
|
+
[repeat_field(4, "task_share")],
|
|
130
|
+
[repeat_field(4, "task_title")],
|
|
131
|
+
[repeat_field(2, "task_due_date"), repeat_field(2, "task_in_charge")],
|
|
132
|
+
[repeat_field(4, "task_description")],
|
|
133
|
+
],
|
|
134
|
+
collapsed=True,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
class TaskShareSerializer(wb_serializers.Serializer):
|
|
138
|
+
task_share = wb_serializers.BooleanField(default=False, label=_("Share as Task"))
|
|
139
|
+
task_title = wb_serializers.CharField(
|
|
140
|
+
max_length=256, label=_("Title"), required=False, depends_on=[{"field": "task_share", "options": {}}]
|
|
141
|
+
)
|
|
142
|
+
task_description = wb_serializers.TextField(
|
|
143
|
+
label=_("Description"), required=False, depends_on=[{"field": "task_share", "options": {}}]
|
|
144
|
+
)
|
|
145
|
+
task_due_date = wb_serializers.DateField(
|
|
146
|
+
label=_("Date"),
|
|
147
|
+
required=False,
|
|
148
|
+
default=lambda: date.today() + timedelta(days=7),
|
|
149
|
+
depends_on=[{"field": "task_share", "options": {}}],
|
|
150
|
+
)
|
|
151
|
+
task_in_charge = wb_serializers.PrimaryKeyRelatedField(
|
|
152
|
+
queryset=Person.objects.all(),
|
|
153
|
+
label=_("In Charge"),
|
|
154
|
+
required=False,
|
|
155
|
+
depends_on=[{"field": "task_share", "options": {}}],
|
|
156
|
+
)
|
|
157
|
+
_task_in_charge = PersonRepresentationSerializer(
|
|
158
|
+
source="task_in_charge", depends_on=[{"field": "task_share", "options": {}}]
|
|
159
|
+
)
|
wbtasks/tasks.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import absolute_import, unicode_literals
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
from celery import shared_task
|
|
6
|
+
from django.db.models import Q
|
|
7
|
+
from django.utils import timezone
|
|
8
|
+
from wbcore.contrib.notifications.dispatch import send_notification
|
|
9
|
+
from wbtasks.models import Task
|
|
10
|
+
|
|
11
|
+
NOTIFY_DUE_TASKS_INTERVAL_MINUTES = 60
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@shared_task
|
|
15
|
+
def notify_due_tasks(now=None, notification=None):
|
|
16
|
+
if not now:
|
|
17
|
+
now = timezone.now()
|
|
18
|
+
for task in Task.objects.filter(
|
|
19
|
+
~Q(status=Task.Status.COMPLETED)
|
|
20
|
+
& Q(due_date__gt=now)
|
|
21
|
+
& Q(due_date__lt=now + timedelta(minutes=NOTIFY_DUE_TASKS_INTERVAL_MINUTES))
|
|
22
|
+
):
|
|
23
|
+
if (profile := task.in_charge) and (user := profile.user_account):
|
|
24
|
+
send_notification(
|
|
25
|
+
code="wbtasks.task.notify",
|
|
26
|
+
title="Task is due",
|
|
27
|
+
body=f"the task {task.title} is due at {task.due_date:%d.%m.%Y}",
|
|
28
|
+
user=user,
|
|
29
|
+
reverse_name="wbtasks:task-detail",
|
|
30
|
+
reverse_args=[task.id],
|
|
31
|
+
)
|
|
32
|
+
return notification
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# from restbench.celery import app as celery_app
|
|
36
|
+
|
|
37
|
+
# celery_app.conf.beat_schedule.update({
|
|
38
|
+
# 'Tasks: Notify Due tasks': {
|
|
39
|
+
# 'task': 'wbtasks.tasks.notify_due_tasks',
|
|
40
|
+
# 'schedule': NOTIFY_DUE_TASKS_INTERVAL_MINUTES*60
|
|
41
|
+
# }
|
|
42
|
+
# })
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.apps import apps
|
|
2
|
+
from django.db import connection
|
|
3
|
+
from django.db.models.signals import pre_migrate
|
|
4
|
+
from pytest_factoryboy import register
|
|
5
|
+
from wbcore.contrib.geography.tests.signals import app_pre_migration
|
|
6
|
+
from wbtasks.factories import TaskFactory
|
|
7
|
+
|
|
8
|
+
register(TaskFactory)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
pre_migrate.connect(app_pre_migration, sender=apps.get_app_config("wbtasks"))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from wbcore.contrib.authentication.factories import AuthenticatedPersonFactory
|
|
3
|
+
from wbtasks.viewsets.viewsets import TaskModelViewSet
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.django_db
|
|
7
|
+
class TestSpecificFilters:
|
|
8
|
+
def test_filter_in_charge(self, task_factory):
|
|
9
|
+
person = AuthenticatedPersonFactory()
|
|
10
|
+
person2 = AuthenticatedPersonFactory()
|
|
11
|
+
task_factory(in_charge=person2)
|
|
12
|
+
mvs = TaskModelViewSet()
|
|
13
|
+
qs = mvs.get_serializer_class().Meta.model.objects.all()
|
|
14
|
+
assert mvs.filterset_class().filter_in_charge(qs, "", None) == qs
|
|
15
|
+
assert mvs.filterset_class().filter_in_charge(qs, "", person).count() == 0
|
|
16
|
+
assert mvs.filterset_class().filter_in_charge(qs, "", person2).count() == 1
|
|
17
|
+
|
|
18
|
+
def test_filter_participants(self, task_factory):
|
|
19
|
+
person = AuthenticatedPersonFactory()
|
|
20
|
+
person2 = AuthenticatedPersonFactory()
|
|
21
|
+
person3 = AuthenticatedPersonFactory()
|
|
22
|
+
task_factory(
|
|
23
|
+
in_charge=person2,
|
|
24
|
+
assigned_to=(
|
|
25
|
+
person2,
|
|
26
|
+
person3,
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
mvs = TaskModelViewSet()
|
|
30
|
+
qs = mvs.get_serializer_class().Meta.model.objects.all()
|
|
31
|
+
assert mvs.filterset_class().filter_participants(qs, "", None) == qs
|
|
32
|
+
assert mvs.filterset_class().filter_participants(qs, "", [person]).count() == 0
|
|
33
|
+
assert mvs.filterset_class().filter_participants(qs, "", [person2]).count() == 1
|
|
34
|
+
assert mvs.filterset_class().filter_participants(qs, "", [person3]).count() == 1
|
|
35
|
+
assert mvs.filterset_class().filter_participants(qs, "", [person2, person3]).count() == 1
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@pytest.mark.django_db
|
|
5
|
+
class TestSpecificModels:
|
|
6
|
+
def test_start(self, task_factory):
|
|
7
|
+
obj = task_factory()
|
|
8
|
+
assert obj.status == obj.Status.UNSCHEDULED
|
|
9
|
+
obj.start()
|
|
10
|
+
assert obj.status == obj.Status.STARTED
|
|
11
|
+
|
|
12
|
+
def test_complete(self, task_factory):
|
|
13
|
+
obj = task_factory()
|
|
14
|
+
obj.start()
|
|
15
|
+
obj.complete()
|
|
16
|
+
assert obj.status == obj.Status.COMPLETED
|
wbtasks/tests/tests.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from wbcore.test import GenerateTest, default_config
|
|
3
|
+
|
|
4
|
+
config = {}
|
|
5
|
+
for key, value in default_config.items():
|
|
6
|
+
config[key] = list(filter(lambda x: x.__module__.startswith("wbtasks"), value))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.django_db
|
|
10
|
+
@GenerateTest(config)
|
|
11
|
+
class TestProject:
|
|
12
|
+
pass
|
wbtasks/urls.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.urls import include, path
|
|
2
|
+
from wbcore.routers import WBCoreRouter
|
|
3
|
+
from wbtasks.viewsets import viewsets
|
|
4
|
+
|
|
5
|
+
router = WBCoreRouter()
|
|
6
|
+
router.register(r"task", viewsets.TaskModelViewSet, basename="task")
|
|
7
|
+
router.register(r"taskrepresentation", viewsets.TaskRepresentationViewSet, basename="taskrepresentation")
|
|
8
|
+
|
|
9
|
+
urlpatterns = [
|
|
10
|
+
path("", include(router.urls)),
|
|
11
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from wbcore.contrib.icons import WBIcon
|
|
2
|
+
from wbcore.metadata.configs import buttons as bt
|
|
3
|
+
from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TaskButtonConfig(ButtonViewConfig):
|
|
7
|
+
def get_custom_instance_buttons(self):
|
|
8
|
+
return {
|
|
9
|
+
bt.WidgetButton(key="see_activity", label="Linked Activity", icon=WBIcon.CALENDAR.icon),
|
|
10
|
+
bt.WidgetButton(key="widget", label="Linked Widget", icon=WBIcon.LINK.icon),
|
|
11
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from wbcore.contrib.color.enums import WBColor
|
|
4
|
+
from wbcore.enums import Operator
|
|
5
|
+
from wbcore.metadata.configs import display as dp
|
|
6
|
+
from wbcore.metadata.configs.display.instance_display.shortcuts import (
|
|
7
|
+
Display,
|
|
8
|
+
create_simple_display,
|
|
9
|
+
)
|
|
10
|
+
from wbcore.metadata.configs.display.view_config import DisplayViewConfig
|
|
11
|
+
from wbtasks.models import Task
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskDisplayConfig(DisplayViewConfig):
|
|
15
|
+
def get_list_display(self) -> Optional[dp.ListDisplay]:
|
|
16
|
+
return dp.ListDisplay(
|
|
17
|
+
fields=[
|
|
18
|
+
dp.Field(key="title", label="Title"),
|
|
19
|
+
dp.Field(key="in_charge", label="In Charge"),
|
|
20
|
+
dp.Field(key="due_date", label="Due at"),
|
|
21
|
+
dp.Field(key="description", label="Description"),
|
|
22
|
+
dp.Field(key="tags", label="Tags"),
|
|
23
|
+
],
|
|
24
|
+
formatting=[
|
|
25
|
+
dp.Formatting(
|
|
26
|
+
formatting_rules=[
|
|
27
|
+
dp.FormattingRule(
|
|
28
|
+
style={"backgroundColor": WBColor.BLUE_LIGHT.value},
|
|
29
|
+
condition=dp.Condition(operator=Operator.EQUAL, value=Task.Status.UNSCHEDULED),
|
|
30
|
+
),
|
|
31
|
+
dp.FormattingRule(
|
|
32
|
+
style={"backgroundColor": WBColor.YELLOW.value},
|
|
33
|
+
condition=dp.Condition(operator=Operator.EQUAL, value=Task.Status.STARTED),
|
|
34
|
+
),
|
|
35
|
+
dp.FormattingRule(
|
|
36
|
+
style={"backgroundColor": WBColor.GREEN_LIGHT.value},
|
|
37
|
+
condition=dp.Condition(operator=Operator.EQUAL, value=Task.Status.COMPLETED),
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
column="status",
|
|
41
|
+
)
|
|
42
|
+
],
|
|
43
|
+
legends=[
|
|
44
|
+
dp.Legend(
|
|
45
|
+
key="status",
|
|
46
|
+
items=[
|
|
47
|
+
dp.LegendItem(
|
|
48
|
+
label=Task.Status.UNSCHEDULED.label,
|
|
49
|
+
icon=WBColor.BLUE_LIGHT.value,
|
|
50
|
+
value=Task.Status.UNSCHEDULED.value,
|
|
51
|
+
),
|
|
52
|
+
dp.LegendItem(
|
|
53
|
+
label=Task.Status.STARTED.label,
|
|
54
|
+
icon=WBColor.YELLOW.value,
|
|
55
|
+
value=Task.Status.STARTED.value,
|
|
56
|
+
),
|
|
57
|
+
dp.LegendItem(
|
|
58
|
+
label=Task.Status.COMPLETED.label,
|
|
59
|
+
icon=WBColor.GREEN_LIGHT.value,
|
|
60
|
+
value=Task.Status.COMPLETED.value,
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def get_instance_display(self) -> Display:
|
|
68
|
+
return create_simple_display(
|
|
69
|
+
[
|
|
70
|
+
["status", "status", "status"],
|
|
71
|
+
["title", "title", "title"],
|
|
72
|
+
["creation_date", "due_date", "."],
|
|
73
|
+
["starting_date", "completion_date", "."],
|
|
74
|
+
["requester", "in_charge", "tags"],
|
|
75
|
+
["description", "description", "description"],
|
|
76
|
+
["comment", "comment", "comment"],
|
|
77
|
+
]
|
|
78
|
+
)
|
wbtasks/viewsets/menu.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from wbcore.menus import ItemPermission, MenuItem
|
|
2
|
+
from wbcore.permissions.shortcuts import is_internal_user
|
|
3
|
+
|
|
4
|
+
TASK_MENUITEM = MenuItem(
|
|
5
|
+
label="Tasks",
|
|
6
|
+
endpoint="wbtasks:task-list",
|
|
7
|
+
add=MenuItem(label="Create Task", endpoint="wbtasks:task-list"),
|
|
8
|
+
permission=ItemPermission(
|
|
9
|
+
method=lambda request: is_internal_user(request.user), permissions=["wbtasks.view_task"]
|
|
10
|
+
),
|
|
11
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from wbcore.metadata.configs.titles import TitleViewConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TaskTitleConfig(TitleViewConfig):
|
|
5
|
+
def get_instance_title(self):
|
|
6
|
+
return "{{title}}"
|
|
7
|
+
|
|
8
|
+
def get_list_title(self):
|
|
9
|
+
return "Tasks"
|
|
10
|
+
|
|
11
|
+
def get_create_title(self):
|
|
12
|
+
return "Create Task"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from rest_framework import filters
|
|
2
|
+
from wbcore.filters import DjangoFilterBackend
|
|
3
|
+
from wbcore.viewsets import ModelViewSet, RepresentationViewSet
|
|
4
|
+
from wbtasks.filters import TaskFilter
|
|
5
|
+
from wbtasks.models import Task
|
|
6
|
+
from wbtasks.serializers import TaskModelSerializer, TaskRepresentationSerializer
|
|
7
|
+
from wbtasks.viewsets import TaskButtonConfig, TaskDisplayConfig, TaskTitleConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskRepresentationViewSet(RepresentationViewSet):
|
|
11
|
+
serializer_class = TaskRepresentationSerializer
|
|
12
|
+
queryset = Task.objects.all()
|
|
13
|
+
|
|
14
|
+
search_fields = ("title",)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskModelViewSet(ModelViewSet):
|
|
18
|
+
IDENTIFIER = "wbtasks:task"
|
|
19
|
+
|
|
20
|
+
filter_backends = (
|
|
21
|
+
DjangoFilterBackend,
|
|
22
|
+
filters.SearchFilter,
|
|
23
|
+
filters.OrderingFilter,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ordering_fields = ["due_date", "creation_date", "starting_date", "completion_date"]
|
|
27
|
+
ordering = ["-due_date"]
|
|
28
|
+
search_fields = ["title", "requester__computed_str", "assigned_to__computed_str", "description"]
|
|
29
|
+
|
|
30
|
+
serializer_class = TaskModelSerializer
|
|
31
|
+
filterset_class = TaskFilter
|
|
32
|
+
queryset = Task.objects.all()
|
|
33
|
+
|
|
34
|
+
display_config_class = TaskDisplayConfig
|
|
35
|
+
button_config_class = TaskButtonConfig
|
|
36
|
+
title_config_class = TaskTitleConfig
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
wbtasks/__init__.py,sha256=uyfGiipFnhOCQlqywx7wsgp6d-SYnqPqsPQd_xePZl8,23
|
|
2
|
+
wbtasks/admin.py,sha256=3rYF0UgidghWI6rPj5rqc5CGBLIC68MA_lngGaHxq6c,256
|
|
3
|
+
wbtasks/apps.py,sha256=eIxOYl_x-u_EHItcUMH9Re5Rn9q2Eo4PI9K8PRgIUTU,89
|
|
4
|
+
wbtasks/factories.py,sha256=TAmS4XE5y5HT9MMwdQijAg5km-2Xt6CVbpw8LlKRmxs,1177
|
|
5
|
+
wbtasks/filters.py,sha256=Q_XdTPGkrMmX45Z4_TUZ3_QSXMvfV2bX7lTAZpmf3Dg,1828
|
|
6
|
+
wbtasks/models.py,sha256=O9P3TU9CBuVLWE8bo7Ms661ZfQP5yZy_VUoj7npndx4,7511
|
|
7
|
+
wbtasks/serializers.py,sha256=J_BaCKtExLzoE32u4iNTO_CrJvgRY9EYzoQmmDpo2zs,5461
|
|
8
|
+
wbtasks/tasks.py,sha256=5bsdkQSKUtCks2mCT5lvEAh8ugyGVVGnLAYW42csPys,1315
|
|
9
|
+
wbtasks/urls.py,sha256=LeCiUudUpxB3aZOucf2RakH12qxk9lq9_1E0zcpernc,371
|
|
10
|
+
wbtasks/migrations/0001_initial_squashed_squashed_0006_alter_task_assigned_to_alter_task_in_charge_and_more.py,sha256=zga3Fo0BOJTHv9A-tFxE1uFnjDtFLr9MehwwR9UDn8Q,4563
|
|
11
|
+
wbtasks/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
wbtasks/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
wbtasks/tests/conftest.py,sha256=9an2J7m-nya6lr3BqRA7CSk_cT1b4Azy4_z8GU-82Rk,364
|
|
14
|
+
wbtasks/tests/test_filters.py,sha256=qVE29DY2zz-ZLWsQcEitZtx3i5npklxncFSrw1KBCrA,1611
|
|
15
|
+
wbtasks/tests/test_models.py,sha256=f6kSF-ZSymFIVawuP_crZuGU-9Jr4CCeLqh9TdBpQbo,420
|
|
16
|
+
wbtasks/tests/test_serializers.py,sha256=yiF6x5IRbWKwndlZkVlFwQd-zCebAJVbqmfDbpI87dE,79
|
|
17
|
+
wbtasks/tests/test_tasks.py,sha256=mAc8oOD3wyYTLYjchfjfynMVCqsgJ7He-cQRghVjUdY,73
|
|
18
|
+
wbtasks/tests/tests.py,sha256=io2rUOF_-psqMduZ-b7pKxvZGULcvM_kVs7wHiF7FII,281
|
|
19
|
+
wbtasks/viewsets/__init__.py,sha256=0g4dXN9rw6kqdAchmaxnkkjvhRF3H-LUjDNejr7hcOo,161
|
|
20
|
+
wbtasks/viewsets/buttons.py,sha256=Ifb8tUGvS1zt0QNaSiUrmpGHzP1-YxC5iybgaI0NMzE,467
|
|
21
|
+
wbtasks/viewsets/display.py,sha256=cNFSPOXDPm0f2gPQiz0iN3bfwrAfYQ1uWXNzV3yt57g,3223
|
|
22
|
+
wbtasks/viewsets/menu.py,sha256=xvwBp3V2fPMNtdk3KV_bfB4_8PLO4BWSErquwkU5spI,394
|
|
23
|
+
wbtasks/viewsets/titles.py,sha256=mEIGvg7Kxz050T-3pKmzUjNfxLsSwsuEwSFKrmF_Sus,278
|
|
24
|
+
wbtasks/viewsets/viewsets.py,sha256=eUP0QuxhGQvcTPFA00TF6gB3YCdqg6YLbs-mRFsa3OE,1211
|
|
25
|
+
wbtasks-2.2.1.dist-info/METADATA,sha256=PHfu0GdTPJ-kQOXQGB15JmvXVx0hRF8uBUQcb1Ng3_M,137
|
|
26
|
+
wbtasks-2.2.1.dist-info/WHEEL,sha256=aO3RJuuiFXItVSnAUEmQ0yRBvv9e1sbJh68PtuQkyAE,105
|
|
27
|
+
wbtasks-2.2.1.dist-info/RECORD,,
|