Merge branch 'feature/events' into develop

This commit is contained in:
Nathan Chapman 2021-07-28 18:13:35 -06:00
commit f34a85fcdd
28 changed files with 813 additions and 48 deletions

View File

@ -6,6 +6,8 @@ name = "pypi"
[packages] [packages]
django = "*" django = "*"
django-anymail = {extras = ["mailgun"], version = "*"} django-anymail = {extras = ["mailgun"], version = "*"}
celery = {extras = ["redis"], version = "*"}
django-celery-beat = "*"
[dev-packages] [dev-packages]

132
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "88b6a9fdda6471bfc16c42a906945bb034d4b2d576f31179a72471296eac58cd" "sha256": "ffe96f34ca738a35a283e9ac98b5f1a74609c67619f18e8eb7ba62967247766f"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -16,6 +16,14 @@
] ]
}, },
"default": { "default": {
"amqp": {
"hashes": [
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
],
"markers": "python_version >= '3.6'",
"version": "==5.0.6"
},
"asgiref": { "asgiref": {
"hashes": [ "hashes": [
"sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9", "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9",
@ -24,6 +32,24 @@
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==3.4.1" "version": "==3.4.1"
}, },
"billiard": {
"hashes": [
"sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547",
"sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"
],
"version": "==3.6.4.0"
},
"celery": {
"extras": [
"redis"
],
"hashes": [
"sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0",
"sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42"
],
"index": "pypi",
"version": "==5.1.2"
},
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
@ -39,6 +65,34 @@
"markers": "python_version >= '3'", "markers": "python_version >= '3'",
"version": "==2.0.3" "version": "==2.0.3"
}, },
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"click-didyoumean": {
"hashes": [
"sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"
],
"version": "==0.0.3"
},
"click-plugins": {
"hashes": [
"sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b",
"sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"
],
"version": "==1.1.1"
},
"click-repl": {
"hashes": [
"sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b",
"sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8"
],
"version": "==0.2.0"
},
"django": { "django": {
"hashes": [ "hashes": [
"sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd", "sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd",
@ -58,6 +112,22 @@
"index": "pypi", "index": "pypi",
"version": "==8.4" "version": "==8.4"
}, },
"django-celery-beat": {
"hashes": [
"sha256:97ae5eb309541551bdb07bf60cc57cadacf42a74287560ced2d2c06298620234",
"sha256:ab43049634fd18dc037927d7c2c7d5f67f95283a20ebbda55f42f8606412e66c"
],
"index": "pypi",
"version": "==2.2.1"
},
"django-timezone-field": {
"hashes": [
"sha256:6dc782e31036a58da35b553bd00c70f112d794700025270d8a6a4c1d2e5b26c6",
"sha256:97780cde658daa5094ae515bb55ca97c1352928ab554041207ad515dee3fe971"
],
"markers": "python_version >= '3.5'",
"version": "==4.2.1"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
@ -66,6 +136,36 @@
"markers": "python_version >= '3'", "markers": "python_version >= '3'",
"version": "==3.2" "version": "==3.2"
}, },
"kombu": {
"hashes": [
"sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d",
"sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"
],
"markers": "python_version >= '3.6'",
"version": "==5.1.0"
},
"prompt-toolkit": {
"hashes": [
"sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f",
"sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==3.0.19"
},
"python-crontab": {
"hashes": [
"sha256:4bbe7e720753a132ca4ca9d4094915f40e9d9dc8a807a4564007651018ce8c31"
],
"version": "==2.5.1"
},
"python-dateutil": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2"
},
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
@ -73,6 +173,13 @@
], ],
"version": "==2021.1" "version": "==2021.1"
}, },
"redis": {
"hashes": [
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
],
"version": "==3.5.3"
},
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
@ -81,6 +188,14 @@
"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"
}, },
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sqlparse": { "sqlparse": {
"hashes": [ "hashes": [
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
@ -96,6 +211,21 @@
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.6" "version": "==1.26.6"
},
"vine": {
"hashes": [
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
],
"markers": "python_version >= '3.6'",
"version": "==5.0.0"
},
"wcwidth": {
"hashes": [
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
],
"version": "==0.2.5"
} }
}, },
"develop": {} "develop": {}

