Merge branch 'release/1.4.0'

This commit is contained in:
Nathan Chapman 2021-08-07 19:30:07 -06:00
commit 12761b3103
15 changed files with 2758 additions and 52 deletions

View File

@ -1,14 +1,19 @@
{% extends 'base.html' %} {% extends "base.html" %}
{% load static %} {% load static %}
{% block head %}
<script defer src="{% static "scripts/stimulus.umd.js" %}"></script>
<script type="module" defer src="{% static "scripts/index.js" %}"></script>
{% endblock %}
{% block content %} {% block content %}
<article class="panel"> <article class="panel">
<header> <header>
<h1>Hi, {{user.first_name}}</h1> <h1>Today, {% now "D, M j" %}</h1>
</header> </header>
<section> <section>
<h2>Today, {% now "D, M j" %}</h2>
<div> <div>
<h2>Events</h2>
{% for event in today %} {% for event in today %}
<div class="today__event"> <div class="today__event">
<strong class="today__date">{{event.date|date:"D, M j"}}</strong> <strong class="today__date">{{event.date|date:"D, M j"}}</strong>
@ -21,12 +26,10 @@
<span><a href="{% url 'event-update' event.pk %}">Edit</a></span> <span><a href="{% url 'event-update' event.pk %}">Edit</a></span>
</div> </div>
{% empty %} {% empty %}
<p><em>Nothing for today.</em></p> <p><em>No events for today.</em></p>
{% endfor %} {% endfor %}
</div> </div>
</section> {% if upcoming_events %}
<p><a href="{% url 'event-create' %}" class="action-button">Add event</a></p>
<section>
<h3>Upcoming (next seven days)</h3> <h3>Upcoming (next seven days)</h3>
<div> <div>
{% for event in upcoming_events %} {% for event in upcoming_events %}
@ -42,7 +45,127 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
<p><a href="{% url 'event-list' %}">See all events</a></p> <p><a href="{% url 'event-list' %}">See all events</a></p>
</section>
<p><a href="{% url 'event-create' %}" class="action-button">Add event</a></p>
<hr>
<section>
<h2>To-do's</h2>
<div class="todos__overdue">
<h4 class="--danger">Overdue</h4>
<div>
{% regroup overdue_todos by employee as overdue_list %}
{% for employee in overdue_list %}
<p><a href="{% url 'employee-detail' employee.grouper.pk %}">{{employee.grouper}}</a></p>
{% for todo in employee.list %}
<li
class="todo__item"
data-controller="todo"
data-todo-url-value="{% url 'todo-update' todo.employee.pk todo.pk %}"
data-todo-delete-url-value="{% url 'todo-delete' todo.employee.pk todo.pk %}"
>
<div class="todo_display"
data-todo-target="display"
>
<span
data-action="click->todo#toggle"
class="todo__checkbox_button {% if todo.completed %}todo__checkbox_button--completed{% endif %}"></span>
<span class="todo__description_display">{{todo.description}}</span>
<span class="todo__due_date">{% if todo.due_date %}{{todo.due_date}}{% endif %}</span>
<div>
{% if not todo.employee.archived %}
<button class="hidden_action" data-action="todo#edit" name="edit">edit</button>
<button class="hidden_action" data-action="todo#destroy" name="destroy">delete</button>
{% endif %}
</div>
</div>
<form
class="todo_form --hidden"
data-todo-target="form"
data-action="todo#post"
data-action="change->todo#change"
action="{% url 'todo-update' todo.employee.pk todo.pk %}"
method="POST">
{% csrf_token %}
<input
data-action="todo#post"
data-todo-target="checkbox"
class="todo__checkbox_input"
name="completed"
type="checkbox"
{% if todo.completed %}checked{% endif %}
{% if todo.employee.archived %}disabled{% endif %}
>
<input name="description" type="text" value="{{todo.description}}">
<input name="due_date" type="date" value="{% if todo.due_date %}{{todo.due_date|date:"Y-m-d"}}{% endif %}">
<div class="form__savecancel">
<input class="action-button" type="submit" value="Save changes">
<a data-action="todo#cancel" href="">cancel</a>
</div>
</form>
</li>
{% endfor %}
{% endfor %}
</div>
</div>
<h4>Due today</h4>
{% regroup todos by employee as todo_list %}
{% for employee in todo_list %}
<p><a href="{% url 'employee-detail' employee.grouper.pk %}">{{employee.grouper}}</a></p>
{% for todo in employee.list %}
<li
class="todo__item"
data-controller="todo"
data-todo-url-value="{% url 'todo-update' todo.employee.pk todo.pk %}"
data-todo-delete-url-value="{% url 'todo-delete' todo.employee.pk todo.pk %}"
>
<div class="todo_display"
data-todo-target="display"
>
<span
data-action="click->todo#toggle"
class="todo__checkbox_button {% if todo.completed %}todo__checkbox_button--completed{% endif %}"></span>
<span class="todo__description_display">{{todo.description}}</span>
<span class="todo__due_date">{% if todo.due_date %}{{todo.due_date}}{% endif %}</span>
<div>
{% if not todo.employee.archived %}
<button class="hidden_action" data-action="todo#edit" name="edit">edit</button>
<button class="hidden_action" data-action="todo#destroy" name="destroy">delete</button>
{% endif %}
</div>
</div>
<form
class="todo_form --hidden"
data-todo-target="form"
data-action="todo#post"
data-action="change->todo#change"
action="{% url 'todo-update' todo.employee.pk todo.pk %}"
method="POST">
{% csrf_token %}
<input
data-action="todo#post"
data-todo-target="checkbox"
class="todo__checkbox_input"
name="completed"
type="checkbox"
{% if todo.completed %}checked{% endif %}
{% if todo.employee.archived %}disabled{% endif %}
>
<input name="description" type="text" value="{{todo.description}}">
<input name="due_date" type="date" value="{% if todo.due_date %}{{todo.due_date|date:"Y-m-d"}}{% endif %}">
<div class="form__savecancel">
<input class="action-button" type="submit" value="Save changes">
<a data-action="todo#cancel" href="">cancel</a>
</div>
</form>
</li>
{% endfor %}
{% endfor %}
</section>
<section>
</section> </section>
<section> <section>
<h3>Recent Activity</h3> <h3>Recent Activity</h3>

