Compare commits

...

10 Commits

Author SHA1 Message Date
Nathan Chapman
64058cadd8 Merge branch 'hotfix/1.4.2' 2021-08-24 09:25:12 -06:00
Nathan Chapman
8ee75f7ae3 Remove redundancy in profile 2021-08-24 09:25:05 -06:00
Nathan Chapman
2b7b14a4ba Merge branch 'release/1.4.1' 2021-08-24 09:13:19 -06:00
Nathan Chapman
b870a76a31 Add css compressor and add todo hover effect for easy editing 2021-08-24 09:12:52 -06:00
Nathan Chapman
12761b3103 Merge branch 'release/1.4.0' 2021-08-07 19:30:07 -06:00
Nathan Chapman
ae816accd1 Merge tag '1.4.0' into develop
1.4.0 1.4.0
2021-08-07 19:30:07 -06:00
Nathan Chapman
25da0f93ab Add due dates to To-do's 2021-08-07 19:29:39 -06:00
Nathan Chapman
83e34f2b85 Merge branch 'release/1.3.2' 2021-07-28 21:19:30 -06:00
Nathan Chapman
b446384d17 Merge tag '1.3.2' into develop
1.3.2 1.3.2
2021-07-28 21:19:30 -06:00
Nathan Chapman
01ddc76720 Add delete button to events 2021-07-28 21:19:09 -06:00
24 changed files with 2878 additions and 279 deletions

1
.gitignore vendored
View File

@ -102,6 +102,7 @@ venv.bak/
# Static CACHE # Static CACHE
/static/CACHE/ /static/CACHE/
/staticfiles/CACHE/
# sftp configuration file # sftp configuration file
sftp-config.json sftp-config.json

View File

@ -10,6 +10,7 @@ psycopg2-binary = "*"
django-anymail = {extras = ["mailgun"], version = "*"} django-anymail = {extras = ["mailgun"], version = "*"}
celery = {extras = ["redis"], version = "*"} celery = {extras = ["redis"], version = "*"}
django-celery-beat = "*" django-celery-beat = "*"
django-compressor = "*"
[dev-packages] [dev-packages]