View File

@ -1,11 +1,13 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<section class="panel"> <article>
<h1>{{ user.first_name }} {{ user.last_name }}</h1> <h1>{{ user.first_name }} {{ user.last_name }}</h1>
<section>
<p>{{ user.email }}</p> <p>{{ user.email }}</p>
<p> <p>
<a href="{% url 'account-update' user.id %}">Update email/change password</a> <a href="{% url 'account-update' user.id %}">Update email/change password</a>
</p> </p>
</section> </section>
</article>
{% endblock %} {% endblock %}

View File

@ -6,5 +6,63 @@
<header> <header>
<h1>Hi, {{user.first_name}}</h1> <h1>Hi, {{user.first_name}}</h1>
</header> </header>
<section>
<h2>Today, {% now "D, M j" %}</h2>
<div>
{% for event in today %}
<div class="today__event">
<strong class="today__date">{{event.date|date:"D, M j"}}</strong>
<span>{{event.name}}
{% if event.employee %} for
<a href="{% url 'employee-detail' event.employee.pk %}">{{event.employee}}</a>
{% endif %}
</span>
<span class="today__time">{{event.time|time:"TIME_FORMAT"}}</span>
<span><a href="{% url 'event-update' event.pk %}">Edit</a></span>
</div>
{% endfor %}
</div>
</section>
<p><a href="{% url 'event-create' %}" class="action-button">Add event</a></p>
<section>
<h3>Upcoming (next seven days)</h3>
<div>
{% for event in upcoming_events %}
<div class="today__event">
<strong class="today__date">{{event.date|date:"D, M j"}}</strong>
<span>{{event.name}}
{% if event.employee %} for
<a href="{% url 'employee-detail' event.employee.pk %}">{{event.employee}}</a>
{% endif %}
</span>
<span class="today__time">{{event.time|time:"TIME_FORMAT"}}</span>
<span><a href="{% url 'event-update' event.pk %}">Edit</a></span>
</div>
{% endfor %}
</div>
<p><a href="{% url 'event-list' %}">See all events</a></p>
</section>
<section>
<h3>Recent Activity</h3>
{% regroup latest_activity by created_at.date as latest_activity_re %}
{% for date in latest_activity_re %}
<h5>{{date.grouper|date:"D, M j"}}</h5>
{% for entry in date.list %}
<p class="activity__item">
<span>{{entry.created_at|time:"TIME_FORMAT"}}</span>
<span>
{{entry.notes}}
<em>for</em>
<a href="{% url 'employee-detail' entry.employee.pk %}">{{entry.employee}}</a>
</span>
<span><a href="{% url 'entry-update' entry.employee.pk entry.pk %}">Edit</a></span>
</p>
{% endfor %}
{% endfor %}
</section>
</article> </article>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,6 @@
import pytz import pytz
from django.utils import timezone from django.utils import timezone
from datetime import date, timedelta
from django.shortcuts import render, reverse, redirect from django.shortcuts import render, reverse, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
@ -12,14 +13,25 @@ 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
class ProfileView(LoginRequiredMixin, TemplateView): class ProfileView(LoginRequiredMixin, TemplateView):
template_name = 'accounts/profile.html' template_name = 'accounts/profile.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
today = date.today()
tomorrow = today + timedelta(days=1)
enddate = today + timedelta(days=6)
context['profile'] = self.request.user.profile context['profile'] = self.request.user.profile
context['latest_activity'] = LogEntry.objects.all()[:10]
context['today'] = Event.objects.filter(
date=today
)
context['upcoming_events'] = Event.objects.filter(
date__range=[tomorrow, enddate]
)
return context return context
class ProfileUpdateView(LoginRequiredMixin, UpdateView): class ProfileUpdateView(LoginRequiredMixin, UpdateView):

View File

@ -4,8 +4,10 @@ from .models import (
Employee, Employee,
LogEntry, LogEntry,
Todo, Todo,
Event,
) )
admin.site.register(Employee) admin.site.register(Employee)
admin.site.register(LogEntry) admin.site.register(LogEntry)
admin.site.register(Todo) admin.site.register(Todo)
admin.site.register(Event)

