@ -52,6 +52,18 @@ class TicketForm(
|
|||||||
self.fields['ticket_type'].widget = self.fields['ticket_type'].hidden_widget()
|
self.fields['ticket_type'].widget = self.fields['ticket_type'].hidden_widget()
|
||||||
self.fields['organization'].widget = self.fields['organization'].hidden_widget()
|
self.fields['organization'].widget = self.fields['organization'].hidden_widget()
|
||||||
|
|
||||||
|
if self.instance.project is not None:
|
||||||
|
|
||||||
|
self.fields['milestone'].queryset = self.fields['milestone'].queryset.filter(
|
||||||
|
project=self.instance.project
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
self.fields['milestone'].queryset = self.fields['milestone'].queryset.filter(
|
||||||
|
id=0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
original_fields = self.fields.copy()
|
original_fields = self.fields.copy()
|
||||||
ticket_type = []
|
ticket_type = []
|
||||||
|
@ -49,6 +49,7 @@ class TicketValidation(
|
|||||||
'planned_finish_date',
|
'planned_finish_date',
|
||||||
'priority',
|
'priority',
|
||||||
'project',
|
'project',
|
||||||
|
'milestone',
|
||||||
'real_start_date',
|
'real_start_date',
|
||||||
'real_finish_date',
|
'real_finish_date',
|
||||||
'subscribed_users',
|
'subscribed_users',
|
||||||
@ -67,6 +68,7 @@ class TicketValidation(
|
|||||||
'planned_finish_date',
|
'planned_finish_date',
|
||||||
'priority',
|
'priority',
|
||||||
'project',
|
'project',
|
||||||
|
'milestone',
|
||||||
'real_start_date',
|
'real_start_date',
|
||||||
'real_finish_date',
|
'real_finish_date',
|
||||||
'subscribed_users',
|
'subscribed_users',
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# Generated by Django 5.0.8 on 2024-09-13 05:06
|
# Generated by Django 5.0.8 on 2024-09-14 06:29
|
||||||
|
|
||||||
import access.fields
|
import access.fields
|
||||||
import access.models
|
import access.models
|
||||||
|
import core.lib.slash_commands
|
||||||
import core.models.ticket.ticket
|
import core.models.ticket.ticket
|
||||||
import core.models.ticket.ticket_comment
|
import core.models.ticket.ticket_comment
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -66,6 +67,7 @@ class Migration(migrations.Migration):
|
|||||||
'verbose_name_plural': 'Comments',
|
'verbose_name_plural': 'Comments',
|
||||||
'ordering': ['ticket', 'parent_id'],
|
'ordering': ['ticket', 'parent_id'],
|
||||||
},
|
},
|
||||||
|
bases=(core.lib.slash_commands.SlashCommands, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='TicketCommentCategory',
|
name='TicketCommentCategory',
|
||||||
@ -121,8 +123,6 @@ class Migration(migrations.Migration):
|
|||||||
('real_finish_date', models.DateTimeField(blank=True, help_text='Real finish date', null=True, verbose_name='Real Finish Date')),
|
('real_finish_date', models.DateTimeField(blank=True, help_text='Real finish date', null=True, verbose_name='Real Finish Date')),
|
||||||
('assigned_teams', models.ManyToManyField(blank=True, help_text='Assign the ticket to a Team(s)', related_name='assigned_teams', to='access.team', verbose_name='Assigned Team(s)')),
|
('assigned_teams', models.ManyToManyField(blank=True, help_text='Assign the ticket to a Team(s)', related_name='assigned_teams', to='access.team', verbose_name='Assigned Team(s)')),
|
||||||
('assigned_users', models.ManyToManyField(blank=True, help_text='Assign the ticket to a User(s)', related_name='assigned_users', to=settings.AUTH_USER_MODEL, verbose_name='Assigned User(s)')),
|
('assigned_users', models.ManyToManyField(blank=True, help_text='Assign the ticket to a User(s)', related_name='assigned_users', to=settings.AUTH_USER_MODEL, verbose_name='Assigned User(s)')),
|
||||||
('opened_by', models.ForeignKey(help_text='Who is the ticket for', on_delete=django.db.models.deletion.DO_NOTHING, related_name='opened_by', to=settings.AUTH_USER_MODEL, verbose_name='Opened By')),
|
|
||||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Ticket',
|
'verbose_name': 'Ticket',
|
||||||
@ -130,5 +130,6 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ['id'],
|
'ordering': ['id'],
|
||||||
'permissions': [('add_ticket_request', 'Can add a request ticket'), ('change_ticket_request', 'Can change any request ticket'), ('delete_ticket_request', 'Can delete a request ticket'), ('import_ticket_request', 'Can import a request ticket'), ('purge_ticket_request', 'Can purge a request ticket'), ('triage_ticket_request', 'Can triage all request ticket'), ('view_ticket_request', 'Can view all request ticket'), ('add_ticket_incident', 'Can add a incident ticket'), ('change_ticket_incident', 'Can change any incident ticket'), ('delete_ticket_incident', 'Can delete a incident ticket'), ('import_ticket_incident', 'Can import a incident ticket'), ('purge_ticket_incident', 'Can purge a incident ticket'), ('triage_ticket_incident', 'Can triage all incident ticket'), ('view_ticket_incident', 'Can view all incident ticket'), ('add_ticket_problem', 'Can add a problem ticket'), ('change_ticket_problem', 'Can change any problem ticket'), ('delete_ticket_problem', 'Can delete a problem ticket'), ('import_ticket_problem', 'Can import a problem ticket'), ('purge_ticket_problem', 'Can purge a problem ticket'), ('triage_ticket_problem', 'Can triage all problem ticket'), ('view_ticket_problem', 'Can view all problem ticket'), ('add_ticket_change', 'Can add a change ticket'), ('change_ticket_change', 'Can change any change ticket'), ('delete_ticket_change', 'Can delete a change ticket'), ('import_ticket_change', 'Can import a change ticket'), ('purge_ticket_change', 'Can purge a change ticket'), ('triage_ticket_change', 'Can triage all change ticket'), ('view_ticket_change', 'Can view all change ticket'), ('add_ticket_project_task', 'Can add a project task'), ('change_ticket_project_task', 'Can change any project task'), ('delete_ticket_project_task', 'Can delete a project task'), ('import_ticket_project_task', 'Can import a project task'), ('purge_ticket_project_task', 'Can purge a project task'), ('triage_ticket_project_task', 'Can triage all project task'), ('view_ticket_project_task', 'Can view all project task')],
|
'permissions': [('add_ticket_request', 'Can add a request ticket'), ('change_ticket_request', 'Can change any request ticket'), ('delete_ticket_request', 'Can delete a request ticket'), ('import_ticket_request', 'Can import a request ticket'), ('purge_ticket_request', 'Can purge a request ticket'), ('triage_ticket_request', 'Can triage all request ticket'), ('view_ticket_request', 'Can view all request ticket'), ('add_ticket_incident', 'Can add a incident ticket'), ('change_ticket_incident', 'Can change any incident ticket'), ('delete_ticket_incident', 'Can delete a incident ticket'), ('import_ticket_incident', 'Can import a incident ticket'), ('purge_ticket_incident', 'Can purge a incident ticket'), ('triage_ticket_incident', 'Can triage all incident ticket'), ('view_ticket_incident', 'Can view all incident ticket'), ('add_ticket_problem', 'Can add a problem ticket'), ('change_ticket_problem', 'Can change any problem ticket'), ('delete_ticket_problem', 'Can delete a problem ticket'), ('import_ticket_problem', 'Can import a problem ticket'), ('purge_ticket_problem', 'Can purge a problem ticket'), ('triage_ticket_problem', 'Can triage all problem ticket'), ('view_ticket_problem', 'Can view all problem ticket'), ('add_ticket_change', 'Can add a change ticket'), ('change_ticket_change', 'Can change any change ticket'), ('delete_ticket_change', 'Can delete a change ticket'), ('import_ticket_change', 'Can import a change ticket'), ('purge_ticket_change', 'Can purge a change ticket'), ('triage_ticket_change', 'Can triage all change ticket'), ('view_ticket_change', 'Can view all change ticket'), ('add_ticket_project_task', 'Can add a project task'), ('change_ticket_project_task', 'Can change any project task'), ('delete_ticket_project_task', 'Can delete a project task'), ('import_ticket_project_task', 'Can import a project task'), ('purge_ticket_project_task', 'Can purge a project task'), ('triage_ticket_project_task', 'Can triage all project task'), ('view_ticket_project_task', 'Can view all project task')],
|
||||||
},
|
},
|
||||||
|
bases=(core.lib.slash_commands.SlashCommands, models.Model),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.8 on 2024-09-13 05:06
|
# Generated by Django 5.0.8 on 2024-09-14 06:29
|
||||||
|
|
||||||
import access.models
|
import access.models
|
||||||
import core.models.ticket.ticket_comment
|
import core.models.ticket.ticket_comment
|
||||||
@ -18,6 +18,21 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='milestone',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Assign to a milestone', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='project_management.projectmilestone', verbose_name='Project Milestone'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='opened_by',
|
||||||
|
field=models.ForeignKey(help_text='Who is the ticket for', on_delete=django.db.models.deletion.DO_NOTHING, related_name='opened_by', to=settings.AUTH_USER_MODEL, verbose_name='Opened By'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='organization',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists]),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='ticket',
|
model_name='ticket',
|
||||||
name='project',
|
name='project',
|
@ -11,7 +11,7 @@ from core.lib.slash_commands import SlashCommands
|
|||||||
from core.middleware.get_request import get_request
|
from core.middleware.get_request import get_request
|
||||||
from core.models.ticket.ticket_category import TicketCategory
|
from core.models.ticket.ticket_category import TicketCategory
|
||||||
|
|
||||||
from project_management.models.projects import Project
|
from project_management.models.project_milestone import Project, ProjectMilestone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -528,6 +528,15 @@ class Ticket(
|
|||||||
verbose_name = 'Project',
|
verbose_name = 'Project',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
milestone = models.ForeignKey(
|
||||||
|
ProjectMilestone,
|
||||||
|
blank= True,
|
||||||
|
help_text = 'Assign to a milestone',
|
||||||
|
null = True,
|
||||||
|
on_delete = models.DO_NOTHING,
|
||||||
|
verbose_name = 'Project Milestone',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
opened_by = models.ForeignKey(
|
opened_by = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
@ -642,6 +651,7 @@ class Ticket(
|
|||||||
'category'
|
'category'
|
||||||
'urgency',
|
'urgency',
|
||||||
'project',
|
'project',
|
||||||
|
'milestone',
|
||||||
'priority',
|
'priority',
|
||||||
'impact',
|
'impact',
|
||||||
'subscribed_teams',
|
'subscribed_teams',
|
||||||
@ -680,6 +690,7 @@ class Ticket(
|
|||||||
|
|
||||||
fields_project_task: list(str()) = common_fields + [
|
fields_project_task: list(str()) = common_fields + [
|
||||||
'category',
|
'category',
|
||||||
|
'milestone',
|
||||||
'status',
|
'status',
|
||||||
'urgency',
|
'urgency',
|
||||||
'priority',
|
'priority',
|
||||||
@ -695,6 +706,7 @@ class Ticket(
|
|||||||
tech_fields = [
|
tech_fields = [
|
||||||
'category',
|
'category',
|
||||||
'project',
|
'project',
|
||||||
|
'milestone',
|
||||||
'assigned_users',
|
'assigned_users',
|
||||||
'assigned_teams',
|
'assigned_teams',
|
||||||
'subscribed_teams',
|
'subscribed_teams',
|
||||||
|
@ -132,16 +132,22 @@
|
|||||||
</span>
|
</span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if ticket.project %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>Project</label>
|
<label>Project</label>
|
||||||
<span class="text">
|
<span class="text">
|
||||||
{% if ticket.project %}
|
|
||||||
<a href="{% url 'Project Management:_project_view' pk=ticket.project_id %}">{{ ticket.project }}</a>
|
<a href="{% url 'Project Management:_project_view' pk=ticket.project_id %}">{{ ticket.project }}</a>
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
</span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
|
{% if ticket.milestone %}
|
||||||
|
<fieldset>
|
||||||
|
<label>Milestone</label>
|
||||||
|
<span class="text">
|
||||||
|
<a href="{% url 'Project Management:_project_milestone_view' project_id=ticket.project_id pk=ticket.milestone.id %}">{{ ticket.milestone }}</a>
|
||||||
|
</span>
|
||||||
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>Priority</label>
|
<label>Priority</label>
|
||||||
<span class="text">U{{ ticket.get_urgency_display }} / I{{ ticket.get_impact_display }} / P{{ ticket.get_priority_display }}</span>
|
<span class="text">U{{ ticket.get_urgency_display }} / I{{ ticket.get_impact_display }} / P{{ ticket.get_priority_display }}</span>
|
||||||
|
@ -54,4 +54,38 @@ class TicketModel(
|
|||||||
as a subscribed user.
|
as a subscribed user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason='test to be written')
|
||||||
|
def test_field_milestone_no_project(self):
|
||||||
|
"""Field Value Test
|
||||||
|
|
||||||
|
Ensure that a milestone can't be applied if no project
|
||||||
|
has been selected
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason='test to be written')
|
||||||
|
def test_field_milestone_has_project(self):
|
||||||
|
"""Field Value Test
|
||||||
|
|
||||||
|
Ensure that a milestone can be applied if a project
|
||||||
|
has been selected
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason='test to be written')
|
||||||
|
def test_field_milestone_different_project(self):
|
||||||
|
"""Field Value Test
|
||||||
|
|
||||||
|
Ensure that a milestone from a different project
|
||||||
|
can't be applied
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
@ -97,6 +97,11 @@ class DetailForm(ProjectForm):
|
|||||||
"slug": "tasks",
|
"slug": "tasks",
|
||||||
"sections": []
|
"sections": []
|
||||||
},
|
},
|
||||||
|
"milestones": {
|
||||||
|
"name": "Milestones",
|
||||||
|
"slug": "milestones",
|
||||||
|
"sections": []
|
||||||
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"name": "Notes",
|
"name": "Notes",
|
||||||
"slug": "notes",
|
"slug": "notes",
|
||||||
|
109
app/project_management/forms/project_milestone.py
Normal file
109
app/project_management/forms/project_milestone.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from app import settings
|
||||||
|
|
||||||
|
from core.forms.common import CommonModelForm
|
||||||
|
from core.templatetags.markdown import to_duration
|
||||||
|
|
||||||
|
from project_management.models.project_milestone import ProjectMilestone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectMilestoneForm(CommonModelForm):
|
||||||
|
|
||||||
|
prefix = 'project'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = [
|
||||||
|
'id',
|
||||||
|
'organization',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'project',
|
||||||
|
'start_date',
|
||||||
|
'finish_date',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
model = ProjectMilestone
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['start_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local', 'format': "%Y-%m-%dT%H:%M"})
|
||||||
|
self.fields['start_date'].input_formats = settings.DATETIME_FORMAT
|
||||||
|
self.fields['start_date'].format="%Y-%m-%dT%H:%M"
|
||||||
|
|
||||||
|
self.fields['finish_date'].widget = forms.widgets.DateTimeInput(attrs={'type': 'datetime-local'})
|
||||||
|
self.fields['finish_date'].input_formats = settings.DATETIME_FORMAT
|
||||||
|
self.fields['finish_date'].format="%Y-%m-%dT%H:%M"
|
||||||
|
|
||||||
|
self.fields['description'].widget.attrs = {'style': "height: 800px; width: 1000px"}
|
||||||
|
|
||||||
|
self.fields['project'].widget = self.fields['project'].hidden_widget()
|
||||||
|
|
||||||
|
|
||||||
|
class DetailForm(ProjectMilestoneForm):
|
||||||
|
|
||||||
|
|
||||||
|
tabs: dict = {
|
||||||
|
"details": {
|
||||||
|
"name": "Details",
|
||||||
|
"slug": "details",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"layout": "double",
|
||||||
|
"left": [
|
||||||
|
'name',
|
||||||
|
'percent_completed',
|
||||||
|
'organization'
|
||||||
|
'c_created',
|
||||||
|
'c_modified',
|
||||||
|
],
|
||||||
|
"right": [
|
||||||
|
'start_date',
|
||||||
|
'finish_date',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"layout": "single",
|
||||||
|
"name": "Description",
|
||||||
|
"fields": [
|
||||||
|
'description',
|
||||||
|
],
|
||||||
|
"markdown": [
|
||||||
|
'description',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"name": "Tasks",
|
||||||
|
"slug": "tasks",
|
||||||
|
"sections": []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['c_created'] = forms.DateTimeField(
|
||||||
|
label = 'Created',
|
||||||
|
input_formats=settings.DATETIME_FORMAT,
|
||||||
|
disabled = True,
|
||||||
|
initial = self.instance.created,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields['c_modified'] = forms.DateTimeField(
|
||||||
|
label = 'Modified',
|
||||||
|
input_formats=settings.DATETIME_FORMAT,
|
||||||
|
disabled = True,
|
||||||
|
initial = self.instance.modified,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.url_index_view = reverse('Project Management:_project_view', kwargs={'pk': self.instance.project.id}) + '?tab=milestones'
|
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.8 on 2024-09-13 05:06
|
# Generated by Django 5.0.8 on 2024-09-14 06:29
|
||||||
|
|
||||||
import access.fields
|
import access.fields
|
||||||
import access.models
|
import access.models
|
||||||
@ -44,4 +44,25 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ['code', 'name'],
|
'ordering': ['code', 'name'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProjectMilestone',
|
||||||
|
fields=[
|
||||||
|
('is_global', models.BooleanField(default=False)),
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False, unique=True)),
|
||||||
|
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||||
|
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||||
|
('name', models.CharField(max_length=50, unique=True)),
|
||||||
|
('slug', access.fields.AutoSlugField()),
|
||||||
|
('description', models.TextField(blank=True, default=None, null=True)),
|
||||||
|
('start_date', models.DateTimeField(blank=True, help_text='When work commenced on the project.', null=True, verbose_name='Real Start Date')),
|
||||||
|
('finish_date', models.DateTimeField(blank=True, help_text='When work was completed for the project', null=True, verbose_name='Real Finish Date')),
|
||||||
|
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||||
|
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_management.project')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Project Milestone',
|
||||||
|
'verbose_name_plural': 'Project Milestones',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
74
app/project_management/models/project_milestone.py
Normal file
74
app/project_management/models/project_milestone.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .projects import Project, ProjectCommonFieldsName, SaveHistory
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectMilestone(ProjectCommonFieldsName):
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
ordering = [
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
verbose_name = 'Project Milestone'
|
||||||
|
|
||||||
|
verbose_name_plural = 'Project Milestones'
|
||||||
|
|
||||||
|
|
||||||
|
description = models.TextField(
|
||||||
|
blank = True,
|
||||||
|
default = None,
|
||||||
|
null= True,
|
||||||
|
)
|
||||||
|
|
||||||
|
start_date = models.DateTimeField(
|
||||||
|
blank = True,
|
||||||
|
help_text = 'When work commenced on the project.',
|
||||||
|
null = True,
|
||||||
|
verbose_name = 'Real Start Date',
|
||||||
|
)
|
||||||
|
|
||||||
|
finish_date = models.DateTimeField(
|
||||||
|
blank = True,
|
||||||
|
help_text = 'When work was completed for the project',
|
||||||
|
null = True,
|
||||||
|
verbose_name = 'Real Finish Date',
|
||||||
|
)
|
||||||
|
|
||||||
|
project = models.ForeignKey(
|
||||||
|
Project,
|
||||||
|
blank= False,
|
||||||
|
help_text = '',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null = False,
|
||||||
|
)
|
||||||
|
|
||||||
|
model_notes = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent_object(self):
|
||||||
|
""" Fetch the parent object """
|
||||||
|
|
||||||
|
return self.project
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percent_completed(self) -> str: # Auto-Calculate
|
||||||
|
""" How much of the milestone is completed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Calculated percentage of project completion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return 'xx %'
|
@ -17,6 +17,39 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="milestones" class="content-tab">
|
||||||
|
|
||||||
|
{% include 'content/section.html.j2' with tab=form.tabs.milestones %}
|
||||||
|
|
||||||
|
<input type="button" value="New Milestone" onclick="window.location='{% url 'Project Management:_project_milestone_add' project_id=project.id %}';">
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Completed</th>
|
||||||
|
<th>Started</th>
|
||||||
|
<th>Finished</th>
|
||||||
|
</thead>
|
||||||
|
{% if milestones %}
|
||||||
|
{% for milestone in milestones %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'Project Management:_project_milestone_view' project_id=project.id pk=milestone.id %}">
|
||||||
|
{{ milestone.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ milestone.percent_completed }}</td>
|
||||||
|
<td>{{ milestone.start_date }}</td>
|
||||||
|
<td>{{ milestone.finish_date }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="4">Nothing Found</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="tasks" class="content-tab">
|
<div id="tasks" class="content-tab">
|
||||||
|
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
{% extends 'detail.html.j2' %}
|
||||||
|
|
||||||
|
{% block additional-stylesheet %}
|
||||||
|
{% load static %}
|
||||||
|
<link rel="stylesheet" href="{% static 'ticketing.css' %}">
|
||||||
|
{% endblock additional-stylesheet %}
|
||||||
|
|
||||||
|
{% load json %}
|
||||||
|
{% load markdown %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block tabs %}
|
||||||
|
|
||||||
|
<div id="details" class="content-tab">
|
||||||
|
|
||||||
|
{% include 'content/section.html.j2' with tab=form.tabs.details %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tasks" class="content-tab">
|
||||||
|
|
||||||
|
{% include 'content/section.html.j2' with tab=form.tabs.tasks %}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Created</th>
|
||||||
|
</tr>
|
||||||
|
{% for task in tasks %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ task.id }}</td>
|
||||||
|
<td>
|
||||||
|
{% if task.get_ticket_type_display|lower == 'change' %}
|
||||||
|
<a href="{% url 'ITIM:_ticket_change_view' ticket_type='change' pk=task.id %}">
|
||||||
|
{% elif task.get_ticket_type_display|lower == 'incident' %}
|
||||||
|
<a href="{% url 'ITIM:_ticket_incident_view' ticket_type='incident' pk=task.id %}">
|
||||||
|
{% elif task.get_ticket_type_display|lower == 'problem' %}
|
||||||
|
<a href="{% url 'ITIM:_ticket_problem_view' ticket_type='problem' pk=task.id %}">
|
||||||
|
{% elif task.get_ticket_type_display|lower == 'request' %}
|
||||||
|
<a href="{% url 'Assistance:_ticket_request_view' ticket_type='request' pk=task.id %}">
|
||||||
|
{% elif task.get_ticket_type_display|lower == 'project task' %}
|
||||||
|
<a href="{% url 'Project Management:_project_task_view' ticket_type='project_task' project_id=project.id pk=task.id %}">
|
||||||
|
{% else %}
|
||||||
|
<a href=""></a>
|
||||||
|
{% endif %}
|
||||||
|
{{ task.title }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% include 'core/ticket/badge_ticket_status.html.j2' with ticket_status_text=task.get_status_display ticket_status=task.get_status_display|ticket_status %}
|
||||||
|
</td>
|
||||||
|
<td>{{ task.get_ticket_type_display }}</td>
|
||||||
|
<td>{{ task.created }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="notes" class="content-tab">
|
||||||
|
|
||||||
|
{% include 'content/section.html.j2' with tab=form.tabs.notes %}
|
||||||
|
|
||||||
|
{{ notes_form }}
|
||||||
|
<input type="submit" name="{{notes_form.prefix}}" value="Submit" />
|
||||||
|
<div class="comments">
|
||||||
|
{% if notes %}
|
||||||
|
{% for note in notes%}
|
||||||
|
{% include 'note.html.j2' %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -1,6 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import project
|
from .views import project, project_milestones
|
||||||
|
|
||||||
from core.views import ticket, ticket_comment
|
from core.views import ticket, ticket_comment
|
||||||
|
|
||||||
@ -14,6 +14,11 @@ urlpatterns = [
|
|||||||
path("project/<int:pk>/edit", project.Change.as_view(), name="_project_change"),
|
path("project/<int:pk>/edit", project.Change.as_view(), name="_project_change"),
|
||||||
path("project/<int:pk>/delete", project.Delete.as_view(), name="_project_delete"),
|
path("project/<int:pk>/delete", project.Delete.as_view(), name="_project_delete"),
|
||||||
|
|
||||||
|
path('project/<int:project_id>/milestone/add', project_milestones.Add.as_view(), name="_project_milestone_add"),
|
||||||
|
path('project/<int:project_id>/milestone/<int:pk>/edit', project_milestones.Change.as_view(), name="_project_milestone_change"),
|
||||||
|
path('project/<int:project_id>/milestone/<int:pk>/delete', project_milestones.Delete.as_view(), name="_project_milestone_delete"),
|
||||||
|
path('project/<int:project_id>/milestone/<int:pk>', project_milestones.View.as_view(), name="_project_milestone_view"),
|
||||||
|
|
||||||
path('project/<int:project_id>/<str:ticket_type>/add', ticket.Add.as_view(), name="_project_task_add"),
|
path('project/<int:project_id>/<str:ticket_type>/add', ticket.Add.as_view(), name="_project_task_add"),
|
||||||
path('project/<int:project_id>/<str:ticket_type>/<int:pk>/edit', ticket.Change.as_view(), name="_project_task_change"),
|
path('project/<int:project_id>/<str:ticket_type>/<int:pk>/edit', ticket.Change.as_view(), name="_project_task_change"),
|
||||||
path('project/<int:project_id>/<str:ticket_type>/<int:pk>/delete', ticket.Delete.as_view(), name="_project_task_delete"),
|
path('project/<int:project_id>/<str:ticket_type>/<int:pk>/delete', ticket.Delete.as_view(), name="_project_task_delete"),
|
||||||
|
@ -13,8 +13,8 @@ from core.models.notes import Notes
|
|||||||
from core.models.ticket.ticket import Ticket
|
from core.models.ticket.ticket import Ticket
|
||||||
from core.views.common import AddView, ChangeView, DeleteView, DisplayView, IndexView
|
from core.views.common import AddView, ChangeView, DeleteView, DisplayView, IndexView
|
||||||
|
|
||||||
from project_management.forms.project import ProjectForm, DetailForm
|
from project_management.forms.project import Project, ProjectForm, DetailForm
|
||||||
from project_management.models.projects import Project
|
from project_management.models.project_milestone import ProjectMilestone
|
||||||
|
|
||||||
from settings.models.user_settings import UserSettings
|
from settings.models.user_settings import UserSettings
|
||||||
|
|
||||||
@ -170,6 +170,8 @@ class View(ChangeView):
|
|||||||
# context['notes_form'] = AddNoteForm(prefix='note')
|
# context['notes_form'] = AddNoteForm(prefix='note')
|
||||||
# context['notes'] = Notes.objects.filter(service=self.kwargs['pk'])
|
# context['notes'] = Notes.objects.filter(service=self.kwargs['pk'])
|
||||||
|
|
||||||
|
context['milestones'] = ProjectMilestone.objects.filter(project__id=self.kwargs['pk'])
|
||||||
|
|
||||||
context['project_tasks'] = Ticket.objects.filter(
|
context['project_tasks'] = Ticket.objects.filter(
|
||||||
project = self.object,
|
project = self.object,
|
||||||
)
|
)
|
||||||
|
182
app/project_management/views/project_milestones.py
Normal file
182
app/project_management/views/project_milestones.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.views import generic
|
||||||
|
|
||||||
|
from access.mixin import OrganizationPermission
|
||||||
|
|
||||||
|
from core.forms.comment import AddNoteForm
|
||||||
|
from core.models.notes import Notes
|
||||||
|
from core.models.ticket.ticket import Ticket
|
||||||
|
from core.views.common import AddView, ChangeView, DeleteView, DisplayView, IndexView
|
||||||
|
|
||||||
|
from project_management.forms.project_milestone import DetailForm, ProjectMilestone, ProjectMilestoneForm
|
||||||
|
|
||||||
|
from settings.models.user_settings import UserSettings
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Add(AddView):
|
||||||
|
|
||||||
|
form_class = ProjectMilestoneForm
|
||||||
|
|
||||||
|
model = ProjectMilestone
|
||||||
|
|
||||||
|
permission_required = [
|
||||||
|
'project_management.add_projectmilestone',
|
||||||
|
]
|
||||||
|
|
||||||
|
template_name = 'form.html.j2'
|
||||||
|
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
|
||||||
|
initial.update({
|
||||||
|
'project': self.kwargs['project_id']
|
||||||
|
})
|
||||||
|
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.is_global = False
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
def get_success_url(self, **kwargs):
|
||||||
|
|
||||||
|
return reverse('Project Management:_project_view', kwargs={'pk': self.kwargs['project_id']}) + '?tab=milestones'
|
||||||
|
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context['content_title'] = 'Create a Project Milestone'
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Change(ChangeView):
|
||||||
|
|
||||||
|
form_class = ProjectMilestoneForm
|
||||||
|
|
||||||
|
model = ProjectMilestone
|
||||||
|
|
||||||
|
permission_required = [
|
||||||
|
'project_management.change_projectmilestone',
|
||||||
|
]
|
||||||
|
|
||||||
|
template_name = 'form.html.j2'
|
||||||
|
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.is_global = False
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
def get_success_url(self, **kwargs):
|
||||||
|
|
||||||
|
return reverse('Project Management:_project_view', kwargs={'pk': self.kwargs['pk']})
|
||||||
|
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context['content_title'] = 'Edit'
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Delete(DeleteView):
|
||||||
|
|
||||||
|
model = ProjectMilestone
|
||||||
|
|
||||||
|
permission_required = [
|
||||||
|
'project_management.delete_projectmilestone',
|
||||||
|
]
|
||||||
|
|
||||||
|
template_name = 'form.html.j2'
|
||||||
|
|
||||||
|
|
||||||
|
def get_success_url(self, **kwargs):
|
||||||
|
|
||||||
|
return reverse('Project Management:_project_view', kwargs={'pk': self.kwargs['project_id']}) + '?tab=milestones'
|
||||||
|
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context['content_title'] = 'Delete ' + self.object.name
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class View(ChangeView):
|
||||||
|
|
||||||
|
model = ProjectMilestone
|
||||||
|
|
||||||
|
permission_required = [
|
||||||
|
'project_management.view_projectmilestone'
|
||||||
|
]
|
||||||
|
|
||||||
|
template_name = 'project_management/project_milestone.html.j2'
|
||||||
|
|
||||||
|
form_class = DetailForm
|
||||||
|
|
||||||
|
context_object_name = "project"
|
||||||
|
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
initial = super().get_initial()
|
||||||
|
|
||||||
|
initial.update({
|
||||||
|
'project': self.kwargs['project_id']
|
||||||
|
})
|
||||||
|
|
||||||
|
return initial
|
||||||
|
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context['tasks'] = Ticket.objects.filter(
|
||||||
|
project = self.object.project,
|
||||||
|
milestone = self.kwargs['pk'],
|
||||||
|
)
|
||||||
|
|
||||||
|
context['model_docs_path'] = self.model._meta.app_label + '/' + self.model._meta.model_name + '/'
|
||||||
|
|
||||||
|
context['model_pk'] = self.kwargs['pk']
|
||||||
|
context['model_name'] = self.model._meta.verbose_name.replace(' ', '')
|
||||||
|
|
||||||
|
context['model_delete_url'] = reverse('Project Management:_project_milestone_delete', kwargs={'project_id': self.kwargs['project_id'], 'pk': self.kwargs['pk']})
|
||||||
|
|
||||||
|
context['content_title'] = context['project'].name
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
# def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
# project = self.model.objects.get(pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
# notes = AddNoteForm(request.POST, prefix='note')
|
||||||
|
|
||||||
|
# if notes.is_bound and notes.is_valid() and notes.instance.note != '':
|
||||||
|
|
||||||
|
# if request.user.has_perm('core.add_notes'):
|
||||||
|
|
||||||
|
# notes.instance.organization = device.organization
|
||||||
|
# notes.instance.project = project
|
||||||
|
# notes.instance.usercreated = request.user
|
||||||
|
|
||||||
|
# notes.save()
|
||||||
|
|
||||||
|
# return super().post(request, *args, **kwargs)
|
Reference in New Issue
Block a user