Add... everything

This commit is contained in:
Nathan Chapman 2021-07-21 20:58:18 -06:00
parent 60b30d7d6c
commit b89269d960
60 changed files with 2053 additions and 38 deletions

0
accounts/__init__.py Executable file
View File

4
accounts/admin.py Executable file
View File

@ -0,0 +1,4 @@
from django.contrib import admin
from .models import Profile
admin.site.register(Profile)

9
accounts/apps.py Executable file
View File

@ -0,0 +1,9 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
def ready(self):
from .signals import create_profile

20
accounts/forms.py Normal file
View File

@ -0,0 +1,20 @@
from django import forms
from django.contrib.auth.models import User
from .models import Profile
class AccountUpdateForm(forms.ModelForm):
class Meta:
model = User
fields = [
'first_name',
'last_name',
'email',
]
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = Profile
fields = (
'timezone',
)

View File

@ -0,0 +1,27 @@
# Generated by Django 3.2.5 on 2021-07-20 19:46
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timezone', models.CharField(choices=[('US/Alaska', 'US/Alaska'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific')], default='US/Mountain', max_length=50)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

31
accounts/models.py Executable file
View File

@ -0,0 +1,31 @@
from django.db import models
from django.urls import reverse
from django.contrib.auth.models import User
class Profile(models.Model):
TIMEZONE_CHOICES = [
('US/Alaska', 'US/Alaska'),
('US/Arizona', 'US/Arizona'),
('US/Central', 'US/Central'),
('US/Eastern', 'US/Eastern'),
('US/Hawaii', 'US/Hawaii'),
('US/Mountain', 'US/Mountain'),
('US/Pacific', 'US/Pacific'),
]
user = models.OneToOneField(User, on_delete=models.CASCADE)
timezone = models.CharField(
max_length=50,
choices = TIMEZONE_CHOICES,
default='US/Mountain'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def get_absolute_url(self):
return reverse('profile-detail', kwargs={'pk': self.pk})
def __str__(self):
return f'{self.user.first_name} {self.user.last_name}'

11
accounts/signals.py Executable file
View File

@ -0,0 +1,11 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import Profile
@receiver(post_save, sender=User, dispatch_uid="user_profile_signal")
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
else:
instance.profile.save()

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% block content %}
<section class="panel">
<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>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<section>
<h1>Update Profile</h1>
<p><a href="{% url 'password_change' %}">Change password</a></p>
<form method="post" action="{% url 'account-update' user.id %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save changes" class="action-button"> or
<a href="{% url 'account-detail' user.id %}">Cancel</a>
</form>
</section>
{% include "accounts/profile_form.html" with form=profile_form %}
</article>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<article class="panel">
<header>
<h1>Hi, {{user.first_name}}</h1>
</header>
</article>
{% endblock %}

View File

@ -0,0 +1,10 @@
<section>
<h2>Settings</h2>
<form action="{% url 'profile-update' user.profile.pk %}" method="POST">
{% csrf_token %}
{{form.as_p}}
<p>
<input type="submit" value="Save changes" class="action-button">
</p>
</form>
</section>

3
accounts/tests.py Executable file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Write your tests

12
accounts/urls.py Executable file
View File

@ -0,0 +1,12 @@
from django.urls import path, include
from . import views
urlpatterns = [
path('profile/', views.ProfileView.as_view(), name='profile-detail'),
path('profile/<int:pk>/update/', views.ProfileUpdateView.as_view(), name='profile-update'),
path('<int:pk>/', include([
path('', views.AccountDetailView.as_view(), name='account-detail'),
path('update/', views.AccountUpdateView.as_view(), name='account-update'),
path('delete/', views.AccountDeleteView.as_view(), name='account-delete'),
])),
]

58
accounts/views.py Executable file
View File