View File

@ -1,6 +1,7 @@
from datetime import datetime from datetime import datetime
from django import forms from django import forms
from .models import Employee, LogEntry, Todo from django.utils import timezone
from .models import Employee, LogEntry, Todo, Event
from .regex import process_regex from .regex import process_regex
class EmployeePreCreateForm(forms.Form): class EmployeePreCreateForm(forms.Form):
@ -89,3 +90,25 @@ class TodoCreateForm(forms.ModelForm):
'autofocus': 'autofocus' 'autofocus': 'autofocus'
}) })
} }
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = (
'name',
'date',
'time',
'employee',
)
widgets = {
'name': forms.TextInput(attrs = {
'autofocus': 'autofocus'
}),
'date': forms.DateInput(attrs = {
'type': 'date',
'value': timezone.now().date(),
}),
'time': forms.DateInput(attrs = {
'type': 'time',
}),
}

View File

@ -0,0 +1,70 @@
# Generated by Django 3.2.5 on 2021-07-27 00:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('board', '0002_auto_20210709_0103'),
]
operations = [
migrations.AlterModelOptions(
name='employee',
options={'ordering': ('-hire_date',)},
),
migrations.AlterModelOptions(
name='logentry',
options={'ordering': ('-created_at',)},
),
migrations.AlterField(
model_name='employee',
name='department',
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AlterField(
model_name='employee',
name='first_name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='employee',
name='last_name',
field=models.CharField(max_length=64),
),
migrations.AlterField(
model_name='employee',
name='manager',
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AlterField(
model_name='employee',
name='title',
field=models.CharField(blank=True, max_length=64, null=True),
),
migrations.AlterField(
model_name='logentry',
name='notes',
field=models.CharField(max_length=500),
),
migrations.AlterField(
model_name='todo',
name='description',
field=models.CharField(max_length=64),
),
migrations.CreateModel(
name='Event',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.CharField(max_length=64)),
('date', models.DateField()),
('time', models.TimeField(blank=True, null=True)),
('employee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='board.employee')),
],
options={
'ordering': ('date', 'time'),
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-07-27 01:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('board', '0003_auto_20210727_0006'),
]
operations = [
migrations.RenameField(
model_name='event',
old_name='description',
new_name='name',
),
]

View File

@ -57,3 +57,19 @@ class Todo(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('todo-detail', kwargs={'pk': self.employee.pk, 'todo_pk': self.pk}) return reverse('todo-detail', kwargs={'pk': self.employee.pk, 'todo_pk': self.pk})
class Event(models.Model):
class Meta:
ordering = ('date', 'time')
name = models.CharField(max_length=64)
date = models.DateField()
time = models.TimeField(blank=True, null=True)
employee = models.ForeignKey(Employee, on_delete=models.CASCADE, blank=True, null=True)
def __str__(self):
return f"{self.description} on {self.date} @ {self.time}"
def get_absolute_url(self):
return reverse('event-detail', kwargs={'pk': self.pk})

View File

@ -31,6 +31,17 @@
<p><strong>Initial comments</strong>:<p> <p><strong>Initial comments</strong>:<p>
<blockquote>{{employee.initial_comments|linebreaksbr}}</blockquote> <blockquote>{{employee.initial_comments|linebreaksbr}}</blockquote>
</section> </section>
<section id="events">
<h3>Employee Events</h3>
{% for event in employee.event_set.all %}
<div class="today__event">
<strong class="today__date">{{event.date|date:"D, M j"}}</strong>
<span>{{event.name}}</span>
<span class="today__time">{{event.time|time:"TIME_FORMAT"}}</span>
<span><a href="{% url 'event-update' event.pk %}">Edit</a></span>
</div>
{% endfor %}
</section>
<section id="todos"> <section id="todos">
<h3>To-do's</h3> <h3>To-do's</h3>
{% include "board/todo_list.html" with todo_list=employee.todo_set.all %} {% include "board/todo_list.html" with todo_list=employee.todo_set.all %}
@ -45,12 +56,17 @@
<p> <p>
<a class="action-button" href="{% url 'entry-create' employee.pk %}">Add an Entry</a> <a class="action-button" href="{% url 'entry-create' employee.pk %}">Add an Entry</a>
</p> </p>
{% for entry in employee.logentry_set.all %} {% regroup employee.logentry_set.all by created_at.date as activity %}
<p> {% for date in activity %}
<span>{{entry.created_at|date:"SHORT_DATE_FORMAT"}}&mdash;</span> <h5>{{date.grouper|date:"D, M j"}}</h5>
{% for entry in date.list %}
<p class="activity__item">
<span>{{entry.created_at|time:"TIME_FORMAT"}}</span>
<span>{{entry.notes}}</span> <span>{{entry.notes}}</span>
<span><a href="{% url 'entry-update' employee.pk entry.pk %}">Edit</a></span>
</p> </p>
{% endfor %} {% endfor %}
{% endfor %}
</section> </section>
</article> </article>
{% endblock %} {% endblock %}

View File

@ -8,7 +8,8 @@
{% for employee in employee_list %} {% for employee in employee_list %}
<p> <p>
<a href="{% url 'employee-detail' employee.pk %}"><strong>{{employee.full_name}}</strong></a><br> <a href="{% url 'employee-detail' employee.pk %}"><strong>{{employee.full_name}}</strong></a><br>
<small>Hire date: {{employee.hire_date|date:"SHORT_DATE_FORMAT"}}</small> <small>Hire date: {{employee.hire_date|date:"SHORT_DATE_FORMAT"}}</small><br>
<small>Department: {{employee.department}}</small>
</p> </p>
{% endfor %} {% endfor %}
</section> </section>

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Delete {{event}}</h1>
<form method="post" action="{% url 'event-delete' event.pk %}">
{% csrf_token %}
<p>
<input class="action-button action-delete" type="submit" value="Confirm Delete {{event}}"> or <a href="{% url 'event-detail' event.pk %}">cancel</a>
</p>
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Create Event</h1>
<section>
<form method="POST" action="{% url 'event-create' %}">
{% csrf_token %}
{{form.as_p}}
<p>
<input class="action-button" type="submit" value="Create Event"> or <a href="{% url 'profile-detail' %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Event</h1>
<section>
<div class="today__event">
<span>{{event.name}}
{% if event.employee %} for
<a href="{% url 'employee-detail' event.employee.pk %}">{{event.employee}}</a>
{% endif %}
</span>
<span class="today__time">{{event.time|time:"TIME_FORMAT"}}</span>
</div>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Update Event</h1>
<section>
<form method="POST" action="{% url 'event-update' event.pk %}">
{% csrf_token %}
{{form.as_p}}
<p>
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'profile-detail' %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Schedule</h1>
<p><a href="{% url 'event-create' %}" class="action-button">Add event</a></p>
<section>
{% for event in event_list %}
<div class="today__event">
<strong class="today__date">{{event.date|date:"D, M j"}}</strong>
<span>{{event.name}}
{% if event.employee %} for
<a href="{% url 'employee-detail' event.employee.pk %}">{{event.employee}}</a>
{% endif %}
</span>
<span class="today__time">{{event.time|time:"TIME_FORMAT"}}</span>
<span><a href="{% url 'event-update' event.pk %}">Edit</a></span>
</div>
{% empty %}
<p>There are no events yet.</p>
{% endfor %}
</section>
<section class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1">&laquo; first</a>
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
{% endif %}
</span>
</section>
</article>
{% endblock %}

View File

@ -3,10 +3,10 @@
{% block content %} {% block content %}
<article> <article>
<h1>Delete {{logentry}}</h1> <h1>Delete {{logentry}}</h1>
<form method="post" action="{% url 'logentry-delete' logentry.pk %}"> <form method="post" action="{% url 'entry-delete' employee.pk logentry.pk %}">
{% csrf_token %} {% csrf_token %}
<p> <p>
<input class="action-button action-delete" type="submit" value="Confirm Delete {{logentry}}"> or <a href="{% url 'logentry-detail' logentry.pk %}">cancel</a> <input class="action-button action-button--danger" type="submit" value="Confirm Delete"> or <a href="{% url 'employee-detail' employee.pk %}">cancel</a>
</p> </p>
</form> </form>
</article> </article>

View File

@ -2,9 +2,18 @@
{% block content %} {% block content %}
<article> <article>
<h1></h1> <header>
<h1>Update Event</h1>
<p><a class="action-button action-button--danger" href="{% url 'entry-delete' employee.pk logentry.pk %}">Delete this entry</a></p>
</header>
<section> <section>
<form method="POST" action="{% url 'entry-update' employee.pk logentry.pk %}">
{% csrf_token %}
{{form.as_p}}
<p>
<input class="action-button" type="submit" value="Save changes"> or <a href="{% url 'employee-detail' employee.pk %}">cancel</a>
</p>
</form>
</section> </section>
</article> </article>
{% endblock %} {% endblock %}

View File

@ -32,4 +32,12 @@ urlpatterns = [
])), ])),
])), ])),
path('events/', views.EventListView.as_view(), name='event-list'),
path('events/new/', views.EventCreateView.as_view(), name='event-create'),
path('events/<int:pk>/', include([
path('', views.EventDetailView.as_view(), name='event-detail'),
path('update/', views.EventUpdateView.as_view(), name='event-update'),
path('delete/', views.EventDeleteView.as_view(), name='event-delete'),
])),
] ]

