diff --git a/Pipfile b/Pipfile index 8a5a5a0..48ec8ac 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,8 @@ name = "pypi" [packages] django = "*" django-anymail = {extras = ["mailgun"], version = "*"} +celery = {extras = ["redis"], version = "*"} +django-celery-beat = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 88bca9a..40545b0 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "88b6a9fdda6471bfc16c42a906945bb034d4b2d576f31179a72471296eac58cd" + "sha256": "ffe96f34ca738a35a283e9ac98b5f1a74609c67619f18e8eb7ba62967247766f" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,14 @@ ] }, "default": { + "amqp": { + "hashes": [ + "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2", + "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb" + ], + "markers": "python_version >= '3.6'", + "version": "==5.0.6" + }, "asgiref": { "hashes": [ "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9", @@ -24,6 +32,24 @@ "markers": "python_version >= '3.6'", "version": "==3.4.1" }, + "billiard": { + "hashes": [ + "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547", + "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b" + ], + "version": "==3.6.4.0" + }, + "celery": { + "extras": [ + "redis" + ], + "hashes": [ + "sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0", + "sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42" + ], + "index": "pypi", + "version": "==5.1.2" + }, "certifi": { "hashes": [ "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", @@ -39,6 +65,34 @@ "markers": "python_version >= '3'", "version": "==2.0.3" }, + "click": { + "hashes": [ + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" + }, + "click-didyoumean": { + "hashes": [ + "sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb" + ], + "version": "==0.0.3" + }, + "click-plugins": { + "hashes": [ + "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", + "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8" + ], + "version": "==1.1.1" + }, + "click-repl": { + "hashes": [ + "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b", + "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8" + ], + "version": "==0.2.0" + }, "django": { "hashes": [ "sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd", @@ -58,6 +112,22 @@ "index": "pypi", "version": "==8.4" }, + "django-celery-beat": { + "hashes": [ + "sha256:97ae5eb309541551bdb07bf60cc57cadacf42a74287560ced2d2c06298620234", + "sha256:ab43049634fd18dc037927d7c2c7d5f67f95283a20ebbda55f42f8606412e66c" + ], + "index": "pypi", + "version": "==2.2.1" + }, + "django-timezone-field": { + "hashes": [ + "sha256:6dc782e31036a58da35b553bd00c70f112d794700025270d8a6a4c1d2e5b26c6", + "sha256:97780cde658daa5094ae515bb55ca97c1352928ab554041207ad515dee3fe971" + ], + "markers": "python_version >= '3.5'", + "version": "==4.2.1" + }, "idna": { "hashes": [ "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", @@ -66,6 +136,36 @@ "markers": "python_version >= '3'", "version": "==3.2" }, + "kombu": { + "hashes": [ + "sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d", + "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a" + ], + "markers": "python_version >= '3.6'", + "version": "==5.1.0" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f", + "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==3.0.19" + }, + "python-crontab": { + "hashes": [ + "sha256:4bbe7e720753a132ca4ca9d4094915f40e9d9dc8a807a4564007651018ce8c31" + ], + "version": "==2.5.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.2" + }, "pytz": { "hashes": [ "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", @@ -73,6 +173,13 @@ ], "version": "==2021.1" }, + "redis": { + "hashes": [ + "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", + "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" + ], + "version": "==3.5.3" + }, "requests": { "hashes": [ "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", @@ -81,6 +188,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.26.0" }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, "sqlparse": { "hashes": [ "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", @@ -96,6 +211,21 @@ ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.6" + }, + "vine": { + "hashes": [ + "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30", + "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e" + ], + "markers": "python_version >= '3.6'", + "version": "==5.0.0" + }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" } }, "develop": {} diff --git a/accounts/templates/accounts/account_detail.html b/accounts/templates/accounts/account_detail.html index 4e6c6e3..0e277e6 100755 --- a/accounts/templates/accounts/account_detail.html +++ b/accounts/templates/accounts/account_detail.html @@ -1,11 +1,13 @@ {% extends 'base.html' %} {% block content %} -
+

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

-

{{ user.email }}

-

- Update email/change password -

-
+
+

{{ user.email }}

+

+ Update email/change password +

+
+ {% endblock %} diff --git a/accounts/templates/accounts/profile.html b/accounts/templates/accounts/profile.html index 927dec5..9a54ced 100644 --- a/accounts/templates/accounts/profile.html +++ b/accounts/templates/accounts/profile.html @@ -6,5 +6,63 @@

Hi, {{user.first_name}}

+
+

Today, {% now "D, M j" %}

+
+ {% for event in today %} +
+ {{event.date|date:"D, M j"}} + {{event.name}} + {% if event.employee %} for + {{event.employee}} + {% endif %} + + {{event.time|time:"TIME_FORMAT"}} + Edit +
+ {% endfor %} +
+
+

Add event

+
+

Upcoming (next seven days)

+
+ {% for event in upcoming_events %} +
+ {{event.date|date:"D, M j"}} + {{event.name}} + {% if event.employee %} for + {{event.employee}} + {% endif %} + + {{event.time|time:"TIME_FORMAT"}} + Edit +
+ {% endfor %} +
+

See all events

+
+
+

Recent Activity

+ {% regroup latest_activity by created_at.date as latest_activity_re %} + {% for date in latest_activity_re %} +
{{date.grouper|date:"D, M j"}}
+ {% for entry in date.list %} +

+ {{entry.created_at|time:"TIME_FORMAT"}} + + {{entry.notes}} + for + {{entry.employee}} + + Edit +

+ {% endfor %} + {% endfor %} +
{% endblock %} + + + + diff --git a/accounts/views.py b/accounts/views.py index b13a258..1d0ffad 100755 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,5 +1,6 @@ import pytz from django.utils import timezone +from datetime import date, timedelta from django.shortcuts import render, reverse, redirect from django.urls import reverse_lazy from django.views.generic.base import TemplateView @@ -12,14 +13,25 @@ from django.contrib.auth.models import User from .models import Profile from .forms import AccountUpdateForm, ProfileUpdateForm +from board.models import LogEntry, Event + class ProfileView(LoginRequiredMixin, TemplateView): template_name = 'accounts/profile.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + today = date.today() + tomorrow = today + timedelta(days=1) + enddate = today + timedelta(days=6) context['profile'] = self.request.user.profile - + context['latest_activity'] = LogEntry.objects.all()[:10] + context['today'] = Event.objects.filter( + date=today + ) + context['upcoming_events'] = Event.objects.filter( + date__range=[tomorrow, enddate] + ) return context class ProfileUpdateView(LoginRequiredMixin, UpdateView): diff --git a/board/admin.py b/board/admin.py index fa58b84..2bbcd67 100644 --- a/board/admin.py +++ b/board/admin.py @@ -4,8 +4,10 @@ from .models import ( Employee, LogEntry, Todo, + Event, ) admin.site.register(Employee) admin.site.register(LogEntry) admin.site.register(Todo) +admin.site.register(Event) diff --git a/board/forms.py b/board/forms.py index d32ebe2..0305e3c 100644 --- a/board/forms.py +++ b/board/forms.py @@ -1,6 +1,7 @@ from datetime import datetime from django import forms -from .models import Employee, LogEntry, Todo +from django.utils import timezone +from .models import Employee, LogEntry, Todo, Event from .regex import process_regex class EmployeePreCreateForm(forms.Form): @@ -89,3 +90,25 @@ class TodoCreateForm(forms.ModelForm): 'autofocus': 'autofocus' }) } + +class EventForm(forms.ModelForm): + class Meta: + model = Event + fields = ( + 'name', + 'date', + 'time', + 'employee', + ) + widgets = { + 'name': forms.TextInput(attrs = { + 'autofocus': 'autofocus' + }), + 'date': forms.DateInput(attrs = { + 'type': 'date', + 'value': timezone.now().date(), + }), + 'time': forms.DateInput(attrs = { + 'type': 'time', + }), + } diff --git a/board/migrations/0003_auto_20210727_0006.py b/board/migrations/0003_auto_20210727_0006.py new file mode 100644 index 0000000..07ee0d7 --- /dev/null +++ b/board/migrations/0003_auto_20210727_0006.py @@ -0,0 +1,70 @@ +# Generated by Django 3.2.5 on 2021-07-27 00:06 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('board', '0002_auto_20210709_0103'), + ] + + operations = [ + migrations.AlterModelOptions( + name='employee', + options={'ordering': ('-hire_date',)}, + ), + migrations.AlterModelOptions( + name='logentry', + options={'ordering': ('-created_at',)}, + ), + migrations.AlterField( + model_name='employee', + name='department', + field=models.CharField(blank=True, max_length=64, null=True), + ), + migrations.AlterField( + model_name='employee', + name='first_name', + field=models.CharField(max_length=64), + ), + migrations.AlterField( + model_name='employee', + name='last_name', + field=models.CharField(max_length=64), + ), + migrations.AlterField( + model_name='employee', + name='manager', + field=models.CharField(blank=True, max_length=64, null=True), + ), + migrations.AlterField( + model_name='employee', + name='title', + field=models.CharField(blank=True, max_length=64, null=True), + ), + migrations.AlterField( + model_name='logentry', + name='notes', + field=models.CharField(max_length=500), + ), + migrations.AlterField( + model_name='todo', + name='description', + field=models.CharField(max_length=64), + ), + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.CharField(max_length=64)), + ('date', models.DateField()), + ('time', models.TimeField(blank=True, null=True)), + ('employee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='board.employee')), + ], + options={ + 'ordering': ('date', 'time'), + }, + ), + ] diff --git a/board/migrations/0004_rename_description_event_name.py b/board/migrations/0004_rename_description_event_name.py new file mode 100644 index 0000000..dc8a6d3 --- /dev/null +++ b/board/migrations/0004_rename_description_event_name.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.5 on 2021-07-27 01:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('board', '0003_auto_20210727_0006'), + ] + + operations = [ + migrations.RenameField( + model_name='event', + old_name='description', + new_name='name', + ), + ] diff --git a/board/models.py b/board/models.py index 1d3ac53..b56eeba 100644 --- a/board/models.py +++ b/board/models.py @@ -57,3 +57,19 @@ class Todo(models.Model): def get_absolute_url(self): return reverse('todo-detail', kwargs={'pk': self.employee.pk, 'todo_pk': self.pk}) + + +class Event(models.Model): + class Meta: + ordering = ('date', 'time') + + name = models.CharField(max_length=64) + date = models.DateField() + time = models.TimeField(blank=True, null=True) + employee = models.ForeignKey(Employee, on_delete=models.CASCADE, blank=True, null=True) + + def __str__(self): + return f"{self.description} on {self.date} @ {self.time}" + + def get_absolute_url(self): + return reverse('event-detail', kwargs={'pk': self.pk}) diff --git a/board/templates/board/employee_detail.html b/board/templates/board/employee_detail.html index 4874dd1..4f57e28 100644 --- a/board/templates/board/employee_detail.html +++ b/board/templates/board/employee_detail.html @@ -31,6 +31,17 @@

