added basic pages + task import

This commit is contained in:
Louis Heredero 2025-01-26 00:53:17 +01:00
parent 6810fc976a
commit cd604e39a0
Signed by: HEL
GPG Key ID: 8D83DE470F8544E7
23 changed files with 425 additions and 6 deletions

View File

@ -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'
],
},
},

View File

@ -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/<int:id>/", views.project_view, name="project"),
]

9
context_processors.py Normal file
View File

@ -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'},
]
}

36
dispatcher/core.py Normal file
View 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
View 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()

View 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'),
),
]

View 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),
),
]

View 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'),
),
]

View File

@ -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")
]

View 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;
}

View File

View File

View 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;
}

View 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);
}

View 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}/`
})
})
})

View File

@ -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"

View File

@ -1,10 +1,24 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<title>{% block title %}Title{% endblock %}</title>
<link rel="stylesheet" href="{% static "base.css" %}">
<script src="{% static "base.js" %}"></script>
{% block head %}{% endblock %}
</head>
<body>
<header>
<nav>
{% for nav_link in nav_links %}
<a href="{% url nav_link.view %}" {% if request.resolver_match.view_name == nav_link.view %}class="active"{% endif %}>{{ nav_link.label }}</a>
{% endfor %}
</nav>
</header>
<main>
{% block main %}{% endblock %}
</main>
<footer>{% block footer %}{% endblock %}</footer>
</body>
</html>

View File

@ -1 +1,9 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Dashboard{% endblock %}
{% block head %}
<link rel="stylesheet" href="{% static "dashboard.css" %}">
{% endblock %}
{% block footer %}{% endblock %}
{% block main %}
{% endblock %}

12
templates/import.html Normal file
View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}Import tasks{% endblock %}
{% block main %}
<form action="" method="POST" enctype="multipart/form-data">
{% csrf_token %}
<div>
<label for="file">File</label>
<input type="file" name="file" id="file">
</div>
<button type="submit">Import</button>
</form>
{% endblock %}

19
templates/list.html Normal file
View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% load static %}
{% block head %}
<link rel="stylesheet" href="{% static "list.css" %}">
{% endblock %}
{% block main %}
<ul class="list">
{% for element in elements %}
<li data-id="{{ element.id }}">
{% block element %}{% endblock %}
<div class="actions">
{% block actions %}{% endblock %}
</div>
</li>
{% empty %}
<li class="empty">No {% block element_name %}{% endblock %} defined yet</li>
{% endfor %}
</ul>
{% endblock %}

11
templates/parents.html Normal file
View File

@ -0,0 +1,11 @@
{% extends "list.html" %}
{% load static %}
{% block title %}Parents{% endblock %}
{% block element_name %}parent{% endblock %}
{% block element %}
<div class="name">{{ element.name }}</div>
<div class="project_num">({{ element.project_num }})</div>
{% endblock %}
{% block actions %}
<button class="edit">Edit</button>
{% endblock %}

10
templates/project.html Normal file
View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block main %}
<h2>Editing project</h2>
<form action="" method="POST">
{% csrf_token %}
{{ form }}
<button type="submit">Save</button>
</form>
{% endblock %}

38
templates/projects.html Normal file
View File

@ -0,0 +1,38 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Projects{% endblock %}
{% block head %}
<link rel="stylesheet" href="{% static "projects.css" %}">
<script src="{% static "projects.js" %}"></script>
{% endblock %}
{% block footer %}{% endblock %}
{% block main %}
<table class="projects">
<thead>
<tr>
<th>Project</th>
<th>Parent</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr data-id="{{ project.id }}">
<td>{{ project.name }}</td>
<td>
<select class="parent-sel">
<option value=""></option>
{% for parent in parents %}
<option value="{{ parent.id }}" {% if project.parent.id == parent.id %}selected{% endif %}>{{ parent.name }}</option>
{% endfor %}
</select>
</td>
<td>
<button class="see">See</button>
<button class="delete">Delete</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}