View File

@ -7,14 +7,15 @@ from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q from django.db.models import Q
from .models import Employee, LogEntry, Todo from .models import Employee, LogEntry, Todo, Event
from .forms import ( from .forms import (
EmployeeForm, EmployeeForm,
EmployeePreCreateForm, EmployeePreCreateForm,
EmployeeArchiveForm, EmployeeArchiveForm,
LogEntryForm, LogEntryForm,
TodoForm, TodoForm,
TodoCreateForm TodoCreateForm,
EventForm,
) )
class SearchResultsView(ListView): class SearchResultsView(ListView):
@ -117,7 +118,14 @@ class LogEntryUpdateView(LoginRequiredMixin, UpdateView):
class LogEntryDeleteView(LoginRequiredMixin, DeleteView): class LogEntryDeleteView(LoginRequiredMixin, DeleteView):
model = LogEntry model = LogEntry
pk_url_kwarg = 'entry_pk' pk_url_kwarg = 'entry_pk'
success_url = reverse_lazy('entry-list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['employee'] = Employee.objects.get(pk=self.kwargs['pk'])
return context
def get_success_url(self):
return reverse('employee-detail', kwargs={'pk': self.kwargs['pk']})
# Todos # Todos
@ -171,3 +179,27 @@ class TodoDeleteView(LoginRequiredMixin, DeleteView):
class TodoDeleteDoneView(LoginRequiredMixin, TemplateView): class TodoDeleteDoneView(LoginRequiredMixin, TemplateView):
template_name = 'board/item_deleted.html' template_name = 'board/item_deleted.html'
# Events
class EventListView(LoginRequiredMixin, ListView):
model = Event
paginate_by = 100
ordering = ('-date', '-time')
class EventCreateView(LoginRequiredMixin, CreateView):
model = Event
template_name_suffix = '_create_form'
form_class = EventForm
success_url = reverse_lazy('profile-detail')
class EventDetailView(LoginRequiredMixin, DetailView):
model = Event
class EventUpdateView(LoginRequiredMixin, UpdateView):
model = Event
form_class = EventForm
class EventDeleteView(LoginRequiredMixin, DeleteView):
model = Event
success_url = reverse_lazy('profile-detail')

View File

@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)