Initial comments:

{{employee.initial_comments|linebreaksbr}}
+
+

Employee Events

+ {% for event in employee.event_set.all %} +
+ {{event.date|date:"D, M j"}} + {{event.name}} + {{event.time|time:"TIME_FORMAT"}} + Edit +
+ {% endfor %} +

To-do's

{% include "board/todo_list.html" with todo_list=employee.todo_set.all %} @@ -45,11 +56,16 @@

Add an Entry

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

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

+ {% regroup employee.logentry_set.all by created_at.date as activity %} + {% for date in activity %} +
{{date.grouper|date:"D, M j"}}
+ {% for entry in date.list %} +

+ {{entry.created_at|time:"TIME_FORMAT"}} + {{entry.notes}} + Edit +

+ {% endfor %} {% endfor %}
diff --git a/board/templates/board/employee_list.html b/board/templates/board/employee_list.html index ac68ece..13f37a0 100644 --- a/board/templates/board/employee_list.html +++ b/board/templates/board/employee_list.html @@ -8,7 +8,8 @@ {% for employee in employee_list %}

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

{% endfor %} diff --git a/board/templates/board/event_confirm_delete.html b/board/templates/board/event_confirm_delete.html new file mode 100644 index 0000000..831a651 --- /dev/null +++ b/board/templates/board/event_confirm_delete.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block content %} +
+