@ -0,0 +1,58 @@
import pytz
from django.utils import timezone
from django.shortcuts import render, reverse, redirect
from django.urls import reverse_lazy
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from .models import Profile
from .forms import AccountUpdateForm, ProfileUpdateForm
class ProfileView(LoginRequiredMixin, TemplateView):
template_name = 'accounts/profile.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['profile'] = self.request.user.profile
return context
class ProfileUpdateView(LoginRequiredMixin, UpdateView):
model = Profile
form_class = ProfileUpdateForm
def get_success_url(self):
return reverse('account-update', kwargs={'pk': self.request.user.pk})
class AccountDetailView(LoginRequiredMixin, DetailView):
model = User
template_name = 'accounts/account_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
class AccountUpdateView(LoginRequiredMixin, UpdateView):
model = User
form_class = AccountUpdateForm
template_name = 'accounts/account_form.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['profile_form'] = ProfileUpdateForm(instance=self.object.profile)
context['timezones'] = pytz.common_timezones
return context
def get_success_url(self):
pk = self.kwargs["pk"]
return reverse('account-detail', kwargs={'pk': pk})
class AccountDeleteView(LoginRequiredMixin, DeleteView):
model = User
success_url = reverse_lazy('account-list')

View File

@ -6,4 +6,4 @@ class BoardConfig(AppConfig):
name = 'board'
def ready(self):
from .signals import create_employee
from .signals import create_employee, complete_todo

91
board/forms.py Normal file
View File

@ -0,0 +1,91 @@
from datetime import datetime
from django import forms
from .models import Employee, LogEntry, Todo
from .regex import process_regex
class EmployeePreCreateForm(forms.Form):
initial_data = forms.CharField(widget=forms.Textarea(attrs = {
'autofocus': True
}))
def filter_onboarding_email(self):
data = process_regex(self.cleaned_data['initial_data'])
try:
hire_date = datetime.strptime(data['hire_date'], '%m/%d/%Y').date()
except ValueError as e:
hire_date = datetime.now().date()
initial_comments = data['initial_comments'] + '\nIT Requests: ' + data['it_requests']
employee = Employee.objects.create(
first_name = data['first_name'],
last_name = data['last_name'],
hire_date = hire_date,
title = data['title'],
department = data['department'],
manager = data['manager'],
initial_comments = initial_comments,
)
return employee
class EmployeeForm(forms.ModelForm):
class Meta:
model = Employee
fields = (
'first_name',
'last_name',
'hire_date',
'title',
'department',
'manager',
'initial_comments',
)
widgets = {
'first_name': forms.TextInput(attrs = {
'autofocus': 'autofocus'
})
}
class EmployeeArchiveForm(forms.ModelForm):
class Meta:
model = Employee
fields = ('archived',)
class LogEntryForm(forms.ModelForm):
class Meta:
model = LogEntry
fields = ('notes',)
widgets = {
'notes': forms.TextInput(attrs = {
'autofocus': 'autofocus'
})
}
class TodoForm(forms.ModelForm):
class Meta:
model = Todo
fields = ('completed', 'description')
widgets = {
'completed': forms.CheckboxInput(attrs = {
'class': 'todo__checkbox',
}),
'description': forms.TextInput(attrs = {
'autofocus': 'autofocus'
})
}
class TodoCreateForm(forms.ModelForm):
class Meta:
model = Todo
fields = ('description',)
widgets = {
'description': forms.TextInput(attrs = {
'autofocus': 'autofocus'
})
}

View File