50
onboard/celery.py Normal file
View File

@ -0,0 +1,50 @@
import os
from celery import Celery
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onboard.settings_dev')
app = Celery('onboard')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print(f'Request: {self.request!r}')
# app.conf.beat_schedule = {
# #Scheduler Name
# 'print-message-ten-seconds': {
# # Task Name (Name Specified in Decorator)
# 'task': 'print_msg_main',
# # Schedule
# 'schedule': 10.0,
# # Function Arguments
# 'args': ("Hello",)
# },
# #Scheduler Name
# 'print-time-twenty-seconds': {
# # Task Name (Name Specified in Decorator)
# 'task': 'print_time',
# # Schedule
# 'schedule': 20.0,
# },
# #Scheduler Name
# 'calculate-forty-seconds': {
# # Task Name (Name Specified in Decorator)
# 'task': 'get_calculation',
# # Schedule
# 'schedule': 40.0,
# # Function Arguments
# 'args': (10,20)
# },
# }

17
onboard/middleware.py Normal file
View File

@ -0,0 +1,17 @@
import pytz
from django.utils import timezone
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tzname = request.session.get('timezone')
if request.user.is_authenticated:
tzname = request.user.profile.timezone
if tzname:
timezone.activate(pytz.timezone(tzname))
else:
timezone.deactivate()
return self.get_response(request)

