From b89269d960f9316a68188efd314aa5256f923fd0 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Wed, 21 Jul 2021 20:58:18 -0600 Subject: [PATCH] Add... everything --- accounts/__init__.py | 0 accounts/admin.py | 4 + accounts/apps.py | 9 + accounts/forms.py | 20 + accounts/migrations/0001_initial.py | 27 ++ accounts/migrations/__init__.py | 0 accounts/models.py | 31 ++ accounts/signals.py | 11 + .../templates/accounts/account_detail.html | 11 + accounts/templates/accounts/account_form.html | 20 + accounts/templates/accounts/profile.html | 10 + accounts/templates/accounts/profile_form.html | 10 + accounts/tests.py | 3 + accounts/urls.py | 12 + accounts/views.py | 58 +++ board/apps.py | 2 +- board/forms.py | 91 +++++ board/models.py | 72 ++-- board/regex.py | 24 ++ board/signals.py | 18 + .../board/employee_archive_form.html | 19 + board/templates/board/employee_archived.html | 32 ++ .../board/employee_confirm_delete.html | 13 + .../templates/board/employee_create_form.html | 16 + board/templates/board/employee_detail.html | 56 +++ board/templates/board/employee_form.html | 16 + board/templates/board/employee_list.html | 19 + board/templates/board/employee_pre_form.html | 16 + .../board/employee_search_results.html | 20 + board/templates/board/item_deleted.html | 1 + .../board/logentry_confirm_delete.html | 13 + .../templates/board/logentry_create_form.html | 16 + board/templates/board/logentry_detail.html | 10 + board/templates/board/logentry_form.html | 10 + board/templates/board/logentry_list.html | 10 + .../templates/board/todo_confirm_delete.html | 13 + board/templates/board/todo_create_form.html | 16 + board/templates/board/todo_detail.html | 13 + board/templates/board/todo_form.html | 16 + board/templates/board/todo_list.html | 5 + board/urls.py | 35 ++ board/views.py | 172 ++++++++- onboard/settings.py | 12 +- onboard/urls.py | 5 +- static/scripts/form.js | 155 ++++++++ static/scripts/get_cookie.js | 16 + static/scripts/index.js | 11 + static/scripts/view.js | 52 +++ static/styles/main.css | 303 +++++++++++++++ static/styles/normalize.css | 350 ++++++++++++++++++ templates/base.html | 85 +++++ templates/registration/logged_out.html | 9 + templates/registration/login.html | 33 ++ .../registration/password_change_done.html | 12 + .../registration/password_change_form.html | 14 + .../registration/password_reset_complete.html | 7 + .../registration/password_reset_confirm.html | 24 ++ .../registration/password_reset_done.html | 7 + .../registration/password_reset_email.html | 10 + .../registration/password_reset_form.html | 16 + 60 files changed, 2053 insertions(+), 38 deletions(-) create mode 100755 accounts/__init__.py create mode 100755 accounts/admin.py create mode 100755 accounts/apps.py create mode 100644 accounts/forms.py create mode 100644 accounts/migrations/0001_initial.py create mode 100644 accounts/migrations/__init__.py create mode 100755 accounts/models.py create mode 100755 accounts/signals.py create mode 100755 accounts/templates/accounts/account_detail.html create mode 100755 accounts/templates/accounts/account_form.html create mode 100644 accounts/templates/accounts/profile.html create mode 100755 accounts/templates/accounts/profile_form.html create mode 100755 accounts/tests.py create mode 100755 accounts/urls.py create mode 100755 accounts/views.py create mode 100644 board/forms.py create mode 100644 board/regex.py create mode 100644 board/templates/board/employee_archive_form.html create mode 100644 board/templates/board/employee_archived.html create mode 100644 board/templates/board/employee_confirm_delete.html create mode 100644 board/templates/board/employee_create_form.html create mode 100644 board/templates/board/employee_detail.html create mode 100644 board/templates/board/employee_form.html create mode 100644 board/templates/board/employee_list.html create mode 100644 board/templates/board/employee_pre_form.html create mode 100644 board/templates/board/employee_search_results.html create mode 100644 board/templates/board/item_deleted.html create mode 100644 board/templates/board/logentry_confirm_delete.html create mode 100644 board/templates/board/logentry_create_form.html create mode 100644 board/templates/board/logentry_detail.html create mode 100644 board/templates/board/logentry_form.html create mode 100644 board/templates/board/logentry_list.html create mode 100644 board/templates/board/todo_confirm_delete.html create mode 100644 board/templates/board/todo_create_form.html create mode 100644 board/templates/board/todo_detail.html create mode 100644 board/templates/board/todo_form.html create mode 100644 board/templates/board/todo_list.html create mode 100644 board/urls.py create mode 100644 static/scripts/form.js create mode 100644 static/scripts/get_cookie.js create mode 100644 static/scripts/index.js create mode 100644 static/scripts/view.js create mode 100644 static/styles/main.css create mode 100644 static/styles/normalize.css create mode 100644 templates/base.html create mode 100755 templates/registration/logged_out.html create mode 100755 templates/registration/login.html create mode 100755 templates/registration/password_change_done.html create mode 100755 templates/registration/password_change_form.html create mode 100755 templates/registration/password_reset_complete.html create mode 100755 templates/registration/password_reset_confirm.html create mode 100755 templates/registration/password_reset_done.html create mode 100755 templates/registration/password_reset_email.html create mode 100755 templates/registration/password_reset_form.html diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100755 index 0000000..d914f1f --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import Profile + +admin.site.register(Profile) diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100755 index 0000000..ff5cd44 --- /dev/null +++ b/accounts/apps.py @@ -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 diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..55d822a --- /dev/null +++ b/accounts/forms.py @@ -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', + ) diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..dbf4485 --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100755 index 0000000..6f1ac62 --- /dev/null +++ b/accounts/models.py @@ -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}' diff --git a/accounts/signals.py b/accounts/signals.py new file mode 100755 index 0000000..1d6820c --- /dev/null +++ b/accounts/signals.py @@ -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() diff --git a/accounts/templates/accounts/account_detail.html b/accounts/templates/accounts/account_detail.html new file mode 100755 index 0000000..4e6c6e3 --- /dev/null +++ b/accounts/templates/accounts/account_detail.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block content %} +
+