View File

@ -13,7 +13,7 @@ from django.contrib.auth.models import User
from .models import Profile from .models import Profile
from .forms import AccountUpdateForm, ProfileUpdateForm from .forms import AccountUpdateForm, ProfileUpdateForm
from board.models import LogEntry, Event from board.models import LogEntry, Event, Todo
class ProfileView(LoginRequiredMixin, TemplateView): class ProfileView(LoginRequiredMixin, TemplateView):
@ -26,6 +26,14 @@ class ProfileView(LoginRequiredMixin, TemplateView):
enddate = today + timedelta(days=7) enddate = today + timedelta(days=7)
context['profile'] = self.request.user.profile context['profile'] = self.request.user.profile
context['latest_activity'] = LogEntry.objects.all()[:10] context['latest_activity'] = LogEntry.objects.all()[:10]
context['todos'] = Todo.objects.filter(
due_date=today,
completed=False
)
context['overdue_todos'] = Todo.objects.filter(
due_date__lt=today,
completed=False
)
context['today'] = Event.objects.filter( context['today'] = Event.objects.filter(
date=today date=today
) )

View File

@ -71,23 +71,29 @@ class LogEntryForm(forms.ModelForm):
class TodoForm(forms.ModelForm): class TodoForm(forms.ModelForm):
class Meta: class Meta:
model = Todo model = Todo
fields = ('completed', 'description') fields = ('completed', 'description', 'due_date')
widgets = { widgets = {
'completed': forms.CheckboxInput(attrs = { 'completed': forms.CheckboxInput(attrs = {
'class': 'todo__checkbox', 'class': 'todo__checkbox',
}), }),
'description': forms.TextInput(attrs = { 'description': forms.TextInput(attrs = {
'autofocus': 'autofocus' 'autofocus': 'autofocus'
}),
'due_date': forms.DateInput(attrs = {
'type': 'date'
}) })
} }
class TodoCreateForm(forms.ModelForm): class TodoCreateForm(forms.ModelForm):
class Meta: class Meta:
model = Todo model = Todo
fields = ('description',) fields = ('description', 'due_date')
widgets = { widgets = {
'description': forms.TextInput(attrs = { 'description': forms.TextInput(attrs = {
'autofocus': 'autofocus' 'autofocus': 'autofocus'
}),
'due_date': forms.DateInput(attrs = {
'type': 'date'
}) })
} }

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-08-04 22:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('board', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='todo',
name='due_date',
field=models.DateField(blank=True, null=True),
),
]