Delete {{event}}

+
+ {% csrf_token %} +

+ or cancel +

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

Create Event

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

+ or cancel +

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

Event

+
+
+ {{event.name}} + {% if event.employee %} for + {{event.employee}} + {% endif %} + + {{event.time|time:"TIME_FORMAT"}} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/board/templates/board/event_form.html b/board/templates/board/event_form.html new file mode 100644 index 0000000..4cd0c9c --- /dev/null +++ b/board/templates/board/event_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+

Update Event

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

+ or cancel +

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

Schedule

+

Add event

+
+ {% for event in event_list %} +
+ {{event.date|date:"D, M j"}} + {{event.name}} + {% if event.employee %} for + {{event.employee}} + {% endif %} + + {{event.time|time:"TIME_FORMAT"}} + Edit +
+ {% empty %} +

There are no events yet.

+ {% 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 %} diff --git a/board/templates/board/logentry_confirm_delete.html b/board/templates/board/logentry_confirm_delete.html index 7aae8ca..8eb1b92 100644 --- a/board/templates/board/logentry_confirm_delete.html +++ b/board/templates/board/logentry_confirm_delete.html @@ -3,10 +3,10 @@ {% block content %}

Delete {{logentry}}

-
+ {% csrf_token %}

- or cancel + or cancel

