From 636f9fbcc06d52a4ffc8c0fbe0ee5f67660b599c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20M=C3=A9ndez?= Date: Fri, 1 Mar 2024 05:10:52 -0600 Subject: [PATCH 1/7] Sort stories inside sprints by priority --- matorral/stories/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matorral/stories/views.py b/matorral/stories/views.py index ec4acd4..d82e139 100644 --- a/matorral/stories/views.py +++ b/matorral/stories/views.py @@ -54,7 +54,7 @@ def get_children(self): except KeyError: return [(None, queryset)] else: - queryset = queryset.order_by(F(order_by).asc(nulls_last=True)) + queryset = queryset.order_by(F(order_by).asc(nulls_last=True), "priority") foo = [(t[0], list(t[1])) for t in groupby(queryset, key=fx)] return foo From 9da4e5c44b06798dbd9d94d430a2769e09731522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20M=C3=A9ndez?= Date: Fri, 1 Mar 2024 05:37:19 -0600 Subject: [PATCH 2/7] Fixed epic & sprint progress calculation --- matorral/models.py | 15 +++++++++++---- matorral/stories/tasks.py | 5 ++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/matorral/models.py b/matorral/models.py index 34e676c..967d879 100644 --- a/matorral/models.py +++ b/matorral/models.py @@ -42,20 +42,27 @@ def update_points_and_progress(self, save=True): parent_dict = {self._meta.model_name: self.id} + # calculate total points total_points = Story.objects.filter(**parent_dict).aggregate(models.Sum("points"))["points__sum"] or 0 + if total_points == 0: + # if no story has points, then count the stories + total_points = Story.objects.filter(**parent_dict).count() + + # calculate points done params = parent_dict.copy() params["state__stype"] = StoryState.STATE_DONE points_done = Story.objects.filter(**params).aggregate(models.Sum("points"))["points__sum"] or 0 + if points_done == 0: + # if no story has points, then count the stories + points_done = Story.objects.filter(**params).count() + self.total_points = total_points self.points_done = points_done self.story_count = Story.objects.filter(**parent_dict).count() - if total_points > 0: - self.progress = int(float(points_done) / total_points * 100) - else: - self.progress = 0 + self.progress = int(float(points_done) / (total_points or 1) * 100) if save: self.save() diff --git a/matorral/stories/tasks.py b/matorral/stories/tasks.py index 4050e84..67adc04 100644 --- a/matorral/stories/tasks.py +++ b/matorral/stories/tasks.py @@ -40,7 +40,10 @@ def story_set_state(story_ids, state_slug): except StoryState.DoesNotExist: return - Story.objects.filter(id__in=story_ids).update(state=state) + # update stories one by one to trigger signals and tasks that update the progress and points, etc + for story in Story.objects.filter(id__in=story_ids): + story.state = state + story.save() @app.task(ignore_result=True) From eaed2ffd185b610ff1ec9e42fba60275304cc998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20M=C3=A9ndez?= Date: Fri, 1 Mar 2024 05:54:31 -0600 Subject: [PATCH 3/7] Setting completed_at field correctly --- matorral/models.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/matorral/models.py b/matorral/models.py index 967d879..f5d3b9a 100644 --- a/matorral/models.py +++ b/matorral/models.py @@ -1,5 +1,6 @@ from django.apps import apps from django.db import models +from django.utils import timezone class BaseModel(models.Model): @@ -16,6 +17,17 @@ class Meta: def __str__(self): return self.title + def is_done(self): + return self.state.stype == self.state.STATE_DONE + + def save(self, *args, **kwargs): + if self.is_done(): + self.completed_at = timezone.now() + else: + self.completed_at = None + + super().save(*args, **kwargs) + class ModelWithProgress(models.Model): class Meta: From f40950cb72d36ab59f06f7afe98080df9738ee78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20M=C3=A9ndez?= Date: Fri, 1 Mar 2024 06:21:16 -0600 Subject: [PATCH 4/7] Update sprint state when certain events ocurr --- matorral/sprints/tasks.py | 3 +++ matorral/stories/tasks.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/matorral/sprints/tasks.py b/matorral/sprints/tasks.py index 3a0fcdb..3d6dbef 100644 --- a/matorral/sprints/tasks.py +++ b/matorral/sprints/tasks.py @@ -42,6 +42,8 @@ def reset_sprint(story_ids): for sprint in Sprint.objects.filter(id__in=sprint_ids): sprint.update_points_and_progress() + update_state.delay() + @app.task(ignore_result=True) def handle_sprint_change(epic_id): @@ -51,3 +53,4 @@ def handle_sprint_change(epic_id): return sprint.update_points_and_progress() + update_state.delay() diff --git a/matorral/stories/tasks.py b/matorral/stories/tasks.py index 67adc04..460c697 100644 --- a/matorral/stories/tasks.py +++ b/matorral/stories/tasks.py @@ -1,6 +1,7 @@ from matorral.taskapp.celery import app from .models import Epic, EpicState, Story, StoryState +from matorral.sprints.tasks import update_state as update_sprint_state @app.task(ignore_result=True) @@ -27,6 +28,8 @@ def remove_stories(story_ids): for sprint in Sprint.objects.filter(story__id__in=story_ids).distinct(): sprint.update_points_and_progress() + update_sprint_state.delay() + @app.task(ignore_result=True) def story_set_assignee(story_ids, user_id): @@ -45,6 +48,8 @@ def story_set_state(story_ids, state_slug): story.state = state story.save() + update_sprint_state.delay() + @app.task(ignore_result=True) def duplicate_epics(epic_ids): @@ -112,6 +117,7 @@ def handle_story_change(story_id): if story.sprint is not None: story.sprint.update_points_and_progress() + update_sprint_state.delay() @app.task(ignore_result=True) @@ -148,3 +154,5 @@ def story_set_sprint(story_ids, sprint_id): for story in Story.objects.filter(id__in=story_ids): story.sprint = sprint story.save() + + update_sprint_state.delay() From 550721b5e1218c071e02fcaf831f2bfc06906ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20M=C3=A9ndez?= Date: Fri, 1 Mar 2024 11:49:34 -0600 Subject: [PATCH 5/7] Added args to hatch scripts for local env --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fbfe2c5..ca98c67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,9 +83,9 @@ extra-dependencies = [ ] [tool.hatch.envs.local.scripts] -server = "python manage.py runserver_plus --settings=config.settings.local" -shell = "python manage.py shell_plus --settings=config.settings.local" -migrate = "python manage.py migrate --settings=config.settings.local" +server = "python manage.py runserver_plus --settings=config.settings.local {args}" +shell = "python manage.py shell_plus --settings=config.settings.local {args}" +migrate = "python manage.py migrate --settings=config.settings.local {args}" makemigrations = "python manage.py makemigrations --settings=config.settings.local {args}" # Production environment From 3b780c59576d4cdb44ac9e79660457ddd6f28a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20M=C3=A9ndez?= Date: Sat, 2 Mar 2024 07:18:41 -0600 Subject: [PATCH 6/7] Added missing migration for sprints app --- .../migrations/0009_alter_sprint_options.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 matorral/sprints/migrations/0009_alter_sprint_options.py diff --git a/matorral/sprints/migrations/0009_alter_sprint_options.py b/matorral/sprints/migrations/0009_alter_sprint_options.py new file mode 100644 index 0000000..482b8f3 --- /dev/null +++ b/matorral/sprints/migrations/0009_alter_sprint_options.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.10 on 2024-03-02 00:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("sprints", "0008_alter_historicalsprint_options_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="sprint", + options={ + "get_latest_by": "created_at", + "ordering": ["starts_at", "-updated_at"], + "verbose_name": "sprint", + "verbose_name_plural": "sprints", + }, + ), + ] From 3701e543a8268d2d8b3f0c67d28d743a5279172d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20M=C3=A9ndez?= Date: Sat, 2 Mar 2024 07:19:52 -0600 Subject: [PATCH 7/7] Added data migration to create default workspace and migrate users to it --- matorral/stories/forms.py | 8 +-- .../migrations/0005_auto_20240302_1301.py | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 matorral/workspaces/migrations/0005_auto_20240302_1301.py diff --git a/matorral/stories/forms.py b/matorral/stories/forms.py index f69aff3..173f686 100644 --- a/matorral/stories/forms.py +++ b/matorral/stories/forms.py @@ -91,11 +91,7 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["requester"].queryset = User.objects.filter(is_active=True, workspace=self.workspace).order_by( - "username" - ) - self.fields["assignee"].queryset = User.objects.filter(is_active=True, workspace=self.workspace).order_by( - "username" - ) + self.fields["requester"].queryset = self.workspace.members.filter(is_active=True).order_by("username") + self.fields["assignee"].queryset = self.workspace.members.filter(is_active=True).order_by("username") self.fields["epic"].queryset = Epic.objects.filter(workspace=self.workspace).order_by("title") self.fields["sprint"].queryset = Sprint.objects.filter(workspace=self.workspace).order_by("ends_at") diff --git a/matorral/workspaces/migrations/0005_auto_20240302_1301.py b/matorral/workspaces/migrations/0005_auto_20240302_1301.py new file mode 100644 index 0000000..7a4ab34 --- /dev/null +++ b/matorral/workspaces/migrations/0005_auto_20240302_1301.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2.10 on 2024-03-02 13:01 + +from django.db import migrations + + +def create_default_workspace(apps, schema_editor): + """ + Creates the default workspace and add all the users to it + """ + + User = apps.get_model("users", "User") + + if User.objects.count() == 0: + # No users, nothing to do: the workspace will be created when the first user is created + return + + # Get the first superuser or admin user + admin_user = User.objects.filter(is_superuser=True).first() + + if admin_user is None: + admin_user = User.objects.filter(is_staff=True).first() + + if admin_user is None: + admin_user = User.objects.first() + + Workspace = apps.get_model("workspaces", "Workspace") + default_workspace = Workspace.objects.create( + name="Default Workspace", slug="default-workspace", owner=admin_user + ) + + # Now add all the users to the default workspace + for user in User.objects.all(): + default_workspace.members.add(user) + + +def delete_default_workspace(apps, schema_editor): + Workspace = apps.get_model("workspaces", "Workspace") + Workspace.objects.filter(slug="default-workspace").delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("workspaces", "0004_auto_20200815_1608"), + ] + + operations = [ + migrations.RunPython(create_default_workspace, reverse_code=delete_default_workspace), + ]