View File

@ -1,5 +1,6 @@
from django.db import models from django.db import models
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from datetime import datetime
from django.utils import timezone from django.utils import timezone
@ -48,10 +49,18 @@ class LogEntry(models.Model):
class Todo(models.Model): class Todo(models.Model):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE) employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
description = models.CharField(max_length=64) description = models.CharField(max_length=64)
due_date = models.DateField(blank=True, null=True)
completed = models.BooleanField(default=False) completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(blank=True, null=True) completed_at = models.DateTimeField(blank=True, null=True)
def is_due(self):
today = timezone.localtime(timezone.now()).date()
if self.due_date == today:
return True
else:
return False
def __str__(self): def __str__(self):
return f"{self.employee}: {self.description}" return f"{self.employee}: {self.description}"

View File

@ -2,6 +2,7 @@
{% load static %} {% load static %}
{% block head %} {% block head %}
<script defer src="{% static "scripts/stimulus.umd.js" %}"></script>
<script type="module" defer src="{% static "scripts/index.js" %}"></script> <script type="module" defer src="{% static "scripts/index.js" %}"></script>
{% endblock %} {% endblock %}

View File

@ -1,13 +1,46 @@
<li class="todo__item"> <li
<form class="todo_form" action="{% url 'todo-update' employee.pk todo.pk %}" method="POST"> class="todo__item"
<label class="todo__details"> data-controller="todo"
<input class="todo__checkbox" name="completed" type="checkbox" {% if todo.completed %}checked{% endif %} {% if employee.archived %}disabled{% endif %}> data-todo-url-value="{% url 'todo-update' employee.pk todo.pk %}"
<input name="description" type="hidden" value="{{todo.description}}"> data-todo-delete-url-value="{% url 'todo-delete' employee.pk todo.pk %}"
<span class="todo__description_display">{{todo.description}}</span> >
</label> <div class="todo_display"
{% if not employee.archived %} data-todo-target="display"
<a class="hidden_action" data-url="{% url 'todo-update' employee.pk todo.pk %}" href="#" name="edit">Edit&hellip;</a> >
<a class="hidden_action" data-url="{% url 'todo-delete' employee.pk todo.pk %}" href="#" name="destroy">Delete&hellip;</a> <span
{% endif %} data-action="click->todo#toggle"
class="todo__checkbox_button {% if todo.completed %}todo__checkbox_button--completed{% endif %}"></span>
<span class="todo__description_display">{{todo.description}}</span>
<span class="todo__due_date">{% if todo.due_date %}{{todo.due_date}}{% endif %}</span>
<div class="todo__actions">
{% if not employee.archived %}
<button class="hidden_action" data-action="todo#edit" name="edit">edit</button>
<button class="hidden_action" data-action="todo#destroy" name="destroy">delete</button>
{% endif %}
</div>
</div>
<form
class="todo_form --hidden"
data-todo-target="form"
data-action="todo#post"
data-action="change->todo#change"
action="{% url 'todo-update' employee.pk todo.pk %}"
method="POST">
{% csrf_token %}
<input
data-action="todo#post"
data-todo-target="checkbox"
class="todo__checkbox_input"
name="completed"
type="checkbox"
{% if todo.completed %}checked{% endif %}
{% if employee.archived %}disabled{% endif %}
>
<input name="description" type="text" value="{{todo.description}}">
<input name="due_date" type="date" value="{% if todo.due_date %}{{todo.due_date|date:"Y-m-d"}}{% endif %}">
<div>
<input class="action-button" type="submit" value="Save changes">
<a data-action="todo#cancel" href="">cancel</a>
</div>
</form> </form>
</li> </li>

View File

@ -1,4 +1,4 @@
<ul id="todo__list"> <ul id="todo__list" data-controller="todo-list">
{% for todo in todo_list %} {% for todo in todo_list %}
{% include "board/todo_detail.html" with todo=todo %} {% include "board/todo_detail.html" with todo=todo %}
{% endfor %} {% endfor %}

View File

@ -3,7 +3,7 @@ import os
from celery import Celery from celery import Celery
# Set the default Django settings module for the 'celery' program. # Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onboard.settings_dev') # os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onboard.settings_dev')
app = Celery('onboard') app = Celery('onboard')

View File