104
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "ffe96f34ca738a35a283e9ac98b5f1a74609c67619f18e8eb7ba62967247766f" "sha256": "f5ee1dc635277e81758920aeb79d3788dd518bde2e01af520b76659587f7c96c"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -59,11 +59,11 @@
}, },
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1", "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
"sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12" "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
], ],
"markers": "python_version >= '3'", "markers": "python_version >= '3'",
"version": "==2.0.3" "version": "==2.0.4"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@ -95,11 +95,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd", "sha256:7f92413529aa0e291f3be78ab19be31aefb1e1c9a52cd59e130f505f27a51f13",
"sha256:c58b5f19c5ae0afe6d75cbdd7df561e6eb929339985dbbda2565e1cabb19a62e" "sha256:f27f8544c9d4c383bbe007c57e3235918e258364577373d4920e9162837be022"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.2.5" "version": "==3.2.6"
}, },
"django-anymail": { "django-anymail": {
"extras": [ "extras": [
@ -112,6 +112,13 @@
"index": "pypi", "index": "pypi",
"version": "==8.4" "version": "==8.4"
}, },
"django-appconf": {
"hashes": [
"sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06",
"sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"
],
"version": "==1.0.4"
},
"django-celery-beat": { "django-celery-beat": {
"hashes": [ "hashes": [
"sha256:97ae5eb309541551bdb07bf60cc57cadacf42a74287560ced2d2c06298620234", "sha256:97ae5eb309541551bdb07bf60cc57cadacf42a74287560ced2d2c06298620234",
@ -120,6 +127,14 @@
"index": "pypi", "index": "pypi",
"version": "==2.2.1" "version": "==2.2.1"
}, },
"django-compressor": {
"hashes": [
"sha256:3358077605c146fdcca5f9eaffb50aa5dbe15f238f8854679115ebf31c0415e0",
"sha256:f8313f59d5e65712fc28787d084fe834997c9dfa92d064a1a3ec3d3366594d04"
],
"index": "pypi",
"version": "==2.4.1"
},
"django-timezone-field": { "django-timezone-field": {
"hashes": [ "hashes": [
"sha256:6dc782e31036a58da35b553bd00c70f112d794700025270d8a6a4c1d2e5b26c6", "sha256:6dc782e31036a58da35b553bd00c70f112d794700025270d8a6a4c1d2e5b26c6",
@ -128,6 +143,14 @@
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==4.2.1" "version": "==4.2.1"
}, },
"gunicorn": {
"hashes": [
"sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
"sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
],
"index": "pypi",
"version": "==20.1.0"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
@ -146,11 +169,46 @@
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
"sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f", "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c",
"sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88" "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"
], ],
"markers": "python_full_version >= '3.6.1'", "markers": "python_full_version >= '3.6.2'",
"version": "==3.0.19" "version": "==3.0.20"
},
"psycopg2-binary": {
"hashes": [
"sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975",
"sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd",
"sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616",
"sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2",
"sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90",
"sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a",
"sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e",
"sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d",
"sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed",
"sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a",
"sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140",
"sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32",
"sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31",
"sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a",
"sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917",
"sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf",
"sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7",
"sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0",
"sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72",
"sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698",
"sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773",
"sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68",
"sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76",
"sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4",
"sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f",
"sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34",
"sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce",
"sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a",
"sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"
],
"index": "pypi",
"version": "==2.9.1"
}, },
"python-crontab": { "python-crontab": {
"hashes": [ "hashes": [
@ -173,6 +231,12 @@
], ],
"version": "==2021.1" "version": "==2021.1"
}, },
"rcssmin": {
"hashes": [
"sha256:ca87b695d3d7864157773a61263e5abb96006e9ff0e021eff90cbe0e1ba18270"
],
"version": "==1.0.6"
},
"redis": { "redis": {
"hashes": [ "hashes": [
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
@ -188,6 +252,24 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==2.26.0" "version": "==2.26.0"
}, },
"rjsmin": {
"hashes": [
"sha256:0ab825839125eaca57cc59581d72e596e58a7a56fbc0839996b7528f0343a0a8",
"sha256:211c2fe8298951663bbc02acdffbf714f6793df54bfc50e1c6c9e71b3f2559a3",
"sha256:466fe70cc5647c7c51b3260c7e2e323a98b2b173564247f9c89e977720a0645f",
"sha256:585e75a84d9199b68056fd4a083d9a61e2a92dfd10ff6d4ce5bdb04bc3bdbfaf",
"sha256:6044ca86e917cd5bb2f95e6679a4192cef812122f28ee08c677513de019629b3",
"sha256:714329db774a90947e0e2086cdddb80d5e8c4ac1c70c9f92436378dedb8ae345",
"sha256:799890bd07a048892d8d3deb9042dbc20b7f5d0eb7da91e9483c561033b23ce2",
"sha256:975b69754d6a76be47c0bead12367a1ca9220d08e5393f80bab0230d4625d1f4",
"sha256:b15dc75c71f65d9493a8c7fa233fdcec823e3f1b88ad84a843ffef49b338ac32",
"sha256:dd0f4819df4243ffe4c964995794c79ca43943b5b756de84be92b445a652fb86",
"sha256:e3908b21ebb584ce74a6ac233bdb5f29485752c9d3be5e50c5484ed74169232c",
"sha256:e487a7783ac4339e79ec610b98228eb9ac72178973e3dee16eba0e3feef25924",
"sha256:ecd29f1b3e66a4c0753105baec262b331bcbceefc22fbe6f7e8bcd2067bcb4d7"
],
"version": "==1.1.0"
},
"six": { "six": {
"hashes": [ "hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",

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,8 +45,129 @@
</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> </section>
<p><a href="{% url 'event-create' %}" class="action-button">Add event</a></p>
<hr>
<section>
<h2>To-do's</h2>
{% if overdue_todos %}
<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>
{% endif %}
<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 %}
{% empty %}
<p><em>No to-do's for today.</em></p>
{% endfor %}
</section>
<hr>
<section> <section>
<h3>Recent Activity</h3> <h3>Recent Activity</h3>
{% regroup latest_activity by created_at.date as latest_activity_re %} {% regroup latest_activity by created_at.date as latest_activity_re %}

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}"
@ -69,7 +78,7 @@ class Event(models.Model):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE, blank=True, null=True) employee = models.ForeignKey(Employee, on_delete=models.CASCADE, blank=True, null=True)
def __str__(self): def __str__(self):
return f"{self.description} on {self.date} @ {self.time}" return f"{self.name} on {self.date} @ {self.time}"
def get_absolute_url(self): def get_absolute_url(self):
return reverse('event-detail', kwargs={'pk': self.pk}) return reverse('event-detail', kwargs={'pk': self.pk})

View File

@ -1,7 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load compress %}
{% 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 %}
@ -40,6 +42,8 @@
<span class="today__time">{{event.time|time:"TIME_FORMAT"}}</span> <span class="today__time">{{event.time|time:"TIME_FORMAT"}}</span>
<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 %}
<p><em>No events for employee.</em></p>
{% endfor %} {% endfor %}
</section> </section>
<section id="todos"> <section id="todos">

View File

@ -6,6 +6,9 @@
<section> <section>
<form method="POST" action="{% url 'event-update' event.pk %}"> <form method="POST" action="{% url 'event-update' event.pk %}">
{% csrf_token %} {% csrf_token %}
<p>
<a class="action-button action-button--danger" href="{% url 'event-delete' event.pk %}">Delete this event</a>
</p>
{{form.as_p}} {{form.as_p}}
<p> <p>
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'profile-detail' %}">cancel</a> <input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'profile-detail' %}">cancel</a>

View File

@ -3,7 +3,7 @@
{% block content %} {% block content %}
<article> <article>
<header> <header>
<h1>Update Event</h1> <h1>Update LogEntry</h1>
<p><a class="action-button action-button--danger" href="{% url 'entry-delete' employee.pk logentry.pk %}">Delete this entry</a></p> <p><a class="action-button action-button--danger" href="{% url 'entry-delete' employee.pk logentry.pk %}">Delete this entry</a></p>
</header> </header>
<section> <section>

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

@ -41,6 +41,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.flatpages', 'django.contrib.flatpages',
'compressor',
'accounts.apps.AccountsConfig', 'accounts.apps.AccountsConfig',
'board.apps.BoardConfig', 'board.apps.BoardConfig',
] ]
@ -132,6 +133,13 @@ STATICFILES_DIRS = [
] ]
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/onboard.windmillapps.org/static/' STATIC_ROOT = '/var/www/onboard.windmillapps.org/static/'
COMPRESS_ENABLED = True
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
)
# Default primary key field type # Default primary key field type