@ -1,53 +1,59 @@
from django.db import models
from django.urls import reverse_lazy, reverse
from django.utils import timezone
class Employee(models.Model):
first_name = models.CharField(max_length=64)
last_name = models.CharField(max_length=64)
hire_date = models.DateField()
title = models.CharField(max_length=64, blank=True, null=True)
department = models.CharField(max_length=64, blank=True, null=True)
manager = models.CharField(max_length=64, blank=True, null=True)
initial_comments = models.TextField(blank=True, null=True)
archived = models.BooleanField(default=False)
class Meta:
ordering = ('-hire_date',)
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
first_name = models.CharField(max_length=64)
last_name = models.CharField(max_length=64)
hire_date = models.DateField()
title = models.CharField(max_length=64, blank=True, null=True)
department = models.CharField(max_length=64, blank=True, null=True)
manager = models.CharField(max_length=64, blank=True, null=True)
initial_comments = models.TextField(blank=True, null=True)
archived = models.BooleanField(default=False)
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
def __str__(self):
return self.full_name
def __str__(self):
return self.full_name
def get_absolute_url(self):
return reverse('employee-detail', kwargs={'pk': self.pk})
class LogEntry(models.Model):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
notes = models.CharField(max_length=500)
class Meta:
ordering = ('-created_at',)
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
notes = models.CharField(max_length=500)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.employee}: {self.notes}"
def __str__(self):
return f"{self.employee}: {self.notes}"
def get_absolute_url(self):
return reverse('todo-detail', kwargs={'pk': self.employee.pk, 'entry_pk': self.pk})
class Todo(models.Model):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
description = models.CharField(max_length=64)
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
description = models.CharField(max_length=64)
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(blank=True, null=True)
completed = models.BooleanField(default=False)
completed_at = models.DateTimeField(blank=True, null=True)
def complete(self):
LogEntry.objects.create(
employee=self.checklist.employee,
notes=f'Completed To-do: "{self.description}"'
)
self.object.completed = True
self.object.completed_at = timezone.now()
self.object.save()
def __str__(self):
return f"{self.employee}: {self.description}"
def __str__(self):
return f"{self.employee}: {self.description}"
def get_absolute_url(self):
return reverse('todo-detail', kwargs={'pk': self.employee.pk, 'todo_pk': self.pk})

24
board/regex.py Normal file
View File

@ -0,0 +1,24 @@
import re
expressions = {
'first_name': "(?<=first\sname:\s)(.*)",
'last_name': "(?<=last\sname:\s)(.*)",
'hire_date': "(?!Hire\sDate:\s)((1[0-2])|([1-9]))/(([1-2][0-9])|([1-9])|(3[0-1]))/[0-9]{4}",
'title': "(?<=title:\s)(.*)",
'department': "(?<=department:\s)(.*)",
'manager': "(?<=manager:\s)(.*)",
'initial_comments': "(?<=comments:\s)(.*)",
'it_requests': "(?<=it\srequests:\s)(.*)",
}
def process_regex(initial_data):
data = {}
for key, value in expressions.items():
try:
data[key] = re.search(value, initial_data, re.IGNORECASE).group()
except AttributeError:
data[key] = 'None'
return data

View File