@ -0,0 +1 @@
<svg enable-background="new 0 0 50 52.5" viewBox="0 0 50 52.5" xmlns="http://www.w3.org/2000/svg"><path d="m14.4 50.4-13.3-17.7c-1.8-2.2-1.4-5.5 1-7.3 2.2-1.6 5.5-1.2 7.2 1l8.8 11.7 22.3-35.7c1.5-2.4 4.8-3.1 7.2-1.6s3.1 4.8 1.6 7.2l-26.1 42.1c-.9 1.4-2.5 2.4-4.2 2.4-.1 0-.1 0-.3 0-1.6 0-3.1-.7-4.2-2.1z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@ -0,0 +1,140 @@
import getCookie from "../get_cookie.js";
export default class extends Stimulus.Controller {
static values = { url: String, deleteUrl: String }
static get targets() {
return [ "completed", "form", "display", "checkbox" ]
}
connect() {
this.editing = false
}
// load() {
// fetch(`${this.urlValue}`)
// .then((response) => response.text())
// .then((html) => {
// this.formTarget.innerHTML = html;
// if (this.formTarget.querySelector("[name=destroy]")) {
// this.destroyButton = this.formTarget.querySelector("[name=destroy]")
// this.destroyButton.addEventListener("click", this.destroy.bind(this))
// }
// if (this.formTarget.querySelector("[name=edit]")) {
// this.editButton = this.formTarget.querySelector("[name=edit]")
// this.editButton.addEventListener("click", this.edit.bind(this))
// }
// })
// }
validate() {
const inputs = new Set()
this.formTarget.querySelectorAll("input").forEach(input => {
inputs.add(input.checkValidity())
})
let valid = (inputs.has(false)) ? false : true
return valid
}
change(event) {
if (event.target.type === "checkbox") {
this.post(event)
}
}
edit(event) {
this.formTarget.classList.remove("--hidden")
this.displayTarget.classList.add("--hidden")
this.editing = true
}
toggle(event) {
if (this.checkboxTarget.checked == true) {
this.checkboxTarget.checked = false
} else {
this.checkboxTarget.checked = true
}
if (this.validate()) {
this.post()
}
}
cancel(event) {
if (event) {
event.preventDefault()
}
if (this.editing) {
this.formTarget.classList.add("--hidden")
this.displayTarget.classList.remove("--hidden")
this.editing = false
}
}
post(event) {
if (event) {
event.preventDefault()
}
// construct a new FormData object from the html form
const formData = new FormData(this.formTarget)
// get the csrftoken
const csrftoken = getCookie("csrftoken")
const options = {
method: "POST",
body: new URLSearchParams(formData),
mode: "same-origin",
};
// construct a new Request passing in the csrftoken
const request = new Request(`${this.urlValue}`, {
headers: { "X-CSRFToken": csrftoken },
})
// finally make the post request and wait for the server to respond
fetch(request, options)
.then((response) => response.text())
.then((html) => {
this.element.outerHTML = html
})
.catch((error) => {
return error;
})
}
destroy(event) {
const confirmation = confirm(
"Are you sure you would like to delete this entry?"
)
if (confirmation) {
// get the csrftoken
const csrftoken = getCookie("csrftoken")
const options = {
method: "POST",
mode: "same-origin",
};
// construct a new Request passing in the csrftoken
const request = new Request(`${this.deleteUrlValue}`, {
headers: { "X-CSRFToken": csrftoken },
})
// finally make the post request and wait for the server to respond
fetch(request, options)
.then((response) => response.text())
.then((html) => {
this.element.innerHTML = html;
setTimeout(() => {
this.element.remove()
}, 3000)
})
.catch((error) => {
return error;
})
}
}
}

View File