{{ user.first_name }} {{ user.last_name }}

+

{{ user.email }}

+

+ Update email/change password +

+
+{% endblock %} diff --git a/accounts/templates/accounts/account_form.html b/accounts/templates/accounts/account_form.html new file mode 100755 index 0000000..222fbe5 --- /dev/null +++ b/accounts/templates/accounts/account_form.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} + +{% block content %} +
+ +
+

Update Profile

+

Change password

+
+ {% csrf_token %} + {{ form.as_p }} + + or + Cancel +
+
+ + {% include "accounts/profile_form.html" with form=profile_form %} +
+{% endblock %} \ No newline at end of file diff --git a/accounts/templates/accounts/profile.html b/accounts/templates/accounts/profile.html new file mode 100644 index 0000000..927dec5 --- /dev/null +++ b/accounts/templates/accounts/profile.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+

Hi, {{user.first_name}}

+
+
+{% endblock %} diff --git a/accounts/templates/accounts/profile_form.html b/accounts/templates/accounts/profile_form.html new file mode 100755 index 0000000..9b1dd9e --- /dev/null +++ b/accounts/templates/accounts/profile_form.html @@ -0,0 +1,10 @@ +
+

Settings

+
+ {% csrf_token %} + {{form.as_p}} +

+ +

+
+
diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100755 index 0000000..03cb7b5 --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Write your tests diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100755 index 0000000..a72c9fc --- /dev/null +++ b/accounts/urls.py @@ -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//update/', views.ProfileUpdateView.as_view(), name='profile-update'), + path('/', 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'), + ])), +] diff --git a/accounts/views.py b/accounts/views.py new file mode 100755 index 0000000..b13a258 --- /dev/null +++ b/accounts/views.py @@ -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') diff --git a/board/apps.py b/board/apps.py index a711ffd..6f82577 100644 --- a/board/apps.py +++ b/board/apps.py @@ -6,4 +6,4 @@ class BoardConfig(AppConfig): name = 'board' def ready(self): - from .signals import create_employee \ No newline at end of file + from .signals import create_employee, complete_todo \ No newline at end of file diff --git a/board/forms.py b/board/forms.py new file mode 100644 index 0000000..d32ebe2 --- /dev/null +++ b/board/forms.py @@ -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' + }) + } diff --git a/board/models.py b/board/models.py index 17bf0ae..1d3ac53 100644 --- a/board/models.py +++ b/board/models.py @@ -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}) diff --git a/board/regex.py b/board/regex.py new file mode 100644 index 0000000..19986d7 --- /dev/null +++ b/board/regex.py @@ -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 \ No newline at end of file diff --git a/board/signals.py b/board/signals.py index 6cbef55..924b3eb 100644 --- a/board/signals.py +++ b/board/signals.py @@ -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() \ No newline at end of file diff --git a/board/templates/board/employee_archive_form.html b/board/templates/board/employee_archive_form.html new file mode 100644 index 0000000..d447cb1 --- /dev/null +++ b/board/templates/board/employee_archive_form.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} +
+