@ -1,3 +1,4 @@
from django.utils import timezone
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import (
@ -21,3 +22,20 @@ def create_employee(sender, instance, created, **kwargs):
employee=instance,
notes=f"Created {instance.full_name}"
)
@receiver(post_save, sender=Todo, dispatch_uid="todo_completed_signal")
def complete_todo(sender, instance, created, **kwargs):
if instance.completed and not instance.completed_at:
LogEntry.objects.create(
employee=instance.employee,
notes=f'Completed To-do: "{instance.description}"'
)
instance.completed_at = timezone.now()
instance.save()
elif instance.completed_at and not instance.completed:
LogEntry.objects.create(
employee=instance.employee,
notes=f'Re-opened To-do: "{instance.description}"'
)
instance.completed_at = None
instance.save()

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Archive {{object}}</h1>
<section>
<form method="POST" action="{% url 'employee-archive' employee.pk %}">
{% csrf_token %}
<label>
Archive?
{{form.archived}}
</label>
<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

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Archived Employees</h1>
<section>
{% 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>
</p>
{% endfor %}
</section>
<section>
<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

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

View File

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

View File

@ -0,0 +1,56 @@
{% extends "base.html" %}
{% load static %}
{% block head %}
<script type="module" defer src="{% static "scripts/index.js" %}"></script>
{% endblock %}
{% block content %}
<article>
{% if employee.archived %}
<p class="--archived"><em>This employee is currently Archived</em></p>
{% endif %}
<hgroup>
<h1>{{employee}}</h1>
<h2>Hire Date: {{employee.hire_date}}</h2>
<details class="menu">
<summary class="menu__title">Options</summary>
<dl class="menu__items">
<dt><a href="{% url 'employee-update' employee.pk %}">Update Employee details</a></dt>
<dt><a href="{% url 'employee-archive' employee.pk %}">Archive Employee</a></dt>
<dt><a href="{% url 'employee-delete' employee.pk %}">Delete Employee</a></dt>
</dl>
</details>
</hgroup>
<section>
<p>
<strong>Title</strong>: {{employee.title}}</br>
<strong>Department</strong>: {{employee.department}}</br>
<strong>Manager</strong>: {{employee.manager}}
</p>
<p><strong>Initial comments</strong>:<p>
<blockquote>{{employee.initial_comments|linebreaksbr}}</blockquote>
</section>
<section id="todos">
<h3>To-do's</h3>
{% include "board/todo_list.html" with todo_list=employee.todo_set.all %}
{% if not employee.archived %}
<p>
<a class="action-button" href="{% url 'todo-create' employee.pk %}">Add a to-do</a>
</p>
{% endif %}
</section>
<section>
<h3>Log</h3>
<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>
{% endfor %}
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Update Employee</h1>
<section>
<form method="POST" action="{% url 'employee-update' employee.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

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Employees</h1>
<p><a href="{% url 'employee-pre' %}" class="action-button">Add employee from email</a> or <a href="{% url 'employee-create' %}">Add employee from scratch</a></p>
<section>
{% 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>
</p>
{% endfor %}
</section>
<section>
<a href="{% url 'employee-archived' %}">Archived Employees&hellip;</a>
</section>
</article>
{% endblock %}

View File

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

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Search Results</h1>
<section>
{% for employee in employee_list %}
<p>
<a href="{% url 'employee-detail' employee.pk %}"><strong>{{employee.full_name}}</strong></a><br>
{% if employee.archived %}
<span class="--archived">Archived</span>
{% endif %}
<small>Hire date: {{employee.hire_date|date:"SHORT_DATE_FORMAT"}}</small>
</p>
{% empty %}
<p>Query did not contain any results.</p>
{% endfor %}
</section>
</article>
{% endblock %}

View File

@ -0,0 +1 @@
<li>Item deleted.</li>

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Delete {{logentry}}</h1>
<form method="post" action="{% url 'logentry-delete' 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>
</p>
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Add note to {{employee}}</h1>
<section>
<form method="POST" action="{% url 'entry-create' employee.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

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1></h1>
<section>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1></h1>
<section>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1></h1>
<section>
</section>
</article>
{% endblock %}

View File

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

View File

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

View File

@ -0,0 +1,13 @@
<li class="todo__item">
<form class="todo_form" action="{% url 'todo-update' employee.pk todo.pk %}" method="POST">
<label class="todo__details">
<input class="todo__checkbox" name="completed" type="checkbox" {% if todo.completed %}checked{% endif %} {% if employee.archived %}disabled{% endif %}>
<input name="description" type="hidden" value="{{todo.description}}">
<span class="todo__description_display">{{todo.description}}</span>
</label>
{% if not employee.archived %}
<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>
{% endif %}
</form>
</li>

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block content %}
<article>
<h1>Update To-do</h1>
<section>
<form method="POST" action="{% url 'todo-update' employee.pk todo.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

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

35
board/urls.py Normal file
View File

@ -0,0 +1,35 @@
from django.urls import path, include
from . import views
urlpatterns = [
path('search/', views.SearchResultsView.as_view(), name='search-results'),
path('employees/', views.EmployeeListView.as_view(), name='employee-list'),
path('employees/archived/', views.EmployeeArchivedView.as_view(), name='employee-archived'),
path('employees/pre/', views.EmployeePreCreateView.as_view(), name='employee-pre'),
path('employees/new/', views.EmployeeCreateView.as_view(), name='employee-create'),
path('employees/<int:pk>/', include([
path('', views.EmployeeDetailView.as_view(), name='employee-detail'),
path('update/', views.EmployeeUpdateView.as_view(), name='employee-update'),
path('delete/', views.EmployeeDeleteView.as_view(), name='employee-delete'),
path('archive/', views.EmployeeArchiveView.as_view(), name='employee-archive'),
path('entries/', views.LogEntryListView.as_view(), name='entry-list'),
path('entries/new/', views.LogEntryCreateView.as_view(), name='entry-create'),
path('entries/<int:entry_pk>/', include([
path('', views.LogEntryDetailView.as_view(), name='entry-detail'),
path('update/', views.LogEntryUpdateView.as_view(), name='entry-update'),
path('delete/', views.LogEntryDeleteView.as_view(), name='entry-delete'),
])),
path('todos/', views.TodoListView.as_view(), name='todo-list'),
path('todos/new/', views.TodoCreateView.as_view(), name='todo-create'),
path('todos/deleted/', views.TodoDeleteDoneView.as_view(), name='todo-deleted'),
path('todos/<int:todo_pk>/', include([
path('', views.TodoDetailView.as_view(), name='todo-detail'),
path('update/', views.TodoUpdateView.as_view(), name='todo-update'),
path('delete/', views.TodoDeleteView.as_view(), name='todo-delete'),
])),
])),
]

View File

@ -1,3 +1,173 @@
from django.shortcuts import render
from django.urls import reverse_lazy, reverse
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
# Create your views here.
from .models import Employee, LogEntry, Todo
from .forms import (
EmployeeForm,
EmployeePreCreateForm,
EmployeeArchiveForm,
LogEntryForm,
TodoForm,
TodoCreateForm
)
class SearchResultsView(ListView):
model = Employee
template_name_suffix = '_search_results'
def get_queryset(self):
query = self.request.GET.get('q')
object_list = Employee.objects.filter(
Q(first_name__icontains=query) | Q(last_name__icontains=query)
)
return object_list
# Employees
class EmployeeListView(LoginRequiredMixin, ListView):
model = Employee
queryset = Employee.objects.filter(archived=False).order_by('hire_date')
class EmployeeArchivedView(LoginRequiredMixin, ListView):
model = Employee
paginate_by = 100
template_name_suffix = '_archived'
queryset = Employee.objects.filter(archived=True)
class EmployeePreCreateView(LoginRequiredMixin, FormView):
template_name = 'board/employee_pre_form.html'
form_class = EmployeePreCreateForm
def form_valid(self, form):
form.filter_onboarding_email()
return super().form_valid(form)
def get_success_url(self):
return reverse('employee-list')
class EmployeeCreateView(LoginRequiredMixin, CreateView):
model = Employee
template_name_suffix = '_create_form'
form_class = EmployeeForm
class EmployeeDetailView(LoginRequiredMixin, DetailView):
model = Employee
class EmployeeUpdateView(LoginRequiredMixin, UpdateView):
model = Employee
form_class = EmployeeForm
success_url = reverse_lazy('employee-list')
class EmployeeArchiveView(LoginRequiredMixin, UpdateView):
model = Employee
form_class = EmployeeArchiveForm
template_name_suffix = '_archive_form'
success_url = reverse_lazy('employee-list')
class EmployeeDeleteView(LoginRequiredMixin, DeleteView):
model = Employee
success_url = reverse_lazy('employee-list')
# LogEntries
class LogEntryListView(LoginRequiredMixin, ListView):
model = LogEntry
pk_url_kwarg = 'entry_pk'
class LogEntryCreateView(LoginRequiredMixin, CreateView):
model = LogEntry
pk_url_kwarg = 'entry_pk'
template_name_suffix = '_create_form'
form_class = LogEntryForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['employee'] = Employee.objects.get(pk=self.kwargs['pk'])
return context
def form_valid(self, form):
form.instance.employee = Employee.objects.get(pk=self.kwargs['pk'])
return super().form_valid(form)
def get_success_url(self):
return reverse('employee-detail', kwargs={'pk': self.kwargs['pk']})
class LogEntryDetailView(LoginRequiredMixin, DetailView):
model = LogEntry
pk_url_kwarg = 'entry_pk'
class LogEntryUpdateView(LoginRequiredMixin, UpdateView):
model = LogEntry
pk_url_kwarg = 'entry_pk'
form_class = LogEntryForm
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']})
class LogEntryDeleteView(LoginRequiredMixin, DeleteView):
model = LogEntry
pk_url_kwarg = 'entry_pk'
success_url = reverse_lazy('entry-list')
# Todos
class TodoListView(LoginRequiredMixin, ListView):
model = Todo
pk_url_kwarg = 'todo_pk'
class TodoCreateView(LoginRequiredMixin, CreateView):
model = Todo
pk_url_kwarg = 'todo_pk'
template_name_suffix = '_create_form'
form_class = TodoCreateForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['employee'] = Employee.objects.get(pk=self.kwargs['pk'])
return context
def form_valid(self, form):
form.instance.employee = Employee.objects.get(pk=self.kwargs['pk'])
return super().form_valid(form)
def get_success_url(self):
return reverse('employee-detail', kwargs={'pk': self.kwargs['pk']})
class TodoDetailView(LoginRequiredMixin, DetailView):
model = Todo
pk_url_kwarg = 'todo_pk'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['employee'] = Employee.objects.get(pk=self.kwargs['pk'])
return context
class TodoUpdateView(LoginRequiredMixin, UpdateView):
model = Todo
pk_url_kwarg = 'todo_pk'
form_class = TodoForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['employee'] = Employee.objects.get(pk=self.kwargs['pk'])
return context
class TodoDeleteView(LoginRequiredMixin, DeleteView):
model = Todo
pk_url_kwarg = 'todo_pk'
def get_success_url(self):
return reverse('todo-deleted', kwargs={'pk': self.kwargs['pk']})
class TodoDeleteDoneView(LoginRequiredMixin, TemplateView):
template_name = 'board/item_deleted.html'

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!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['localhost', 'xcarbon.lan']
# Application definition
@ -37,6 +37,9 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.flatpages',
'accounts.apps.AccountsConfig',
'board.apps.BoardConfig',
]
@ -55,7 +58,7 @@ ROOT_URLCONF = 'onboard.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -119,8 +122,13 @@ USE_TZ = True
# 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

