added basic pages + task import
This commit is contained in:
parent
6810fc976a
commit
cd604e39a0
@ -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'
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -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
9
context_processors.py
Normal 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
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"
|
||||
|
@ -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>
|
@ -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
12
templates/import.html
Normal 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
19
templates/list.html
Normal 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
11
templates/parents.html
Normal 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
10
templates/project.html
Normal 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
38
templates/projects.html
Normal 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 %}
|
Loading…
x
Reference in New Issue
Block a user