Archive {{object}}

+
+
+ {% csrf_token %} + +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/board/templates/board/employee_archived.html b/board/templates/board/employee_archived.html new file mode 100644 index 0000000..b816c56 --- /dev/null +++ b/board/templates/board/employee_archived.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block content %} +
+

Archived Employees

+
+ {% for employee in employee_list %} +

+ {{employee.full_name}}
+ Hire date: {{employee.hire_date|date:"SHORT_DATE_FORMAT"}} +

+ {% endfor %} +
+
+ + {% if page_obj.has_previous %} + « first + previous + {% endif %} + + + Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}. + + + {% if page_obj.has_next %} + next + last » + {% endif %} + +
+
+{% endblock %} \ No newline at end of file diff --git a/board/templates/board/employee_confirm_delete.html b/board/templates/board/employee_confirm_delete.html new file mode 100644 index 0000000..43b5784 --- /dev/null +++ b/board/templates/board/employee_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{employee}}

+
+ {% csrf_token %} +

+ or cancel +

+
+
+{% endblock %} \ No newline at end of file diff --git a/board/templates/board/employee_create_form.html b/board/templates/board/employee_create_form.html new file mode 100644 index 0000000..5172f5b --- /dev/null +++ b/board/templates/board/employee_create_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Create Employee

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/board/templates/board/employee_detail.html b/board/templates/board/employee_detail.html new file mode 100644 index 0000000..4874dd1 --- /dev/null +++ b/board/templates/board/employee_detail.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% load static %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+ {% if employee.archived %} +

This employee is currently Archived

+ {% endif %} +
+

{{employee}}

+

Hire Date: {{employee.hire_date}}

+ +
+
+

+ Title: {{employee.title}}
+ Department: {{employee.department}}
+ Manager: {{employee.manager}} +

+

Initial comments:

+

{{employee.initial_comments|linebreaksbr}}
+
+
+

To-do's

+ {% include "board/todo_list.html" with todo_list=employee.todo_set.all %} + {% if not employee.archived %} +

+ Add a to-do +

+ {% endif %} +
+
+

Log

+

+ Add an Entry +

+ {% for entry in employee.logentry_set.all %} +

+ {{entry.created_at|date:"SHORT_DATE_FORMAT"}}— + {{entry.notes}} +

+ {% endfor %} +
+
+{% endblock %} \ No newline at end of file diff --git a/board/templates/board/employee_form.html b/board/templates/board/employee_form.html new file mode 100644 index 0000000..4274795 --- /dev/null +++ b/board/templates/board/employee_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Update Employee

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/board/templates/board/employee_list.html b/board/templates/board/employee_list.html new file mode 100644 index 0000000..ac68ece --- /dev/null +++ b/board/templates/board/employee_list.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/board/templates/board/employee_pre_form.html b/board/templates/board/employee_pre_form.html new file mode 100644 index 0000000..8ea828d --- /dev/null +++ b/board/templates/board/employee_pre_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Create Employee

+
+
+ {% csrf_token %} + {{form.as_p}} +

+ or cancel +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/board/templates/board/employee_search_results.html b/board/templates/board/employee_search_results.html new file mode 100644 index 0000000..287b82e --- /dev/null +++ b/board/templates/board/employee_search_results.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} +
+

Search Results

+
+ {% for employee in employee_list %} +

+ {{employee.full_name}}
+ {% if employee.archived %} + Archived + {% endif %} + Hire date: {{employee.hire_date|date:"SHORT_DATE_FORMAT"}} +

+ {% empty %} +

Query did not contain any results.