View File

@ -14,8 +14,11 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.urls import path, include
urlpatterns = [
path('board/', include('board.urls')),
path('accounts/', include('accounts.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('admin/', admin.site.urls),
]

155
static/scripts/form.js Normal file
View File

@ -0,0 +1,155 @@
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

@ -0,0 +1,16 @@
export default function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(
cookie.substring(name.length + 1)
);
break;
}
}
}
return cookieValue;
}

11
static/scripts/index.js Normal file
View File

@ -0,0 +1,11 @@
import Form from "./form.js";
import View from "./view.js";
// constructor(element, forms, templateName, addButton, destination)
const todoListView = new View(
document.querySelector("#todos"),
".todo_form",
"#todo__list"
)

52
static/scripts/view.js Normal file
View File

@ -0,0 +1,52 @@
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);
}
}

303
static/styles/main.css Normal file
View File

@ -0,0 +1,303 @@
:root {
--white: #fdfdff;
--black: #393d3f;
--grey: #a7b4bb;
--blue: #2288a2;
}
html {
font-size: 137.5%; /*22px*/
}
@media (max-width: 600px) {
html {
font-size: 112.5%; /* 18px */
}
}
body {
background: white;
font-family: 'IBM Plex Sans', sans-serif;
font-weight: 400;
line-height: 1.75;
color: #000000;
}
p {
margin-bottom: 1rem;
}
h1, h2, h3, h4, h5 {
margin: 3rem 0 1.38rem;
font-family: 'IBM Plex Sans', sans-serif;
font-weight: 700;
line-height: 1.3;
}
h1 {
margin-top: 0;
font-size: 1.802rem;
}
h2 {
font-size: 1.602rem;
}
h1 + h2 {
margin-top: 0;
}
h3 {
font-size: 1.424rem;
}
h4 {
font-size: 1.266rem;
}
h5 {
font-size: 1.125rem;
}
small {
font-size: 0.889rem;
}
a {
color: var(--blue);
white-space: nowrap;
}
blockquote {
margin-left: 0;
padding-left: 2rem;
border-left: 2px solid var(--blue);
}
header {
padding: 1rem;
}
article {
max-width: 64rem;
margin: 0 auto 2rem;
padding: 1rem;
border: 0.2rem solid var(--grey);
}
@media all and (max-width: 64rem) {
article {
border-right: none;
border-left: none;
}
}
.navbar__desktop {
display: flex;
justify-content: space-between;
align-items: baseline;
font-weight: 700;
}
.navbar__mobile {
display: none;
}
.navbar__menu {
}
.navbar__menu_title {
font-weight: 900;
padding: 0.2rem 1rem;
border: none;
background-color: var(--blue);
color: white;
cursor: pointer;
text-decoration: none;
}
.navbar__menu_items {
position: absolute;
z-index: 3;
background-color: var(--white);
padding: 1rem;
margin: 0;
border: 1px solid #bdc3c7;
box-shadow: 3px 3px 3px #bdc3c7;
right: 0;
}
@media (max-width: 900px) {
.navbar__desktop {
display: none;
}
.navbar__mobile {
display: block;
}
}
.navbar div a:not(:last-child) {
margin-right: 1rem;
}
#todo__list {
list-style-type: none;
padding-left: 0;
}
.hidden_action {
margin-left: 1rem;
display: none;
}
.todo__item {
display: flex;
align-items: center;
align-content: flex-start;
}
.todo__item:hover .hidden_action {
display: inline-block;
}
.todo__checkbox {
margin-right: 0.5rem;
}
.todo_form {
display: flex;
}
.todo__details {
display: flex;
align-items: center;
flex-direction: row;
}
.--hidden {
display: none;
}
.--archived {
text-align: center;
background-color: #ccc;
}
.action-button {
padding: 0.25rem 1rem;
background-color: var(--blue);
font-weight: 900;
line-height: 1.75;
border: none;
color: white;
cursor: pointer;
text-decoration: none;
}
/* FORMS */
input[type=text]:not([name=description]),
input[type=number],
input[type=password],
input[type=search],
textarea, select {
border: 0.2rem solid var(--grey);
padding: 0.3rem;
font-family: inherit;
outline: none;
width: 100%;
max-width: 100%;
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:focus, textarea:focus, select:focus {
border-color: var(--blue);
}
input[type=radio], input[type=checkbox] {
cursor: pointer;
width: 1.5rem;
height: 1.5rem;
vertical-align: middle;
}
textarea {
resize: none;
}
label {
display: block;
}
input[type=checkbox], label {
cursor: pointer;
}
input[type=checkbox] {
height: 1rem;
width: 1rem;
}
li label {
display: inline-block;
}
/*MENU*/
hgroup {
position: relative;
}
.menu__title {
font-weight: 900;
padding: 0.2rem 1rem;
border: none;
background-color: var(--blue);
color: white;
cursor: pointer;
text-decoration: none;
}
.menu__items {
background-color: var(--white);
padding: 1rem;
margin: 0;
border: 1px solid #bdc3c7;
}
@media (min-width: 900px) {
.menu {
position: absolute;
top: 0;
right: 0;
}
.menu__items {
width: 12rem;
position: absolute;
z-index: 3;
box-shadow: 3px 3px 3px #bdc3c7;
right: 0;
text-align: left;
}
}

350
static/styles/normalize.css vendored Normal file
View File

@ -0,0 +1,350 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

85
templates/base.html Normal file
View File

@ -0,0 +1,85 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>OnBoard</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}"/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<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 rel="stylesheet" type="text/css" href="{% static "styles/normalize.css" %}">
<link rel="stylesheet" type="text/css" href="{% static "styles/main.css" %}">
{% block head %}
{% endblock %}
</head>
<body>
<header>
<nav class="navbar__desktop">
{% if user.is_authenticated %}
<form class="nav__search" action="{% url 'search-results' %}" method="GET">
<input name="q" type="search" placeholder="Search...">
</form>
<div class="nav__employees">
<a href="{% url 'employee-list' %}">Employees</a>
</div>
<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>
{% endif %}
<a class="action-button" href="{% url 'logout' %}">Logout</a>
</div>
{% else %}
<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_">
<summary class="menu__title">Menu</summary>
<dl class="menu__items">
<dt>
<form class="nav__search" action="{% url 'search-results' %}" method="GET">
<input name="q" type="text" placeholder="Search...">
</form>
</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 %}
<a class="nav__login" class="action-button" href="{% url 'login' %}">Login</a>
{% endif %}
</nav>
</header>
<aside>
{% if messages %}
<div class="messages panel">
{% for message in messages %}
<span {% if message.tags %} class="{{ message.tags }} messages__message"{% endif %}>{{ message }}</span>
{% endfor %}
</div>
{% endif %}
{% block aside %}
{% endblock %}
</aside>
<main>
{% block content %}
{% endblock %}
</main>
<footer>
</footer>
</body>
</html>