diff --git a/board/templates/board/logentry_form.html b/board/templates/board/logentry_form.html index b35e99a..0708d6a 100644 --- a/board/templates/board/logentry_form.html +++ b/board/templates/board/logentry_form.html @@ -2,9 +2,18 @@ {% block content %}
-

-
- -
-
+
+

Update Event

+

Delete this entry

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

+ or cancel +

+
+
+ {% endblock %} \ No newline at end of file diff --git a/board/urls.py b/board/urls.py index 0da3ec1..0ac94c6 100644 --- a/board/urls.py +++ b/board/urls.py @@ -32,4 +32,12 @@ urlpatterns = [ ])), ])), + + path('events/', views.EventListView.as_view(), name='event-list'), + path('events/new/', views.EventCreateView.as_view(), name='event-create'), + path('events//', include([ + path('', views.EventDetailView.as_view(), name='event-detail'), + path('update/', views.EventUpdateView.as_view(), name='event-update'), + path('delete/', views.EventDeleteView.as_view(), name='event-delete'), + ])), ] diff --git a/board/views.py b/board/views.py index d0be241..dc4ccce 100644 --- a/board/views.py +++ b/board/views.py @@ -7,14 +7,15 @@ from django.views.generic.list import ListView from django.contrib.auth.mixins import LoginRequiredMixin from django.db.models import Q -from .models import Employee, LogEntry, Todo +from .models import Employee, LogEntry, Todo, Event from .forms import ( EmployeeForm, EmployeePreCreateForm, EmployeeArchiveForm, LogEntryForm, TodoForm, - TodoCreateForm + TodoCreateForm, + EventForm, ) class SearchResultsView(ListView): @@ -117,7 +118,14 @@ class LogEntryUpdateView(LoginRequiredMixin, UpdateView): class LogEntryDeleteView(LoginRequiredMixin, DeleteView): model = LogEntry pk_url_kwarg = 'entry_pk' - success_url = reverse_lazy('entry-list') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['employee'] = Employee.objects.get(pk=self.kwargs['pk']) + return context + + def get_success_url(self): + return reverse('employee-detail', kwargs={'pk': self.kwargs['pk']}) # Todos @@ -170,4 +178,28 @@ class TodoDeleteView(LoginRequiredMixin, DeleteView): return reverse('todo-deleted', kwargs={'pk': self.kwargs['pk']}) class TodoDeleteDoneView(LoginRequiredMixin, TemplateView): - template_name = 'board/item_deleted.html' \ No newline at end of file + template_name = 'board/item_deleted.html' + + +# Events +class EventListView(LoginRequiredMixin, ListView): + model = Event + paginate_by = 100 + ordering = ('-date', '-time') + +class EventCreateView(LoginRequiredMixin, CreateView): + model = Event + template_name_suffix = '_create_form' + form_class = EventForm + success_url = reverse_lazy('profile-detail') + +class EventDetailView(LoginRequiredMixin, DetailView): + model = Event + +class EventUpdateView(LoginRequiredMixin, UpdateView): + model = Event + form_class = EventForm + +class EventDeleteView(LoginRequiredMixin, DeleteView): + model = Event + success_url = reverse_lazy('profile-detail') diff --git a/onboard/__init__.py b/onboard/__init__.py index e69de29..1e3599b 100644 --- a/onboard/__init__.py +++ b/onboard/__init__.py @@ -0,0 +1,5 @@ +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ('celery_app',) \ No newline at end of file diff --git a/onboard/celery.py b/onboard/celery.py new file mode 100644 index 0000000..0fa169f --- /dev/null +++ b/onboard/celery.py @@ -0,0 +1,50 @@ +import os + +from celery import Celery + +# Set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onboard.settings_dev') + +app = Celery('onboard') + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object('django.conf:settings', namespace='CELERY') + +# Load task modules from all registered Django apps. +app.autodiscover_tasks() + + +@app.task(bind=True) +def debug_task(self): + print(f'Request: {self.request!r}') + +# app.conf.beat_schedule = { +# #Scheduler Name +# 'print-message-ten-seconds': { +# # Task Name (Name Specified in Decorator) +# 'task': 'print_msg_main', +# # Schedule +# 'schedule': 10.0, +# # Function Arguments +# 'args': ("Hello",) +# }, +# #Scheduler Name +# 'print-time-twenty-seconds': { +# # Task Name (Name Specified in Decorator) +# 'task': 'print_time', +# # Schedule +# 'schedule': 20.0, +# }, +# #Scheduler Name +# 'calculate-forty-seconds': { +# # Task Name (Name Specified in Decorator) +# 'task': 'get_calculation', +# # Schedule +# 'schedule': 40.0, +# # Function Arguments +# 'args': (10,20) +# }, +# } \ No newline at end of file diff --git a/onboard/middleware.py b/onboard/middleware.py new file mode 100644 index 0000000..8260705 --- /dev/null +++ b/onboard/middleware.py @@ -0,0 +1,17 @@ +import pytz + +from django.utils import timezone + +class TimezoneMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + tzname = request.session.get('timezone') + if request.user.is_authenticated: + tzname = request.user.profile.timezone + if tzname: + timezone.activate(pytz.timezone(tzname)) + else: + timezone.deactivate() + return self.get_response(request) diff --git a/onboard/settings.py b/onboard/settings.py index 6d2e0bb..a0775c0 100644 --- a/onboard/settings.py +++ b/onboard/settings.py @@ -25,7 +25,7 @@ with open('/etc/secret_key.txt') as f: SECRET_KEY = f.read().strip() # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False' +DEBUG = False ALLOWED_HOSTS = ['onboard.windmillapps.org', '127.0.0.1'] @@ -126,10 +126,11 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ - +STATICFILES_DIRS = [ + BASE_DIR / 'static' + ] STATIC_URL = '/static/' -STATIC_ROOT = [BASE_DIR / 'static/'] -STATICFILES_DIRS = [BASE_DIR / 'static'] +STATIC_ROOT = '/var/www/onboard.windmillapps.org/static/' # Default primary key field type @@ -162,7 +163,7 @@ LOGGING = { # Email ANYMAIL = { - "MAILGUN_API_KEY": os.environ.get('MAILGUN_API_KEY'), + "MAILGUN_API_KEY": "key-b65d1f9e486825a7d01d099fd3062c2b", } SERVER_EMAIL = 'noreply@onboard.windmillapps.org' @@ -173,10 +174,8 @@ ADMINS = ( ) MANAGERS = ADMINS -EMAIL_BACKEND = os.environ.get('EMAIL_BACKEND') - - -SECURE_HSTS_SECONDS = os.environ.get('DJANGO_SECURE_HSTS_SECONDS', '') != 'False' -SECURE_SSL_REDIRECT = os.environ.get('DJANGO_SECURE_SSL_REDIRECT', '') != 'False' -SESSION_COOKIE_SECURE = os.environ.get('DJANGO_SESSION_COOKIE_SECURE', '') != 'False' -CSRF_COOKIE_SECURE = os.environ.get('DJANGO_CSRF_COOKIE_SECURE', '') != 'False' \ No newline at end of file +EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' +SECURE_HSTS_SECONDS = True +SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True diff --git a/onboard/settings_dev.py b/onboard/settings_dev.py new file mode 100644 index 0000000..1f84911 --- /dev/null +++ b/onboard/settings_dev.py @@ -0,0 +1,145 @@ +""" +Django settings for onboard project. + +Generated by 'django-admin startproject' using Django 3.2.5. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-fdq1(f%l*__obb*zvb3gqnlki!ryr@_1_5om(_2ju3mu70mi4%' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'xcarbon.lan'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + 'django.contrib.flatpages', + 'django_celery_beat', + 'accounts.apps.AccountsConfig', + 'board.apps.BoardConfig', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'onboard.middleware.TimezoneMiddleware', +] + +ROOT_URLCONF = 'onboard.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'onboard.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' +STATICFILES_DIRS = [BASE_DIR / 'static'] + + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Site ID +SITE_ID = 1 + + + +# Celery - prefix with CELERY_ +CELERY_BROKER_URL = "redis://localhost:6379" +CELERY_RESULT_BACKEND = CELERY_BROKER_URL +CELERY_ACCEPT_CONTENT = ["json"] +CELERY_TASK_SERIALIZER = "json" +CELERY_TASK_TRACK_STARTED = True diff --git a/static/styles/main.css b/static/styles/main.css index 07c66e4..0e037c5 100644 --- a/static/styles/main.css +++ b/static/styles/main.css @@ -1,8 +1,9 @@ :root { - --white: #fdfdff; + --white: #f8f8fb; --black: #393d3f; --grey: #a7b4bb; - --blue: #2288a2; + --blue: #10638c; + --red: #8c1016; } html { @@ -63,7 +64,7 @@ blockquote { border-left: 2px solid var(--blue); } -header { +body > header { padding: 1rem; } @@ -95,11 +96,14 @@ article { .navbar__mobile { display: none; + font-weight: 900; } -.navbar__menu { - +.navbar__mobile .menu__items { + text-align: center; } + + .navbar__menu_title { font-weight: 900; padding: 0.2rem 1rem; @@ -199,10 +203,16 @@ article { text-decoration: none; } +.action-button--danger { + background-color: var(--red); +} + /* FORMS */ input[type=text]:not([name=description]), input[type=number], +input[type=date], +input[type=time], input[type=password], input[type=search], textarea, select { @@ -223,10 +233,6 @@ input[name=description] { box-sizing: border-box; } -input:focus, textarea:focus, select:focus { - border-color: var(--blue); -} - input[type=radio], input[type=checkbox] { cursor: pointer; width: 1.5rem; @@ -256,6 +262,10 @@ li label { display: inline-block; } +input:focus, textarea:focus, select:focus { + border-color: var(--blue) !important; +} + @@ -300,4 +310,36 @@ hgroup { right: 0; text-align: left; } +} + + + +.log_entry { + display: grid; + grid-template-columns: 1fr 2fr 1fr; + column-gap: 0.5rem; + margin-bottom: 1rem; +} + +.log_entry--employee { + grid-template-columns: 1fr auto; +} + + +.today__event { + display: grid; + grid-template-columns: 1fr 2fr 0.5fr 0.5fr; + column-gap: 1rem; + margin-bottom: 2rem; + padding-bottom: 0.5rem; + border-bottom: 0.046rem solid var(--grey); +} + + + +.activity__item { + display: grid; + grid-template-columns: 0.25fr 2fr 0.25fr; + column-gap: 0.5rem; + margin-bottom: 1rem; } \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 61f2fc5..9ec42f9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -26,6 +26,7 @@ {% else %} + OnBoard Login {% endif %}