+ {% endfor %} +
+
+{% endblock %} \ No newline at end of file diff --git a/board/templates/board/item_deleted.html b/board/templates/board/item_deleted.html new file mode 100644 index 0000000..562fe6d --- /dev/null +++ b/board/templates/board/item_deleted.html @@ -0,0 +1 @@ +
  • Item deleted.
  • \ No newline at end of file diff --git a/board/templates/board/logentry_confirm_delete.html b/board/templates/board/logentry_confirm_delete.html new file mode 100644 index 0000000..7aae8ca --- /dev/null +++ b/board/templates/board/logentry_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Delete {{logentry}}

    +
    + {% csrf_token %} +

    + or cancel +

    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/board/templates/board/logentry_create_form.html b/board/templates/board/logentry_create_form.html new file mode 100644 index 0000000..3395539 --- /dev/null +++ b/board/templates/board/logentry_create_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Add note to {{employee}}

    +
    +
    + {% csrf_token %} + {{form.as_p}} +

    + or cancel +

    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/board/templates/board/logentry_detail.html b/board/templates/board/logentry_detail.html new file mode 100644 index 0000000..b35e99a --- /dev/null +++ b/board/templates/board/logentry_detail.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    +
    + +
    +
    +{% endblock %} \ No newline at end of file diff --git a/board/templates/board/logentry_form.html b/board/templates/board/logentry_form.html new file mode 100644 index 0000000..b35e99a --- /dev/null +++ b/board/templates/board/logentry_form.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    +
    + +
    +
    +{% endblock %} \ No newline at end of file diff --git a/board/templates/board/logentry_list.html b/board/templates/board/logentry_list.html new file mode 100644 index 0000000..b35e99a --- /dev/null +++ b/board/templates/board/logentry_list.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    +
    + +
    +
    +{% endblock %} \ No newline at end of file diff --git a/board/templates/board/todo_confirm_delete.html b/board/templates/board/todo_confirm_delete.html new file mode 100644 index 0000000..41d051b --- /dev/null +++ b/board/templates/board/todo_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Delete {{todo}}

    +
    + {% csrf_token %} +

    + or cancel +

    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/board/templates/board/todo_create_form.html b/board/templates/board/todo_create_form.html new file mode 100644 index 0000000..31d52da --- /dev/null +++ b/board/templates/board/todo_create_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Create To-do

    +
    +
    + {% csrf_token %} + {{form.as_p}} +

    + or cancel +

    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/board/templates/board/todo_detail.html b/board/templates/board/todo_detail.html new file mode 100644 index 0000000..34e6ec3 --- /dev/null +++ b/board/templates/board/todo_detail.html @@ -0,0 +1,13 @@ +
  • +
    + + {% if not employee.archived %} + Edit… + Delete… + {% endif %} +
    +
  • \ No newline at end of file diff --git a/board/templates/board/todo_form.html b/board/templates/board/todo_form.html new file mode 100644 index 0000000..8851dda --- /dev/null +++ b/board/templates/board/todo_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Update To-do

    +
    +
    + {% csrf_token %} + {{form.as_p}} +

    + or cancel +

    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/board/templates/board/todo_list.html b/board/templates/board/todo_list.html new file mode 100644 index 0000000..0372432 --- /dev/null +++ b/board/templates/board/todo_list.html @@ -0,0 +1,5 @@ +
      + {% for todo in todo_list %} + {% include "board/todo_detail.html" with todo=todo %} + {% endfor %} +
    \ No newline at end of file diff --git a/board/urls.py b/board/urls.py new file mode 100644 index 0000000..0da3ec1 --- /dev/null +++ b/board/urls.py @@ -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//', 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//', 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//', 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'), + ])), + + ])), +] diff --git a/board/views.py b/board/views.py index 91ea44a..d0be241 100644 --- a/board/views.py +++ b/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' \ No newline at end of file diff --git a/onboard/settings.py b/onboard/settings.py index fe945bb..87a4c6c 100644 --- a/onboard/settings.py +++ b/onboard/settings.py @@ -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 diff --git a/onboard/urls.py b/onboard/urls.py index fc45c44..a720268 100644 --- a/onboard/urls.py +++ b/onboard/urls.py @@ -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), ] diff --git a/static/scripts/form.js b/static/scripts/form.js new file mode 100644 index 0000000..f8aa2e4 --- /dev/null +++ b/static/scripts/form.js @@ -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; + }) + } + } +} diff --git a/static/scripts/get_cookie.js b/static/scripts/get_cookie.js new file mode 100644 index 0000000..4f920c4 --- /dev/null +++ b/static/scripts/get_cookie.js @@ -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; +} diff --git a/static/scripts/index.js b/static/scripts/index.js new file mode 100644 index 0000000..13c460f --- /dev/null +++ b/static/scripts/index.js @@ -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" +) \ No newline at end of file diff --git a/static/scripts/view.js b/static/scripts/view.js new file mode 100644 index 0000000..b12270f --- /dev/null +++ b/static/scripts/view.js @@ -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); + } + +} diff --git a/static/styles/main.css b/static/styles/main.css new file mode 100644 index 0000000..07c66e4 --- /dev/null +++ b/static/styles/main.css @@ -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; + } +} \ No newline at end of file diff --git a/static/styles/normalize.css b/static/styles/normalize.css new file mode 100644 index 0000000..9d9f37a --- /dev/null +++ b/static/styles/normalize.css @@ -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; +} + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..61f2fc5 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,85 @@ +{% load static %} + + + + OnBoard + + + + + + + + + + + + + {% block head %} + {% endblock %} + + +
    + + +
    + + + +
    + {% block content %} + {% endblock %} +
    + +
    +
    + + diff --git a/templates/registration/logged_out.html b/templates/registration/logged_out.html new file mode 100755 index 0000000..0b464dc --- /dev/null +++ b/templates/registration/logged_out.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block content %} +
    +
    +

    You have been logged out. Log in

    +
    +
    +{% endblock %} diff --git a/templates/registration/login.html b/templates/registration/login.html new file mode 100755 index 0000000..b7d4b80 --- /dev/null +++ b/templates/registration/login.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} + +{% block content %} +
    +

    Log in

    + {% if form.errors %} +

    Your username and password didn't match. Please try again.

    + {% endif %} + +
    + {% csrf_token %} +

    + {{ form.username.label_tag }} + {{ form.username }} +

    + +

    + {{ form.password.label_tag }} + {{ form.password }} +
    + + Forgot your password? + Reset your password here. + +

    + +

    + + +

    +
    +
    +{% endblock %} diff --git a/templates/registration/password_change_done.html b/templates/registration/password_change_done.html new file mode 100755 index 0000000..fb611b7 --- /dev/null +++ b/templates/registration/password_change_done.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +
    +
    +

    + Password has been changed. + Log in +

    +
    +
    +{% endblock %} diff --git a/templates/registration/password_change_form.html b/templates/registration/password_change_form.html new file mode 100755 index 0000000..cad7d69 --- /dev/null +++ b/templates/registration/password_change_form.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block content %} +
    +

    Change password

    +
    + {% csrf_token %} + {{ form.as_p }} + + or + Cancel +
    +
    +{% endblock %} diff --git a/templates/registration/password_reset_complete.html b/templates/registration/password_reset_complete.html new file mode 100755 index 0000000..ad0c905 --- /dev/null +++ b/templates/registration/password_reset_complete.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} + +{% block content %} +
    +

    Password was reset successfully.

    +
    +{% endblock %} diff --git a/templates/registration/password_reset_confirm.html b/templates/registration/password_reset_confirm.html new file mode 100755 index 0000000..1aaf4a8 --- /dev/null +++ b/templates/registration/password_reset_confirm.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{% block content %} + +{% if validlink %} +
    +

    Reset password

    +

    Enter a new password below.

    +
    + {% csrf_token %} + {{ form.as_p }} + + +
    +
    +{% else %} + +
    +

    Password reset failed

    +
    + +{% endif %} + +{% endblock %} diff --git a/templates/registration/password_reset_done.html b/templates/registration/password_reset_done.html new file mode 100755 index 0000000..8f2b394 --- /dev/null +++ b/templates/registration/password_reset_done.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} + +{% block content %} +
    +

    An email with password reset instructions has been sent.

    +
    +{% endblock %} diff --git a/templates/registration/password_reset_email.html b/templates/registration/password_reset_email.html new file mode 100755 index 0000000..8c37094 --- /dev/null +++ b/templates/registration/password_reset_email.html @@ -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 %} diff --git a/templates/registration/password_reset_form.html b/templates/registration/password_reset_form.html new file mode 100755 index 0000000..0c36cf0 --- /dev/null +++ b/templates/registration/password_reset_form.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block content %} +
    +

    Reset your password

    +

    Enter your email address below and we'll send you instructions on how to reset your password.

    +
    + {% csrf_token %} + {{ form.as_p }} + +

    + +

    +
    +
    +{% endblock %}