@ -1,11 +1,20 @@
import Form from "./form.js"; // import Form from "./form.js";
import View from "./view.js"; // import View from "./view.js";
// import TodoListController from "./controllers/todo_list_controller.js"
import TodoController from "./controllers/todo_controller.js"
// constructor(element, forms, templateName, addButton, destination) // constructor(element, forms, templateName, addButton, destination)
const todoListView = new View( // const todoListView = new View(
document.querySelector("#todos"), // document.querySelector("#todos"),
".todo_form", // ".todo_form",
"#todo__list" // "#todo__list"
) // )
const application = Stimulus.Application.start()
// application.register("todo-list", TodoListController)
application.register("todo", TodoController)

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
--blue: #10638c; --blue: #10638c;
--light-blue: #74c0e6; --light-blue: #74c0e6;
--red: #8c1016; --red: #8c1016;
--light-brown: #a69688;
} }
html { html {
@ -62,7 +63,7 @@ a {
blockquote { blockquote {
margin-left: 0; margin-left: 0;
padding-left: 2rem; padding-left: 2rem;
border-left: 2px solid var(--blue); border-left: 0.2rem solid var(--blue);
} }
body > header { body > header {
@ -154,8 +155,14 @@ article {
} }
.hidden_action { .hidden_action {
visibility: hidden;
margin-left: 1rem; margin-left: 1rem;
display: none; border: none;
background-color: var(--light-brown);
color: white;
padding: 0.15rem 0.5rem;
border-radius: 1rem;
cursor: pointer;
} }
.todo__item { .todo__item {
@ -164,19 +171,76 @@ article {
align-content: flex-start; align-content: flex-start;
} }
.todo__item:hover .hidden_action { .todo__item:hover .hidden_action {
display: inline-block; visibility: visible;
} }
.todo_display {
display: grid;
grid-template-columns: 0.25fr 2fr 1fr 1fr;
align-items: center;
gap: 0 1rem;
width: 100%;
}
.todo__actions {
font-size: 0.889rem;
}
.todo__due_date {
color: var(--light-brown);
}
.todo__checkbox { .todo__checkbox {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.todo_form { .todo__checkbox_input {
display: flex; opacity: 0;
position: absolute;
cursor: pointer;
appearance: none;
z-index: -1;
} }
.todo__details { .todos__overdue {
display: flex; background-color: var(--white);
align-items: center; padding: 1rem;
flex-direction: row; }
.todos__overdue h4 {
margin-top: 0;
}
.todo__checkbox_button {
display: inline-block;
width: 1.4em;
height: 1.4em;
border: 1px solid rgba(0,0,0,0.25);
background-color: #fff;
border-radius: 0.3em;
vertical-align: top;
cursor: pointer;
}
.todo__checkbox_button--completed {
position: relative;
border-color: rgba(0,0,0,0.1);
background-color: var(--blue);
box-shadow: none;
}
.todo__checkbox_button--completed::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
bottom: 0;
right: 0;
left: 0;
background: url(../images/checkmark.svg) no-repeat center center;
background-size: auto;
background-size: 65%;
}
.todo_form {
display: grid;
row-gap: 1rem;
margin: 1rem 0;
width: 100%;
border: 0.0625rem solid rgba(0, 0, 0, 0.25);
background-color: var(--white);
padding: 1rem;
} }
@ -207,10 +271,13 @@ article {
.action-button--danger { .action-button--danger {
background-color: var(--red); background-color: var(--red);
} }
.--danger {
color: var(--red);
}
/* FORMS */ /* FORMS */
input[type=text]:not([name=description]), input[type=text],
input[type=number], input[type=number],
input[type=date], input[type=date],
input[type=time], input[type=time],
@ -226,14 +293,6 @@ textarea, select {
box-sizing: border-box; box-sizing: border-box;
} }
input[name=description] {
border: 0.2rem solid var(--grey);
padding: 0.3rem;
font-family: inherit;
outline: none;
box-sizing: border-box;
}
input[type=radio], input[type=checkbox] { input[type=radio], input[type=checkbox] {
cursor: pointer; cursor: pointer;
width: 1.5rem; width: 1.5rem;
@ -267,6 +326,15 @@ input:focus, textarea:focus, select:focus {
border-color: var(--blue) !important; border-color: var(--blue) !important;
} }
button {
margin-left: 1rem;
border: none;
background-color: var(--light-brown);
color: white;
padding: 0.15rem 0.5rem;
border-radius: 1rem;
cursor: pointer;
}

View File

@ -30,11 +30,7 @@
<a href="{% url 'employee-list' %}">Employees</a> <a href="{% url 'employee-list' %}">Employees</a>
</div> </div>
<div class="nav__auth"> <div class="nav__auth">
{% if user.first_name or user.last_name %}
<a href="{% url 'account-detail' user.pk %}">{{ user.first_name }} {{ user.last_name }}</a>
{% else %}
<a href="{% url 'account-detail' user.pk %}">Profile</a> <a href="{% url 'account-detail' user.pk %}">Profile</a>
{% endif %}
<a class="action-button" href="{% url 'logout' %}">Logout</a> <a class="action-button" href="{% url 'logout' %}">Logout</a>
</div> </div>
{% else %} {% else %}