View File

@ -25,7 +25,7 @@ with open('/etc/secret_key.txt') as f:
SECRET_KEY = f.read().strip() SECRET_KEY = f.read().strip()
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False' DEBUG = False
ALLOWED_HOSTS = ['onboard.windmillapps.org', '127.0.0.1'] ALLOWED_HOSTS = ['onboard.windmillapps.org', '127.0.0.1']
@ -126,10 +126,11 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/ # https://docs.djangoproject.com/en/3.2/howto/static-files/
STATICFILES_DIRS = [
BASE_DIR / 'static'
]
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = [BASE_DIR / 'static/'] STATIC_ROOT = '/var/www/onboard.windmillapps.org/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
# Default primary key field type # Default primary key field type
@ -162,7 +163,7 @@ LOGGING = {
# Email # Email
ANYMAIL = { ANYMAIL = {
"MAILGUN_API_KEY": os.environ.get('MAILGUN_API_KEY'), "MAILGUN_API_KEY": "key-b65d1f9e486825a7d01d099fd3062c2b",
} }
SERVER_EMAIL = 'noreply@onboard.windmillapps.org' SERVER_EMAIL = 'noreply@onboard.windmillapps.org'
@ -173,10 +174,8 @@ ADMINS = (
) )
MANAGERS = ADMINS MANAGERS = ADMINS
EMAIL_BACKEND = os.environ.get('EMAIL_BACKEND') EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend'
SECURE_HSTS_SECONDS = True
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = os.environ.get('DJANGO_SECURE_HSTS_SECONDS', '') != 'False' SESSION_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = os.environ.get('DJANGO_SECURE_SSL_REDIRECT', '') != 'False' CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = os.environ.get('DJANGO_SESSION_COOKIE_SECURE', '') != 'False'
CSRF_COOKIE_SECURE = os.environ.get('DJANGO_CSRF_COOKIE_SECURE', '') != 'False'

145
onboard/settings_dev.py Normal file
View File

