Add basic events

This commit is contained in:
Nathan Chapman 2021-07-27 09:48:58 -06:00
parent 608498b296
commit fa98be63fa
21 changed files with 405 additions and 23 deletions

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,59 @@
<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</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">
{{entry.notes}}
<em>for</em>
<a href="{% url 'employee-detail' entry.employee.pk %}">{{entry.employee}}</a>
</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,11 +56,14 @@
<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>
<span>{{entry.notes}}</span> {% for entry in date.list %}
</p> <p class="activity__item">
<span>{{entry.created_at|time:"H:i"}}</span>
<span>{{entry.notes}}</span></p>
{% endfor %}
{% endfor %} {% endfor %}
</section> </section>
</article> </article>

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 'event-list' %}">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 'event-list' %}">cancel</a>
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% 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>
</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.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):
@ -171,3 +172,26 @@ 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
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')

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

@ -53,6 +53,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'onboard.middleware.TimezoneMiddleware',
] ]
ROOT_URLCONF = 'onboard.urls' ROOT_URLCONF = 'onboard.urls'

View File

@ -25,7 +25,7 @@ SECRET_KEY = 'django-insecure-fdq1(f%l*__obb*zvb3gqnlki!ryr@_1_5om(_2ju3mu70mi4%
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'xcarbon.lan']
# Application definition # Application definition
@ -51,6 +51,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'onboard.middleware.TimezoneMiddleware',
] ]
ROOT_URLCONF = 'onboard.urls' ROOT_URLCONF = 'onboard.urls'

View File

@ -2,7 +2,7 @@
--white: #fdfdff; --white: #fdfdff;
--black: #393d3f; --black: #393d3f;
--grey: #a7b4bb; --grey: #a7b4bb;
--blue: #2288a2; --blue: #10638c;
} }
html { html {
@ -63,7 +63,7 @@ blockquote {
border-left: 2px solid var(--blue); border-left: 2px solid var(--blue);
} }
header { body > header {
padding: 1rem; padding: 1rem;
} }
@ -95,6 +95,7 @@ article {
.navbar__mobile { .navbar__mobile {
display: none; display: none;
font-weight: 900;
} }
.navbar__menu { .navbar__menu {
@ -203,6 +204,8 @@ article {
/* 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 +226,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 +255,10 @@ li label {
display: inline-block; display: inline-block;
} }
input:focus, textarea:focus, select:focus {
border-color: var(--blue) !important;
}
@ -301,3 +304,26 @@ 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);
}

View File

@ -26,6 +26,8 @@
<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 'event-list' %}">Events</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,6 +39,7 @@
<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>
@ -51,12 +54,17 @@
</form> </form>
</dt> </dt>
<br> <br>
<dt><a href="{% url 'profile-detail' %}">Today</a></dt>
<br>
<dt><a href="{% url 'event-list' %}">Events</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>