diff --git a/TimeDispatcher/settings.py b/TimeDispatcher/settings.py index b415349..998d997 100644 --- a/TimeDispatcher/settings.py +++ b/TimeDispatcher/settings.py @@ -64,6 +64,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'context_processors.navbar_links' ], }, }, diff --git a/TimeDispatcher/urls.py b/TimeDispatcher/urls.py index 08c09f2..98280b1 100644 --- a/TimeDispatcher/urls.py +++ b/TimeDispatcher/urls.py @@ -20,5 +20,9 @@ from django.urls import path from dispatcher import views urlpatterns = [ - path("", views.dashboard_view, name="dashboard") + path("", views.dashboard_view, name="dashboard"), + path("import/", views.import_view, name="import"), + path("projects/", views.projects_view, name="projects"), + path("parents/", views.ParentsView.as_view(), name="parents"), + path("projects//", views.project_view, name="project"), ] diff --git a/context_processors.py b/context_processors.py new file mode 100644 index 0000000..c0f796a --- /dev/null +++ b/context_processors.py @@ -0,0 +1,9 @@ +def navbar_links(request): + return { + "nav_links": [ + {'view': 'dashboard', 'label': 'Dashboard'}, + {'view': 'projects', 'label': 'Projects'}, + {'view': 'parents', 'label': 'Parents'}, + {'view': 'import', 'label': 'Import'}, + ] + } \ No newline at end of file diff --git a/dispatcher/core.py b/dispatcher/core.py new file mode 100644 index 0000000..fa4a2f5 --- /dev/null +++ b/dispatcher/core.py @@ -0,0 +1,36 @@ +from dispatcher.models import Task, Project + + +def import_tasks(csv_content: str): + tasks = [] + for line in csv_content.splitlines()[1:]: + if len(line.strip()) == 0: + continue + + date, duration, project_name, name = line.split(";") + if duration == "-": + duration = 0 + else: + hours, mins = duration.split(":") + duration = int(hours) * 60 + int(mins) + + project, created = Project.objects.get_or_create(name=project_name) + if created: + print(f"Created new project {project}") + + + tasks.append(Task( + date=date, + duration=duration, + project=project, + name=name + )) + + Task.objects.bulk_create( + tasks, + update_conflicts=True, + unique_fields=["date", "project", "name"], + update_fields=["duration"] + ) + print(f"Imported {len(tasks)} tasks") + diff --git a/dispatcher/forms.py b/dispatcher/forms.py new file mode 100644 index 0000000..894c9aa --- /dev/null +++ b/dispatcher/forms.py @@ -0,0 +1,12 @@ +from django import forms + +from dispatcher.models import Project + + +class ProjectForm(forms.ModelForm): + class Meta: + model = Project + fields = "__all__" + +class ImportForm(forms.Form): + file = forms.FileField() \ No newline at end of file diff --git a/dispatcher/migrations/0002_alter_project_parent.py b/dispatcher/migrations/0002_alter_project_parent.py new file mode 100644 index 0000000..74c4b76 --- /dev/null +++ b/dispatcher/migrations/0002_alter_project_parent.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.5 on 2025-01-25 00:15 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dispatcher', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='parent', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='dispatcher.parent'), + ), + ] diff --git a/dispatcher/migrations/0003_task_name.py b/dispatcher/migrations/0003_task_name.py new file mode 100644 index 0000000..55bc5b4 --- /dev/null +++ b/dispatcher/migrations/0003_task_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.5 on 2025-01-25 12:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dispatcher', '0002_alter_project_parent'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='name', + field=models.CharField(default='', max_length=512), + ), + ] diff --git a/dispatcher/migrations/0004_task_unique_daily_task.py b/dispatcher/migrations/0004_task_unique_daily_task.py new file mode 100644 index 0000000..9844e01 --- /dev/null +++ b/dispatcher/migrations/0004_task_unique_daily_task.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.5 on 2025-01-25 21:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dispatcher', '0003_task_name'), + ] + + operations = [ + migrations.AddConstraint( + model_name='task', + constraint=models.UniqueConstraint(fields=('date', 'project', 'name'), name='unique_daily_task'), + ), + ] diff --git a/dispatcher/models.py b/dispatcher/models.py index 95d497b..79fcd02 100644 --- a/dispatcher/models.py +++ b/dispatcher/models.py @@ -4,13 +4,29 @@ class Parent(models.Model): project_num = models.CharField(max_length=32) name = models.CharField(max_length=256) + def __str__(self): + return self.name + class Project(models.Model): - parent = models.ForeignKey(Parent, on_delete=models.CASCADE) + parent = models.ForeignKey( + Parent, + on_delete=models.CASCADE, + null=True + ) name = models.CharField(max_length=256) + def __str__(self): + return self.name + class Task(models.Model): project = models.ForeignKey(Project, on_delete=models.CASCADE) date = models.DateField(null=False) duration = models.IntegerField(null=False, default=0) + name = models.CharField(max_length=512, default="") + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["date", "project", "name"], name="unique_daily_task") + ] diff --git a/dispatcher/static/base.css b/dispatcher/static/base.css new file mode 100644 index 0000000..5ec9123 --- /dev/null +++ b/dispatcher/static/base.css @@ -0,0 +1,82 @@ +:root { + --dark1: #232323; + --dark2: #2f2f2f; + --dark3: #444444; + --dark4: #656565; + --light1: #ffffff; + --light2: #e5e5e5; + --light3: #cccccc; + --light4: #c5c5c5; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: var(--dark1); + color: var(--light1); + height: 100vh; + display: flex; + flex-direction: column; + font-family: "Segoe UI", system-ui, sans-serif; + font-size: 12pt; +} + +header { + background-color: var(--dark2); + font-size: 120%; +} + +nav { + display: flex; +} + +nav a { + color: var(--light1); + text-decoration: none; + display: grid; + place-items: center; + padding: 1.2em; + transition: background-color 0.2s; +} + +nav a.active { + box-shadow: 0 -2px var(--light1) inset; +} + +nav a:hover { + background-color: var(--dark3); +} + +footer { + background-color: var(--dark2); + min-height: 3em; +} + +main { + flex-grow: 1; + display: flex; + flex-direction: column; + padding: 2em; +} + +button, input, select { + font-family: inherit; + font-size: inherit; + padding: 0.2em 0.4em; + background-color: var(--light1); + color: var(--dark1); + border: solid var(--dark3) 1px; + border-radius: 4px; +} + +button:hover, select:hover { + background-color: var(--light2); +} + +button, select { + cursor: pointer; +} \ No newline at end of file diff --git a/dispatcher/static/base.js b/dispatcher/static/base.js new file mode 100644 index 0000000..e69de29 diff --git a/dispatcher/static/dashboard.css b/dispatcher/static/dashboard.css new file mode 100644 index 0000000..e69de29 diff --git a/dispatcher/static/list.css b/dispatcher/static/list.css new file mode 100644 index 0000000..5216988 --- /dev/null +++ b/dispatcher/static/list.css @@ -0,0 +1,20 @@ +.list { + display: flex; + width: 100%; + max-width: 40em; + align-self: center; + gap: 0.8em; + flex-direction: column; +} + +.list li { + display: flex; + padding: 0.8em 1.2em; + marker: none; + gap: 0.8em; + border: solid var(--dark4) 1px; +} + +.list li .actions { + margin-left: auto; +} \ No newline at end of file diff --git a/dispatcher/static/projects.css b/dispatcher/static/projects.css new file mode 100644 index 0000000..6b8217a --- /dev/null +++ b/dispatcher/static/projects.css @@ -0,0 +1,24 @@ + +.projects { + width: 100%; + max-width: 40em; + border-collapse: collapse; + align-self: center; +} + +.projects thead { + border-bottom: solid var(--light3) 2px; +} + +.projects thead th:not(:first-child) { + border-left: solid var(--light3) 2px; +} + +.projects th, .projects td { + text-align: left; + padding: 0.4em 0.8em; +} + +.projects tbody tr:nth-child(even) { + background-color: var(--dark2); +} \ No newline at end of file diff --git a/dispatcher/static/projects.js b/dispatcher/static/projects.js new file mode 100644 index 0000000..7763704 --- /dev/null +++ b/dispatcher/static/projects.js @@ -0,0 +1,8 @@ +window.addEventListener("load", () => { + document.querySelectorAll(".projects tbody tr").forEach(row => { + let id = row.dataset.id + row.querySelector("button.see").addEventListener("click", () => { + window.location.href = `${id}/` + }) + }) +}) \ No newline at end of file diff --git a/dispatcher/views.py b/dispatcher/views.py index f297325..f2cfcfa 100644 --- a/dispatcher/views.py +++ b/dispatcher/views.py @@ -1,5 +1,46 @@ -from django.shortcuts import render +from django.core.files.uploadedfile import UploadedFile +from django.shortcuts import render, get_object_or_404, redirect +from django.views import generic + +from dispatcher.core import import_tasks +from dispatcher.forms import ProjectForm, ImportForm +from dispatcher.models import Project, Parent def dashboard_view(request): - return render(request, "dashboard.html") \ No newline at end of file + context = { + "projects": Project.objects.all(), + "parents": Parent.objects.all() + } + return render(request, "dashboard.html", context) + +def projects_view(request): + context = { + "projects": Project.objects.all(), + "parents": Parent.objects.all() + } + return render(request, "projects.html", context) + +def project_view(request, id): + project = get_object_or_404(Project, id=id) + context = {} + form = ProjectForm(request.POST or None, request.FILES or None, instance=project) + if form.is_valid(): + form.save() + context["form"] = ProjectForm(instance=project) + return render(request, "project.html", context) + +def import_view(request): + if request.method == "POST": + form = ImportForm(request.POST, request.FILES) + if form.is_valid(): + print(request.FILES) + import_tasks(request.FILES["file"].read().decode("utf-8")) + return redirect("dashboard") + + return render(request, "import.html") + +class ParentsView(generic.ListView): + model = Parent + template_name = "parents.html" + context_object_name = "elements" diff --git a/templates/base.html b/templates/base.html index 566549b..1451746 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,10 +1,24 @@ +{% load static %} - Title + {% block title %}Title{% endblock %} + + + {% block head %}{% endblock %} - +
+ +
+
+ {% block main %}{% endblock %} +
+ \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index 94d9808..bf9594d 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1 +1,9 @@ {% extends "base.html" %} +{% load static %} +{% block title %}Dashboard{% endblock %} +{% block head %} + +{% endblock %} +{% block footer %}{% endblock %} +{% block main %} +{% endblock %} \ No newline at end of file diff --git a/templates/import.html b/templates/import.html new file mode 100644 index 0000000..5c834c4 --- /dev/null +++ b/templates/import.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %}Import tasks{% endblock %} +{% block main %} +
+ {% csrf_token %} +
+ + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/list.html b/templates/list.html new file mode 100644 index 0000000..fa7d0cb --- /dev/null +++ b/templates/list.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load static %} +{% block head %} + +{% endblock %} +{% block main %} + +{% endblock %} \ No newline at end of file diff --git a/templates/parents.html b/templates/parents.html new file mode 100644 index 0000000..996318a --- /dev/null +++ b/templates/parents.html @@ -0,0 +1,11 @@ +{% extends "list.html" %} +{% load static %} +{% block title %}Parents{% endblock %} +{% block element_name %}parent{% endblock %} +{% block element %} +
{{ element.name }}
+
({{ element.project_num }})
+{% endblock %} +{% block actions %} + +{% endblock %} \ No newline at end of file diff --git a/templates/project.html b/templates/project.html new file mode 100644 index 0000000..e68fc06 --- /dev/null +++ b/templates/project.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block main %} +

Editing project

+
+ {% csrf_token %} + {{ form }} + +
+{% endblock %} \ No newline at end of file diff --git a/templates/projects.html b/templates/projects.html new file mode 100644 index 0000000..38ba050 --- /dev/null +++ b/templates/projects.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% load static %} +{% block title %}Projects{% endblock %} +{% block head %} + + +{% endblock %} +{% block footer %}{% endblock %} +{% block main %} + + + + + + + + + + {% for project in projects %} + + + + + + {% endfor %} + +
ProjectParentActions
{{ project.name }} + + + + +
+{% endblock %} \ No newline at end of file