@ -0,0 +1,145 @@
"""
Django settings for onboard project.
Generated by 'django-admin startproject' using Django 3.2.5.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-fdq1(f%l*__obb*zvb3gqnlki!ryr@_1_5om(_2ju3mu70mi4%'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'xcarbon.lan']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.flatpages',
'django_celery_beat',
'accounts.apps.AccountsConfig',
'board.apps.BoardConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'onboard.middleware.TimezoneMiddleware',
]
ROOT_URLCONF = 'onboard.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'onboard.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Site ID
SITE_ID = 1
# Celery - prefix with CELERY_
CELERY_BROKER_URL = "redis://localhost:6379"
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_TASK_TRACK_STARTED = True

View File

@ -1,8 +1,9 @@
:root { :root {
--white: #fdfdff; --white: #f8f8fb;
--black: #393d3f; --black: #393d3f;
--grey: #a7b4bb; --grey: #a7b4bb;
--blue: #2288a2; --blue: #10638c;
--red: #8c1016;
} }
html { html {
@ -63,7 +64,7 @@ blockquote {
border-left: 2px solid var(--blue); border-left: 2px solid var(--blue);
} }
header { body > header {
padding: 1rem; padding: 1rem;
} }
@ -95,11 +96,14 @@ article {
.navbar__mobile { .navbar__mobile {
display: none; display: none;
font-weight: 900;
} }
.navbar__menu { .navbar__mobile .menu__items {
text-align: center;
} }
.navbar__menu_title { .navbar__menu_title {
font-weight: 900; font-weight: 900;
padding: 0.2rem 1rem; padding: 0.2rem 1rem;
@ -199,10 +203,16 @@ article {
text-decoration: none; text-decoration: none;
} }
.action-button--danger {
background-color: var(--red);
}
/* FORMS */ /* FORMS */
input[type=text]:not([name=description]), input[type=text]:not([name=description]),
input[type=number], input[type=number],
input[type=date],
input[type=time],
input[type=password], input[type=password],
input[type=search], input[type=search],
textarea, select { textarea, select {
@ -223,10 +233,6 @@ input[name=description] {
box-sizing: border-box; box-sizing: border-box;
} }
input:focus, textarea:focus, select:focus {
border-color: var(--blue);
}
input[type=radio], input[type=checkbox] { input[type=radio], input[type=checkbox] {
cursor: pointer; cursor: pointer;
width: 1.5rem; width: 1.5rem;
@ -256,6 +262,10 @@ li label {
display: inline-block; display: inline-block;
} }
input:focus, textarea:focus, select:focus {
border-color: var(--blue) !important;
}
@ -301,3 +311,35 @@ hgroup {
text-align: left; text-align: left;
} }
} }
.log_entry {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
column-gap: 0.5rem;
margin-bottom: 1rem;
}
.log_entry--employee {
grid-template-columns: 1fr auto;
}
.today__event {
display: grid;
grid-template-columns: 1fr 2fr 0.5fr 0.5fr;
column-gap: 1rem;
margin-bottom: 2rem;
padding-bottom: 0.5rem;
border-bottom: 0.046rem solid var(--grey);
}
.activity__item {
display: grid;
grid-template-columns: 0.25fr 2fr 0.25fr;
column-gap: 0.5rem;
margin-bottom: 1rem;
}

View File

@ -26,6 +26,7 @@
<input name="q" type="search" placeholder="Search..."> <input name="q" type="search" placeholder="Search...">
</form> </form>
<div class="nav__employees"> <div class="nav__employees">
<a href="{% url 'profile-detail' %}">Today</a>
<a href="{% url 'employee-list' %}">Employees</a> <a href="{% url 'employee-list' %}">Employees</a>
</div> </div>
<div class="nav__auth"> <div class="nav__auth">
@ -37,12 +38,13 @@
<a class="action-button" href="{% url 'logout' %}">Logout</a> <a class="action-button" href="{% url 'logout' %}">Logout</a>
</div> </div>
{% else %} {% else %}
<span>OnBoard</span>
<a class="nav__login" class="action-button" href="{% url 'login' %}">Login</a> <a class="nav__login" class="action-button" href="{% url 'login' %}">Login</a>
{% endif %} {% endif %}
</nav> </nav>
<nav class="navbar__mobile"> <nav class="navbar__mobile">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<details class="menu_"> <details class="menu">
<summary class="menu__title">Menu</summary> <summary class="menu__title">Menu</summary>
<dl class="menu__items"> <dl class="menu__items">
<dt> <dt>
@ -51,12 +53,15 @@
</form> </form>
</dt> </dt>
<br> <br>
<dt><a href="{% url 'profile-detail' %}">Today</a></dt>
<br>
<dt><a href="{% url 'employee-list' %}">Employees</a></dt> <dt><a href="{% url 'employee-list' %}">Employees</a></dt>
<br> <br>
<dt><a class="action-button" href="{% url 'logout' %}">Logout</a></dt> <dt><a class="action-button" href="{% url 'logout' %}">Logout</a></dt>
</dl> </dl>
</details> </details>
{% else %} {% else %}
<span>OnBoard</span>
<a class="nav__login" class="action-button" href="{% url 'login' %}">Login</a> <a class="nav__login" class="action-button" href="{% url 'login' %}">Login</a>
{% endif %} {% endif %}
</nav> </nav>