diff --git a/osinaweb/db.sqlite3 b/osinaweb/db.sqlite3 index 158585bf..61dd0268 100644 Binary files a/osinaweb/db.sqlite3 and b/osinaweb/db.sqlite3 differ diff --git a/osinaweb/osinacore/__pycache__/models.cpython-311.pyc b/osinaweb/osinacore/__pycache__/models.cpython-311.pyc index a2fd1662..c87b53ee 100644 Binary files a/osinaweb/osinacore/__pycache__/models.cpython-311.pyc and b/osinaweb/osinacore/__pycache__/models.cpython-311.pyc differ diff --git a/osinaweb/osinacore/__pycache__/views.cpython-311.pyc b/osinaweb/osinacore/__pycache__/views.cpython-311.pyc index dffd31f2..3f1a7d37 100644 Binary files a/osinaweb/osinacore/__pycache__/views.cpython-311.pyc and b/osinaweb/osinacore/__pycache__/views.cpython-311.pyc differ diff --git a/osinaweb/osinacore/migrations/0047_task_status_date.py b/osinaweb/osinacore/migrations/0047_task_status_date.py new file mode 100644 index 00000000..5304d7ef --- /dev/null +++ b/osinaweb/osinacore/migrations/0047_task_status_date.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.5 on 2024-01-15 10:49 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('osinacore', '0046_task_requirement'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='status_date', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/osinaweb/osinacore/migrations/__pycache__/0047_task_status_date.cpython-311.pyc b/osinaweb/osinacore/migrations/__pycache__/0047_task_status_date.cpython-311.pyc new file mode 100644 index 00000000..f8853b99 Binary files /dev/null and b/osinaweb/osinacore/migrations/__pycache__/0047_task_status_date.cpython-311.pyc differ diff --git a/osinaweb/osinacore/models.py b/osinaweb/osinacore/models.py index 1fa3b29e..83cf6719 100644 --- a/osinaweb/osinacore/models.py +++ b/osinaweb/osinacore/models.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User from colorfield.fields import ColorField from datetime import datetime from django.db.models import Max +from django.utils import timezone # Create your models here. @@ -205,6 +206,7 @@ class Task(models.Model): ('Closed', 'Closed') ) status = models.CharField(max_length=200, choices=STATUS_CHOICES, null=True) + status_date = models.DateTimeField(default=timezone.now) extra = models.BooleanField(default=False) description = models.TextField() start_date = models.CharField(max_length=200) @@ -219,6 +221,12 @@ class Task(models.Model): max_id = Task.objects.aggregate(models.Max('task_id'))['task_id__max'] new_id = str(int(max_id[-4:]) + 1).zfill(4) if max_id else '0001' # If no existing records, start with '0001' self.task_id = 'T' + current_year + new_id # Add 'p' prefix + + if self.pk is not None: + original_task = Task.objects.get(pk=self.pk) + if original_task.status != self.status: + self.status_date = timezone.now() + super(Task, self).save(*args, **kwargs) def formatted_start_date(self): diff --git a/osinaweb/osinacore/views.py b/osinaweb/osinacore/views.py index ae2ca033..7843a778 100644 --- a/osinaweb/osinacore/views.py +++ b/osinaweb/osinacore/views.py @@ -6,7 +6,7 @@ from django.contrib import messages from .forms import * from django.utils import timezone from django.urls import reverse -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseServerError, Http404 from django.db.models import Q from django.http import JsonResponse from .models import Task, Epic @@ -54,12 +54,11 @@ def home(request, *args, **kwargs): if request.user.is_superuser: # Superadmin can see the last 8 tasks for all users - tasks = Task.objects.filter(Q(status='Open') | Q(status='Working On')).order_by('-id')[:8] + tasks = Task.objects.filter(Q(status='Open') | Q(status='Working On')).order_by('-status_date', '-id')[:8] else: # Non-superadmin user can only see their assigned tasks - tasks = Task.objects.filter(Q(assigned_to=request.user.staffprofile) & (Q(status='Open') | Q(status='Working On'))).order_by('-id') - + tasks = Task.objects.filter(Q(assigned_to=request.user.staffprofile) & (Q(status='Open') | Q(status='Working On'))).order_by('-status_date', '-id') context = { 'notes': notes, @@ -98,10 +97,10 @@ def my_projects(request, *args, **kwargs): def my_tasks(request, *args, **kwargs): if request.user.is_superuser: # Superadmin can see all projects - my_tasks = Task.objects.all().order_by('-id') + my_tasks = Task.objects.all().order_by('-status_date', '-id') else: # Non-superuser, filter projects where the user is either the manager or a member - my_tasks = Task.objects.all().filter(assigned_to=request.user.staffprofile).order_by('-id') + my_tasks = Task.objects.all().filter(Q(assigned_to=request.user.staffprofile)).order_by('-status_date', '-id') context = { 'my_tasks' : my_tasks @@ -222,8 +221,8 @@ def detailed_project(request, project_id): epics = Epic.objects.filter(project=project) latest_epic = Epic.objects.filter(project=project).last() - # if latest_epic: - # epics = epics.exclude(pk=latest_epic.pk) + if latest_epic: + epics = epics.exclude(pk=latest_epic.pk) selected_epic_id = request.GET.get('epic_id') @@ -536,7 +535,7 @@ def status_mobile_modal (request, *args, **kwargs): -#Fetch EPIC RELATED TASKS +#FETCH EPIC RELATED TASKS def get_tasks (request, epic_id): epic = get_object_or_404(Epic, id=epic_id) related_tasks = Task.objects.filter(epic_id=epic).order_by('-id') @@ -549,6 +548,32 @@ def get_tasks (request, epic_id): return render(request, 'epic-fetched-tasks.html', context) +# TO FETCH THE LATEST EPIC AND ITS TASKS +def get_latest_epic(request, project_id): + try: + project = get_object_or_404(Project, id=project_id) + latest_epic = Epic.objects.filter(project=project).order_by('-id').first() + + if latest_epic: + related_tasks = Task.objects.filter(epic=latest_epic) + else: + related_tasks = [] + + context = { + 'latest_epic': latest_epic, + 'related_tasks': related_tasks, + } + + return render(request, 'epic-fetched-tasks.html', context) + except Http404: + # Handle case where the specified project does not exist + return HttpResponseServerError("Project not found") + except Exception as e: + # Log the error or return a specific error response + print(f"An error occurred: {str(e)}") + return HttpResponseServerError("Internal Server Error") + + diff --git a/osinaweb/osinaweb/__pycache__/urls.cpython-311.pyc b/osinaweb/osinaweb/__pycache__/urls.cpython-311.pyc index 1fad44b9..7b0c1c5c 100644 Binary files a/osinaweb/osinaweb/__pycache__/urls.cpython-311.pyc and b/osinaweb/osinaweb/__pycache__/urls.cpython-311.pyc differ diff --git a/osinaweb/osinaweb/urls.py b/osinaweb/osinaweb/urls.py index 7f53c7fb..386d3cea 100644 --- a/osinaweb/osinaweb/urls.py +++ b/osinaweb/osinaweb/urls.py @@ -48,6 +48,7 @@ urlpatterns = [ path('tags/', views.tags, name='tags'), path('dailyreports/', views.daily_reports, name='dailyreports'), path('add-dailyreport/', views.add_daily_report, name='adddailyreport'), + @@ -63,6 +64,7 @@ urlpatterns = [ #Fetch urls path('get_tasks//', views.get_tasks, name='get_tasks'), + path('get_latest_epic//', views.get_latest_epic, name='get_latest_epic'), #Modals urls diff --git a/osinaweb/static/dist/output.css b/osinaweb/static/dist/output.css index 16037a79..7bc9615d 100644 --- a/osinaweb/static/dist/output.css +++ b/osinaweb/static/dist/output.css @@ -610,6 +610,12 @@ video { } } +@media (min-width: 1600px) { + .container { + max-width: 1600px; + } +} + @media (min-width: 1750px) { .container { max-width: 1750px; @@ -981,10 +987,6 @@ video { width: 25%; } -.w-\[250px\] { - width: 250px; -} - .w-\[25px\] { width: 25px; } @@ -1025,6 +1027,10 @@ video { width: 45px; } +.w-\[50\%\] { + width: 50%; +} + .w-\[50px\] { width: 50px; } @@ -1262,6 +1268,10 @@ video { overflow-y: auto; } +.overflow-x-scroll { + overflow-x: scroll; +} + .truncate { overflow: hidden; text-overflow: ellipsis; @@ -1284,6 +1294,10 @@ video { border-radius: 0.375rem; } +.rounded-none { + border-radius: 0px; +} + .rounded-b-md { border-bottom-right-radius: 0.375rem; border-bottom-left-radius: 0.375rem; @@ -2351,6 +2365,14 @@ video { height: 150px; } + .md\:h-\[70px\] { + height: 70px; + } + + .md\:w-\[160px\] { + width: 160px; + } + .md\:w-\[300px\] { width: 300px; } @@ -2359,6 +2381,38 @@ video { width: -moz-fit-content; width: fit-content; } + + .md\:flex-row { + flex-direction: row; + } + + .md\:py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + } +} + +@media (min-width: 960px) { + .l\:w-\[160px\] { + width: 160px; + } + + .l\:w-\[300px\] { + width: 300px; + } + + .l\:w-\[50\%\] { + width: 50%; + } + + .l\:w-fit { + width: -moz-fit-content; + width: fit-content; + } + + .l\:flex-row { + flex-direction: row; + } } @media (min-width: 1110px) { @@ -2373,10 +2427,6 @@ video { .lg\:hidden { display: none; } - - .lg\:w-\[75\%\] { - width: 75%; - } } @media (min-width: 1200px) { @@ -2384,9 +2434,25 @@ video { position: fixed; } + .xlg1\:block { + display: block; + } + + .xlg1\:hidden { + display: none; + } + .xlg1\:w-\[300px\] { width: 300px; } + + .xlg1\:w-\[74\.5\%\] { + width: 74.5%; + } + + .xlg1\:w-\[75\%\] { + width: 75%; + } } @media (min-width: 1300px) { @@ -2415,4 +2481,37 @@ video { .xxlg1\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} + +@media (min-width: 1536px) { + .xl\:w-\[250px\] { + width: 250px; + } + + .xl\:w-fit { + width: -moz-fit-content; + width: fit-content; + } + + .xl\:flex-row { + flex-direction: row; + } +} + +@media (min-width: 1600px) { + .xll\:mt-1 { + margin-top: 0.25rem; + } + + .xll\:flex { + display: flex; + } + + .xll\:hidden { + display: none; + } + + .xll\:rounded-none { + border-radius: 0px; + } } \ No newline at end of file diff --git a/osinaweb/static/js/epics.js b/osinaweb/static/js/epics.js index 03649492..6deb23a5 100644 --- a/osinaweb/static/js/epics.js +++ b/osinaweb/static/js/epics.js @@ -1,41 +1,14 @@ const epicSelect = document.getElementById('epicSelect'); const epicDetails = document.getElementById('epicDetails'); -const epicRelatedTasks = document.getElementById('epicRelatedTasks'); -const editEpicButton = document.getElementById('editEpicButton'); -const createTaskButton = document.getElementById('createTaskButton'); -const createStoryButton = document.getElementById('createStoryButton'); +const latestEpicDetails = document.getElementById('latestEpicDetails'); epicSelect.addEventListener('change', function () { if (this.value !== 'EPICS') { epicDetails.classList.remove('hidden'); epicDetails.classList.add('flex'); - - epicRelatedTasks.classList.remove('hidden'); - - editEpicButton.classList.remove('cursor-not-allowed', 'opacity-30'); - editEpicButton.classList.add('cursor-pointer'); - editEpicButton.removeAttribute('disabled'); - - createTaskButton.classList.remove('cursor-not-allowed', 'opacity-30'); - createTaskButton.classList.add('cursor-pointer'); - createTaskButton.removeAttribute('disabled'); - - createStoryButton.classList.remove('cursor-not-allowed', 'opacity-30'); - createStoryButton.classList.add('cursor-pointer'); - createStoryButton.removeAttribute('disabled'); - + + latestEpicDetails.classList.add('hidden'); } else { epicDetails.classList.add('hidden'); - - epicRelatedTasks.classList.add('hidden'); - - editEpicButton.classList.add('cursor-not-allowed', 'opacity-30'); - editEpicButton.setAttribute('disabled', true); - - createTaskButton.classList.add('cursor-not-allowed', 'opacity-30'); - createTaskButton.setAttribute('disabled', true); - - createStoryButton.classList.add('cursor-not-allowed', 'opacity-30'); - createStoryButton.setAttribute('disabled', true); } }); \ No newline at end of file diff --git a/osinaweb/static/js/fetch-epic-tasks.js b/osinaweb/static/js/fetch-epic-tasks.js index 26b4514b..2cdb007c 100644 --- a/osinaweb/static/js/fetch-epic-tasks.js +++ b/osinaweb/static/js/fetch-epic-tasks.js @@ -30,6 +30,39 @@ $(document).ready(function () { } }); + + // TO FETCH THE LATEST EPIC BY DEFAULT + var projectId = $("#projectId").text().trim(); + + function fetchLatestEpicTasks(projectId) { + $.ajax({ + type: "GET", + url: "/get_latest_epic/" + projectId + "/", + success: function (data) { + var latestEpicId = data.latest_epic ? data.latest_epic.id : null; + + if (latestEpicId) { + // Fetch related tasks based on the latest epic + fetchRelatedTasks(latestEpicId); + } else { + // console.log("No latest epic found."); + } + + $("#epicRelatedTasksContainer").html(data); + }, + error: function (xhr, status, error) { + console.log("Ajax call failed. Error details:"); + console.log("XHR Object:", xhr); + console.log("Status:", status); + console.log("Error:", error); + } + }); + } + + fetchLatestEpicTasks(projectId); + + + $('#epicSelect').change(function () { var selectedEpic = $(this).find(':selected'); var startDate = selectedEpic.data('start-date'); diff --git a/osinaweb/static/js/tasks.js b/osinaweb/static/js/tasks.js new file mode 100644 index 00000000..979c0454 --- /dev/null +++ b/osinaweb/static/js/tasks.js @@ -0,0 +1,18 @@ +// TO OPEN ACTION BUTTONS CONTAINER IN TASKS CONTAINER ON MOBILE +document.addEventListener('DOMContentLoaded', function () { + var actionsButtons = document.querySelectorAll('.actionsButton'); + + actionsButtons.forEach(function (button) { + button.addEventListener('click', function () { + var arrowDown = button.querySelector('.fa-angle-down'); + var arrowUp = button.querySelector('.fa-angle-up'); + + arrowDown.style.display = arrowDown.style.display === 'none' ? 'inline-block' : 'none'; + arrowUp.style.display = arrowUp.style.display === 'none' ? 'inline-block' : 'none'; + + var actionsContainer = button.nextElementSibling; + actionsContainer.classList.toggle('hidden'); + actionsContainer.classList.toggle('grid'); + }); + }); +}); \ No newline at end of file diff --git a/osinaweb/tailwind.config.js b/osinaweb/tailwind.config.js index 23019b3e..c3d89c1f 100644 --- a/osinaweb/tailwind.config.js +++ b/osinaweb/tailwind.config.js @@ -20,6 +20,7 @@ module.exports = { xxlg1: '1350px', xxlg: '1390px', xl: '1536px', + xll: '1600px', xxl: '1750px', k: '2400px', }, diff --git a/osinaweb/templates/details_pages/project-details.html b/osinaweb/templates/details_pages/project-details.html index 7a6631bd..3eb044cb 100644 --- a/osinaweb/templates/details_pages/project-details.html +++ b/osinaweb/templates/details_pages/project-details.html @@ -2,70 +2,85 @@ {%load static%} {% block content %} - + +
+ Users +
+ -
-
-
-
+
+
+
+

Recent Note:

-

Send an Email to Salim.

+
+
+ +
+

{{recent_note.text}}

+
-
+
-