Add... everything
This commit is contained in:
parent
60b30d7d6c
commit
b89269d960
0
accounts/__init__.py
Executable file
0
accounts/__init__.py
Executable file
4
accounts/admin.py
Executable file
4
accounts/admin.py
Executable file
@ -0,0 +1,4 @@
|
||||
from django.contrib import admin
|
||||
from .models import Profile
|
||||
|
||||
admin.site.register(Profile)
|
||||
9
accounts/apps.py
Executable file
9
accounts/apps.py
Executable 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
20
accounts/forms.py
Normal 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',
|
||||
)
|
||||
27
accounts/migrations/0001_initial.py
Normal file
27
accounts/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
accounts/migrations/__init__.py
Normal file
0
accounts/migrations/__init__.py
Normal file
31
accounts/models.py
Executable file
31
accounts/models.py
Executable 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
11
accounts/signals.py
Executable 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()
|
||||
11
accounts/templates/accounts/account_detail.html
Executable file
11
accounts/templates/accounts/account_detail.html
Executable 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 %}
|
||||
20
accounts/templates/accounts/account_form.html
Executable file
20
accounts/templates/accounts/account_form.html
Executable 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 %}
|
||||
10
accounts/templates/accounts/profile.html
Normal file
10
accounts/templates/accounts/profile.html
Normal 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 %}
|
||||
10
accounts/templates/accounts/profile_form.html
Executable file
10
accounts/templates/accounts/profile_form.html
Executable 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
3
accounts/tests.py
Executable file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Write your tests
|
||||
12
accounts/urls.py
Executable file
12
accounts/urls.py
Executable 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
58
accounts/views.py
Executable 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')
|
||||
@ -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
91
board/forms.py
Normal 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'
|
||||
})
|
||||
}
|
||||
@ -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
24
board/regex.py
Normal 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
|
||||
@ -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()
|
||||
19
board/templates/board/employee_archive_form.html
Normal file
19
board/templates/board/employee_archive_form.html
Normal 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 %}
|
||||
32
board/templates/board/employee_archived.html
Normal file
32
board/templates/board/employee_archived.html
Normal 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">« 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 »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
13
board/templates/board/employee_confirm_delete.html
Normal file
13
board/templates/board/employee_confirm_delete.html
Normal 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 %}
|
||||
16
board/templates/board/employee_create_form.html
Normal file
16
board/templates/board/employee_create_form.html
Normal 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 %}
|
||||
56
board/templates/board/employee_detail.html
Normal file
56
board/templates/board/employee_detail.html
Normal 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"}}—</span>
|
||||
<span>{{entry.notes}}</span>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
16
board/templates/board/employee_form.html
Normal file
16
board/templates/board/employee_form.html
Normal 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 %}
|
||||
19
board/templates/board/employee_list.html
Normal file
19
board/templates/board/employee_list.html
Normal 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…</a>
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
16
board/templates/board/employee_pre_form.html
Normal file
16
board/templates/board/employee_pre_form.html
Normal 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 %}
|
||||
20
board/templates/board/employee_search_results.html
Normal file
20
board/templates/board/employee_search_results.html
Normal 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 %}
|
||||
1
board/templates/board/item_deleted.html
Normal file
1
board/templates/board/item_deleted.html
Normal file
@ -0,0 +1 @@
|
||||
<li>Item deleted.</li>
|
||||
13
board/templates/board/logentry_confirm_delete.html
Normal file
13
board/templates/board/logentry_confirm_delete.html
Normal 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 %}
|
||||
16
board/templates/board/logentry_create_form.html
Normal file
16
board/templates/board/logentry_create_form.html
Normal 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 %}
|
||||
10
board/templates/board/logentry_detail.html
Normal file
10
board/templates/board/logentry_detail.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1></h1>
|
||||
<section>
|
||||
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
10
board/templates/board/logentry_form.html
Normal file
10
board/templates/board/logentry_form.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1></h1>
|
||||
<section>
|
||||
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
10
board/templates/board/logentry_list.html
Normal file
10
board/templates/board/logentry_list.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1></h1>
|
||||
<section>
|
||||
|
||||
</section>
|
||||
</article>
|
||||
{% endblock %}
|
||||
13
board/templates/board/todo_confirm_delete.html
Normal file
13
board/templates/board/todo_confirm_delete.html
Normal 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 %}
|
||||
16
board/templates/board/todo_create_form.html
Normal file
16
board/templates/board/todo_create_form.html
Normal 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 %}
|
||||
13
board/templates/board/todo_detail.html
Normal file
13
board/templates/board/todo_detail.html
Normal 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…</a>
|
||||
<a class="hidden_action" data-url="{% url 'todo-delete' employee.pk todo.pk %}" href="#" name="destroy">Delete…</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</li>
|
||||
16
board/templates/board/todo_form.html
Normal file
16
board/templates/board/todo_form.html
Normal 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 %}
|
||||
5
board/templates/board/todo_list.html
Normal file
5
board/templates/board/todo_list.html
Normal 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
35
board/urls.py
Normal 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'),
|
||||
])),
|
||||
|
||||
])),
|
||||
]
|
||||
172
board/views.py
172
board/views.py
@ -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'
|
||||
@ -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
|
||||
|
||||
@ -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
155
static/scripts/form.js
Normal 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;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
16
static/scripts/get_cookie.js
Normal file
16
static/scripts/get_cookie.js
Normal 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
11
static/scripts/index.js
Normal 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
52
static/scripts/view.js
Normal 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
303
static/styles/main.css
Normal 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
350
static/styles/normalize.css
vendored
Normal 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
85
templates/base.html
Normal 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>
|
||||
9
templates/registration/logged_out.html
Executable file
9
templates/registration/logged_out.html
Executable 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 %}
|
||||
33
templates/registration/login.html
Executable file
33
templates/registration/login.html
Executable 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 %}
|
||||
12
templates/registration/password_change_done.html
Executable file
12
templates/registration/password_change_done.html
Executable 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 %}
|
||||
14
templates/registration/password_change_form.html
Executable file
14
templates/registration/password_change_form.html
Executable 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 %}
|
||||
7
templates/registration/password_reset_complete.html
Executable file
7
templates/registration/password_reset_complete.html
Executable file
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<article class="panel">
|
||||
<p>Password was reset successfully.</p>
|
||||
</article>
|
||||
{% endblock %}
|
||||
24
templates/registration/password_reset_confirm.html
Executable file
24
templates/registration/password_reset_confirm.html
Executable 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 %}
|
||||
7
templates/registration/password_reset_done.html
Executable file
7
templates/registration/password_reset_done.html
Executable 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 %}
|
||||
10
templates/registration/password_reset_email.html
Executable file
10
templates/registration/password_reset_email.html
Executable 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 you’ve forgotten:' %} {{ user.get_username }}
|
||||
|
||||
{% endautoescape %}
|
||||
16
templates/registration/password_reset_form.html
Executable file
16
templates/registration/password_reset_form.html
Executable 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user