added basic pages + task import
This commit is contained in:
36
dispatcher/core.py
Normal file
36
dispatcher/core.py
Normal file
@ -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")
|
||||
|
12
dispatcher/forms.py
Normal file
12
dispatcher/forms.py
Normal file
@ -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()
|
19
dispatcher/migrations/0002_alter_project_parent.py
Normal file
19
dispatcher/migrations/0002_alter_project_parent.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
18
dispatcher/migrations/0003_task_name.py
Normal file
18
dispatcher/migrations/0003_task_name.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
17
dispatcher/migrations/0004_task_unique_daily_task.py
Normal file
17
dispatcher/migrations/0004_task_unique_daily_task.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
@ -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")
|
||||
]
|
||||
|
82
dispatcher/static/base.css
Normal file
82
dispatcher/static/base.css
Normal file
@ -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;
|
||||
}
|
0
dispatcher/static/base.js
Normal file
0
dispatcher/static/base.js
Normal file
0
dispatcher/static/dashboard.css
Normal file
0
dispatcher/static/dashboard.css
Normal file
20
dispatcher/static/list.css
Normal file
20
dispatcher/static/list.css
Normal file
@ -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;
|
||||
}
|
24
dispatcher/static/projects.css
Normal file
24
dispatcher/static/projects.css
Normal file
@ -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);
|
||||
}
|
8
dispatcher/static/projects.js
Normal file
8
dispatcher/static/projects.js
Normal file
@ -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}/`
|
||||
})
|
||||
})
|
||||
})
|
@ -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")
|
||||
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"
|
||||
|
Reference in New Issue
Block a user