View File

@ -0,0 +1,9 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<section>
<p>You have been logged out. <a href="{% url 'login' %}">Log in</a></p>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<h1>Log in</h1>
{% if form.errors %}
<p class="error">Your username and password didn't match. Please try again.</p>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<p>
{{ form.username.label_tag }}
{{ form.username }}
</p>
<p>
{{ form.password.label_tag }}
{{ form.password }}
<br>
<small>
Forgot your password?
<a class="password__reset hover" href="{% url 'password_reset' %}">Reset your password here</a>.
</small>
</p>
<p>
<input type="submit" value="Login" class="action-button">
<input type="hidden" name="next" value="{{ next }}">
</p>
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<section>
<p>
Password has been changed.
<a href="{% url 'login' %}" class="action-button">Log in</a>
</p>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<h1>Change password</h1>
<form method="post" action="{% url 'password_change' %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Change my password" class="action-button"> or
<a href="{{request.META.HTTP_REFERER}}">Cancel</a>
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<p>Password was reset successfully.</p>
</article>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block content %}
{% if validlink %}
<article class="panel">
<h1>Reset password</h1>
<p>Enter a <em>new</em> password below.</p>
<form method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Reset your password" class="action-button">
</form>
</article>
{% else %}
<article class="panel">
<h1 class="error">Password reset failed</h1>
</article>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<p>An email with password reset instructions has been sent.</p>
</article>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% load i18n %}{% autoescape off %}
{% blocktranslate %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktranslate %}
{% translate "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% translate 'Your username, in case youve forgotten:' %} {{ user.get_username }}
{% endautoescape %}

View File

@ -0,0 +1,16 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<h1>Reset your password</h1>
<p>Enter your email address below and we'll send you instructions on how to reset your password.</p>
<form method="post" action="{% url 'password_reset' %}">
{% csrf_token %}
{{ form.as_p }}
<p>
<input type="submit" value="Send me instructions" class="action-button">
</p>
</form>
</article>
{% endblock %}