View File

@ -39,6 +39,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.flatpages', 'django.contrib.flatpages',
'compressor',
'django_celery_beat', 'django_celery_beat',
'accounts.apps.AccountsConfig', 'accounts.apps.AccountsConfig',
'board.apps.BoardConfig', 'board.apps.BoardConfig',
@ -124,8 +125,15 @@ USE_TZ = True
# https://docs.djangoproject.com/en/3.2/howto/static-files/ # https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static'] STATICFILES_DIRS = [BASE_DIR / 'static']
COMPRESS_ENABLED = True
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
)
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

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,155 +0,0 @@
import getCookie from "./get_cookie.js";
export default class Form {
constructor(form, isNew=false) {
if (typeof form === "string") {
this.form = document.querySelector(`#${form}`)
} else {
this.form = form;
}
this.url = this.form.attributes.action.value;
this.destroyButton = null
if (isNew) {
this.load()
} else {
if (this.form.querySelector("[name=destroy]")) {
this.destroyButton = this.form.querySelector("[name=destroy]")
this.destroyButton.addEventListener("click", this.destroy.bind(this))
}
if (this.form.querySelector("[name=edit]")) {
this.editButton = this.form.querySelector("[name=edit]")
this.editButton.addEventListener("click", this.edit.bind(this))
}
}
this.form.addEventListener("submit", this.post.bind(this))
this.form.addEventListener("change", this.change.bind(this))
}
load() {
fetch(`${this.url}`)
.then((response) => response.text())
.then((html) => {
this.form.innerHTML = html;
if (this.form.querySelector("[name=destroy]")) {
this.destroyButton = this.form.querySelector("[name=destroy]")
this.destroyButton.addEventListener("click", this.destroy.bind(this))
}
if (this.form.querySelector("[name=edit]")) {
this.editButton = this.form.querySelector("[name=edit]")
this.editButton.addEventListener("click", this.edit.bind(this))
}
})
}
validate() {
const inputs = new Set()
this.form.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) {
if (event) {
event.preventDefault()
}
this.form.querySelector("[name=description]").type = 'text'
let display = this.form.querySelector(".todo__description_display")
display.classList.add('--hidden')
}
post(event) {
if (event) {
event.preventDefault()
}
// construct a new FormData object from the html form
const formData = new FormData(this.form)
// 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.url}`, {
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) => {
var doc = new DOMParser().parseFromString(html, "text/html")
this.url = doc.forms[0].attributes.action.value;
this.form.innerHTML = html;
if (this.form.querySelector("[name=destroy]")) {
this.destroyButton = this.form.querySelector("[name=destroy]")
this.destroyButton.addEventListener(
"click",
this.destroy.bind(this)
)
}
if (this.form.querySelector("[name=edit]")) {
this.editButton = this.form.querySelector("[name=edit]")
this.editButton.addEventListener("click", this.edit.bind(this))
}
})
.catch((error) => {
return error;
})
}
destroy(event) {
if (event) {
event.preventDefault()
}
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.destroyButton.dataset.url}`, {
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.form.innerHTML = html;
setTimeout(() => {
this.form.remove()
}, 3000)
})
.catch((error) => {
return error;
})
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +0,0 @@
import Form from "./form.js";
export default class View {
constructor(element, forms, list) {
this.element = element
this.forms = this.element.querySelectorAll(forms)
this.list = this.element.querySelector(list)
this.observer = null
this.connect()
for (const form of this.forms) {
new Form(form);
}
}
connect() {
this.observe(this.list)
}
observe(list, config = { attributes: true, childList: true, subtree: true }) {
const callback = function (mutationList, observer) {
mutationList.forEach(mutation => {
switch(mutation.type) {
case 'childList':
if (mutation.target === list) {
mutation.addedNodes.forEach(node => {
if (node.children) {
let potentialForm = node.children[0].children[0]
if (potentialForm.nodeName === "FORM") {
new Form(potentialForm, true);
}
}
})
}
break;
case 'attributes':
/* An attribute value changed on the element in
mutation.target.
The attribute name is in mutation.attributeName, and
its previous value is in mutation.oldValue. */
break;
}
});
};
this.observer = new MutationObserver(callback);
this.observer.observe(this.list, config);
}
}

View File

@ -1,10 +1,11 @@
:root { :root {
--white: #f8f8fb; --white: #f8f8fb;
--black: #393d3f; --black: #393d3f;
--grey: #a7b4bb; --gray: #a7b4bb;
--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 {
@ -73,7 +74,7 @@ article {
max-width: 64rem; max-width: 64rem;
margin: 0 auto 2rem; margin: 0 auto 2rem;
padding: 1rem; padding: 1rem;
border: 0.2rem solid var(--grey); border: 0.2rem solid var(--gray);
} }
@media all and (max-width: 64rem) { @media all and (max-width: 64rem) {
@ -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 {
@ -163,20 +170,83 @@ article {
align-items: center; align-items: center;
align-content: flex-start; align-content: flex-start;
} }
.todo__item:hover {
background-color: #e3e3e3;
}
.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;
}
h2 + 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,17 +277,20 @@ 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],
input[type=password], input[type=password],
input[type=search], input[type=search],
textarea, select { textarea, select {
border: 0.2rem solid var(--grey); border: 0.2rem solid var(--gray);
padding: 0.3rem; padding: 0.3rem;
font-family: inherit; font-family: inherit;
outline: none; outline: none;
@ -226,14 +299,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 +332,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;
}
@ -340,7 +414,7 @@ hgroup {
column-gap: 1rem; column-gap: 1rem;
margin-bottom: 2rem; margin-bottom: 2rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
border-bottom: 0.046rem solid var(--grey); border-bottom: 0.046rem solid var(--gray);
} }

View File

@ -1,4 +1,5 @@
{% load static %} {% load static %}
{% load compress %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -12,8 +13,10 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
{% compress css %}
<link rel="stylesheet" type="text/css" href="{% static "styles/normalize.css" %}"> <link rel="stylesheet" type="text/css" href="{% static "styles/normalize.css" %}">
<link rel="stylesheet" type="text/css" href="{% static "styles/main.css" %}"> <link rel="stylesheet" type="text/css" href="{% static "styles/main.css" %}">
{% endcompress %}
{% block head %} {% block head %}
{% endblock %} {% endblock %}
@ -30,11 +33,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 %}