Merge branch 'release/1.1.0'

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

View File

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

132
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "88b6a9fdda6471bfc16c42a906945bb034d4b2d576f31179a72471296eac58cd"
"sha256": "ffe96f34ca738a35a283e9ac98b5f1a74609c67619f18e8eb7ba62967247766f"
},
"pipfile-spec": 6,
"requires": {
@ -16,6 +16,14 @@
]
},
"default": {
"amqp": {
"hashes": [
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
],
"markers": "python_version >= '3.6'",
"version": "==5.0.6"
},
"asgiref": {
"hashes": [
"sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9",
@ -24,6 +32,24 @@
"markers": "python_version >= '3.6'",
"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": {
"hashes": [
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
@ -39,6 +65,34 @@
"markers": "python_version >= '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": {
"hashes": [
"sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd",
@ -58,6 +112,22 @@
"index": "pypi",
"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": {
"hashes": [
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
@ -66,6 +136,36 @@
"markers": "python_version >= '3'",
"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": {
"hashes": [
"sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
@ -73,6 +173,13 @@
],
"version": "==2021.1"
},
"redis": {
"hashes": [
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
],
"version": "==3.5.3"
},
"requests": {
"hashes": [
"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'",
"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": {
"hashes": [
"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'",
"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": {}

View File

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

View File

@ -6,5 +6,63 @@
<header>
<h1>Hi, {{user.first_name}}</h1>
</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>
{% endblock %}

View File

@ -1,5 +1,6 @@
import pytz
from django.utils import timezone
from datetime import date, timedelta
from django.shortcuts import render, reverse, redirect
from django.urls import reverse_lazy
from django.views.generic.base import TemplateView
@ -12,14 +13,25 @@ from django.contrib.auth.models import User
from .models import Profile
from .forms import AccountUpdateForm, ProfileUpdateForm
from board.models import LogEntry, Event
class ProfileView(LoginRequiredMixin, TemplateView):
template_name = 'accounts/profile.html'
def get_context_data(self, **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['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
class ProfileUpdateView(LoginRequiredMixin, UpdateView):

View File

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

View File

@ -1,6 +1,7 @@
from datetime import datetime
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
class EmployeePreCreateForm(forms.Form):
@ -89,3 +90,25 @@ class TodoCreateForm(forms.ModelForm):
'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):
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>
<blockquote>{{employee.initial_comments|linebreaksbr}}</blockquote>
</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">
<h3>To-do's</h3>
{% include "board/todo_list.html" with todo_list=employee.todo_set.all %}
@ -45,11 +56,16 @@
<p>
<a class="action-button" href="{% url 'entry-create' employee.pk %}">Add an Entry</a>
</p>
{% for entry in employee.logentry_set.all %}
<p>
<span>{{entry.created_at|date:"SHORT_DATE_FORMAT"}}&mdash;</span>
<span>{{entry.notes}}</span>
</p>
{% regroup employee.logentry_set.all by created_at.date as activity %}
{% for date in activity %}
<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><a href="{% url 'entry-update' employee.pk entry.pk %}">Edit</a></span>
</p>
{% endfor %}
{% endfor %}
</section>
</article>

View File

@ -8,7 +8,8 @@
{% for employee in employee_list %}
<p>
<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>
{% endfor %}
</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 %}
<article>
<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 %}
<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>
</form>
</article>

View File

@ -2,9 +2,18 @@
{% block content %}
<article>
<h1></h1>
<section>
</section>
</article>
<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>
<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>
</article>
{% 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.db.models import Q
from .models import Employee, LogEntry, Todo
from .models import Employee, LogEntry, Todo, Event
from .forms import (
EmployeeForm,
EmployeePreCreateForm,
EmployeeArchiveForm,
LogEntryForm,
TodoForm,
TodoCreateForm
TodoCreateForm,
EventForm,
)
class SearchResultsView(ListView):
@ -117,7 +118,14 @@ class LogEntryUpdateView(LoginRequiredMixin, UpdateView):
class LogEntryDeleteView(LoginRequiredMixin, DeleteView):
model = LogEntry
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
@ -170,4 +178,28 @@ class TodoDeleteView(LoginRequiredMixin, DeleteView):
return reverse('todo-deleted', kwargs={'pk': self.kwargs['pk']})
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()
# 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']
@ -126,10 +126,11 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATICFILES_DIRS = [
BASE_DIR / 'static'
]
STATIC_URL = '/static/'
STATIC_ROOT = [BASE_DIR / 'static/']
STATICFILES_DIRS = [BASE_DIR / 'static']
STATIC_ROOT = '/var/www/onboard.windmillapps.org/static/'
# Default primary key field type
@ -162,7 +163,7 @@ LOGGING = {
# Email
ANYMAIL = {
"MAILGUN_API_KEY": os.environ.get('MAILGUN_API_KEY'),
"MAILGUN_API_KEY": "key-b65d1f9e486825a7d01d099fd3062c2b",
}
SERVER_EMAIL = 'noreply@onboard.windmillapps.org'
@ -173,10 +174,8 @@ ADMINS = (
)
MANAGERS = ADMINS
EMAIL_BACKEND = os.environ.get('EMAIL_BACKEND')
SECURE_HSTS_SECONDS = os.environ.get('DJANGO_SECURE_HSTS_SECONDS', '') != 'False'
SECURE_SSL_REDIRECT = os.environ.get('DJANGO_SECURE_SSL_REDIRECT', '') != 'False'
SESSION_COOKIE_SECURE = os.environ.get('DJANGO_SESSION_COOKIE_SECURE', '') != 'False'
CSRF_COOKIE_SECURE = os.environ.get('DJANGO_CSRF_COOKIE_SECURE', '') != 'False'
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend'
SECURE_HSTS_SECONDS = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

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 {
--white: #fdfdff;
--white: #f8f8fb;
--black: #393d3f;
--grey: #a7b4bb;
--blue: #2288a2;
--blue: #10638c;
--red: #8c1016;
}
html {
@ -63,7 +64,7 @@ blockquote {
border-left: 2px solid var(--blue);
}
header {
body > header {
padding: 1rem;
}
@ -95,11 +96,14 @@ article {
.navbar__mobile {
display: none;
font-weight: 900;
}
.navbar__menu {
.navbar__mobile .menu__items {
text-align: center;
}
.navbar__menu_title {
font-weight: 900;
padding: 0.2rem 1rem;
@ -199,10 +203,16 @@ article {
text-decoration: none;
}
.action-button--danger {
background-color: var(--red);
}
/* FORMS */
input[type=text]:not([name=description]),
input[type=number],
input[type=date],
input[type=time],
input[type=password],
input[type=search],
textarea, select {
@ -223,10 +233,6 @@ input[name=description] {
box-sizing: border-box;
}
input:focus, textarea:focus, select:focus {
border-color: var(--blue);
}
input[type=radio], input[type=checkbox] {
cursor: pointer;
width: 1.5rem;
@ -256,6 +262,10 @@ li label {
display: inline-block;
}
input:focus, textarea:focus, select:focus {
border-color: var(--blue) !important;
}
@ -300,4 +310,36 @@ hgroup {
right: 0;
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...">
</form>
<div class="nav__employees">
<a href="{% url 'profile-detail' %}">Today</a>
<a href="{% url 'employee-list' %}">Employees</a>
</div>
<div class="nav__auth">
@ -37,12 +38,13 @@
<a class="action-button" href="{% url 'logout' %}">Logout</a>
</div>
{% else %}
<span>OnBoard</span>
<a class="nav__login" class="action-button" href="{% url 'login' %}">Login</a>
{% endif %}
</nav>
<nav class="navbar__mobile">
{% if user.is_authenticated %}
<details class="menu_">
<details class="menu">
<summary class="menu__title">Menu</summary>
<dl class="menu__items">
<dt>
@ -51,12 +53,15 @@
</form>
</dt>
<br>
<dt><a href="{% url 'profile-detail' %}">Today</a></dt>
<br>
<dt><a href="{% url 'employee-list' %}">Employees</a></dt>
<br>
<dt><a class="action-button" href="{% url 'logout' %}">Logout</a></dt>
</dl>
</details>
{% else %}
<span>OnBoard</span>
<a class="nav__login" class="action-button" href="{% url 'login' %}">Login</a>
{% endif %}
</nav>