Browse Source

Resolved some responsive design issues. Also have the basic time functionality working.

Adam Day 4 years ago
parent
commit
9666fe9d60

+ 13 - 1
app/admin.py

@@ -1,5 +1,5 @@
 from django.contrib import admin
-from . models import User, Project, Message, Entry
+from . models import User, Project, Message, Entry, Setting, EntryNote
 # Register your models here.
 
 
@@ -20,6 +20,18 @@ class EntryAdmin(admin.ModelAdmin):
     pass
 
 
+@admin.register(EntryNote)
+class EntryNoteAdmin(admin.ModelAdmin):
+    pass
+
+
 @admin.register(Message)
 class MessageAdmin(admin.ModelAdmin):
     pass
+
+
+@admin.register(Setting)
+class SettingAdmin(admin.ModelAdmin):
+    readonly_fields = ['setting']
+    ordering = ['setting']
+    list_display = ['setting', 'value']

+ 35 - 5
app/forms.py

@@ -1,9 +1,29 @@
 from django import forms
 from django.core.validators import RegexValidator, MaxLengthValidator
-from . models import Project
+from . models import Project, Setting
 
 numeric = RegexValidator(r'^[0-9+]', 'Only numeric characters.')
 time_max_length = MaxLengthValidator(4, 'Length limit exceeds 4 characters')
+max_daily_hours_length = MaxLengthValidator(2, 'Only 2 place values allowed')
+session_timeout_length = MaxLengthValidator(4, 'Only 4 place values allowed')
+
+hours = []
+try:
+    max_daily_hours = Setting.objects.get(setting='Max Daily Hours')
+    for hour in range(0, int(max_daily_hours.value)+1):
+        hours.append(("%i" % hour, "%i" % hour))
+except Exception as e:
+    print(e)
+    for hour in range(0, 9):
+        hours.append(("%i" % hour, "%i" % hour))
+
+
+minutes = (
+    ('0', '0'),
+    ('0.25', '15'),
+    ('0.50', '30'),
+    ('0.75', '45')
+)
 
 
 class LoginForm(forms.Form):
@@ -24,8 +44,18 @@ class TimeEntryForm(forms.Form):
     project = forms.ModelChoiceField(Project.objects.all(), required=False, widget=forms.Select(attrs={
         'class': 'form-control form-control-lg'
     }))
-    time_worked = forms.CharField(required=True, label="Hours Worked", widget=forms.TextInput(attrs={
+
+    hours = forms.ChoiceField(required=True, choices=hours, widget=forms.Select(attrs={
         'class': 'form-control form-control-lg',
-        'max-length': '4',
-        'placeholder': 'Examples: 0.25, 1.5, 2'
-    }), validators=[time_max_length, numeric])
+    }))
+
+    minutes = forms.ChoiceField(required=True, choices=minutes, widget=forms.Select(attrs={
+        'class': 'form-control form-control-lg',
+    }))
+
+
+class SettingsForm(forms.Form):
+    max_daily_hours = forms.CharField(required=True, validators=[max_daily_hours_length])
+    session_timeout = forms.CharField(required=True, validators=[session_timeout_length])
+    allow_entry_edit = forms.BooleanField(required=True)
+

+ 28 - 4
app/models.py

@@ -37,12 +37,28 @@ class Project(models.Model):
 
 
 class Entry(models.Model):
-    user_id = models.IntegerField(blank=False, editable=False)
+    user = models.ForeignKey(User, on_delete=models.DO_NOTHING, default=None, null=True)
     date = models.DateField(blank=False)
-    project = models.ForeignKey(Project, on_delete=models.CASCADE, blank=True)
-    time_worked = models.CharField(max_length=4, default=0, blank=False)
-    note = models.TextField(blank=True)
+    project = models.ForeignKey(Project, on_delete=models.CASCADE, blank=True, null=True)
+    hours = models.CharField(max_length=2, default=0, blank=False)
+    minutes = models.CharField(max_length=4, default=0, blank=False)
 
+    def __str__(self):
+        return "%s, %s %s" % (self.date, self.user.last_name, self.user.first_name)
+
+    class Meta:
+        verbose_name_plural = "Time Entries"
+
+
+class EntryNote(models.Model):
+    entry = models.ForeignKey(Entry, on_delete=models.CASCADE)
+    note = models.TextField(blank=False)
+
+    def __str__(self):
+        return "%s %s, %s - %s" % (self.entry.date, self.entry.user.last_name, self.entry.user.first_name, self.note)
+
+    class Meta:
+        verbose_name_plural = "Time Entry Notes"
 
 class Message(models.Model):
     message = models.TextField(blank=False)
@@ -51,3 +67,11 @@ class Message(models.Model):
 
     def __str__(self):
         return "%s" % (self.message,)
+
+
+class Setting(models.Model):
+    setting = models.CharField(max_length=255, blank=False, default="")
+    value = models.CharField(max_length=255, blank=False, default="")
+
+    def __str__(self):
+        return "%s" % (self.setting,)

+ 9 - 8
app/templates/forms/timesheet_entry.html

@@ -1,18 +1,19 @@
 {% load crispy_forms_tags %}
 
-<form action="" method="post">
+<form action="{% url 'timesheet' %}" method="post">
     <div class="row">
-        <div class="col-sm-12 col-md-6">
+        <div class="col-sm-12 col-md-12">
             {{ form.project | as_crispy_field }}
         </div>
-        <div class="col-sm-12 col-md-6">
-            {{ form.time_worked | as_crispy_field }}
+        <div class="col-sm-12 col-md-4">
+            {{ form.hours | as_crispy_field }}
         </div>
-        <div class="col-12">
-            {% include 'tools/keypad.html' %}
+        <div class="col-sm-12 col-md-4">
+            {{ form.minutes | as_crispy_field }}
         </div>
-        <div class="col-12 text-center">
-            <input type="submit" value="Submit" class="btn btn-lg btn-dark">
+        <div class="col-sm-12 col-md-4">
+            <label class="form-label">&nbsp;</label>
+            <input type="submit" value="Submit" class="form-control form-control-lg btn btn-lg btn-dark">
         </div>
         {% csrf_token %}
     </div>

+ 2 - 2
app/templates/layout.html

@@ -16,12 +16,12 @@
     <div class="d-flex align-items-center min-vh-100">
         <div class="container-fluid">
             <div class="row justify-content-center">
-                <div class="col-12 text-center mb-3">
+                <div class="col-12 text-center mt-5 mb-3">
                     <h1 class="display-3"><span class="oi" data-glyph="clock"></span> Time Sheet Station</h1>
                 </div>
             </div>
             <div class="row justify-content-center">
-                <div class="col-sm-12 col-md-6 col-lg-5">
+                <div class="col-sm-12 col-md-7">
                     <div class="card shadow">
                         <div class="card-body">
                             <div class="row">

+ 18 - 0
app/templates/setup.html

@@ -0,0 +1,18 @@
+{% extends 'layout.html' %}
+{% load crispy_forms_tags %}
+{% block content %}
+
+    <div class="row">
+        <div class="col-12 text-center">
+            <h1>System Setup</h1>
+        </div>
+        <div class="col-12">
+            <form action="{% url 'setup' %}" method="post">
+                {{ form | crispy }}
+                {% csrf_token %}
+                <input type="submit" class="form-control btn btn-lg btn-dark" value="Submit">
+            </form>
+        </div>
+    </div>
+
+{% endblock %}

+ 53 - 5
app/templates/timesheet.html

@@ -3,20 +3,68 @@
 {% block content %}
 
     <div class="row mb-3">
-        <div class="col-sm-12 col-md-6">
+        <div class="col-sm-12 col-md-12 col-lg-6 text-sm-center text-lg-start">
             <h2>{{ user.first_name | title }} {{ user.last_name | title }}</h2>
         </div>
-        <div class="col-sm-12 col-md-6 text-end">
+        <div class="col-sm-12 col-md-12 col-lg-6 text-sm-center text-lg-end">
             {% if user.level == '3' %}
             <a href="#!" class="btn btn-lg btn-secondary"><span class="oi" data-glyph="wrench"></span></a>
             {% endif %}
-            <a href="#!" class="btn btn-lg btn-secondary">Reports</a>
-            <a href="{% url 'logout' %}" class="btn btn-lg btn-danger">Logout</a>
+            <a href="#!" class="btn btn-lg btn-secondary"><span class="oi" data-glyph="document"></span></a>
+            <a href="{% url 'logout' %}" class="btn btn-lg btn-danger"><span class="oi" data-glyph="account-logout"></span></a>
+        </div>
+    </div>
+    <div class="row mb-3">
+        <div class="col-12">
+            <div class="card">
+                <div class="card-body">
+                    {% include 'forms/timesheet_entry.html' %}
+                </div>
+            </div>
         </div>
     </div>
     <div class="row">
         <div class="col-12">
-            {% include 'forms/timesheet_entry.html' %}
+            <div class="card">
+                <div class="card-body">
+                    <div class="row mb-3 border-bottom">
+                        <div class="col-sm-6">
+                            <h3>Total Time Worked</h3>
+                        </div>
+                        <div class="col-sm-6">
+                            <h3>{{ total_time_worked }}</h3>
+                        </div>
+                    </div>
+                    <div class="row">
+                        <div class="col-4">
+                            <b>Date</b>
+                        </div>
+                        <div class="col-4">
+                            <b>Time Worked</b>
+                        </div>
+                        <div class="col-4">
+                            <b>Project</b>
+                        </div>
+                    </div>
+                    {% for entry in entries %}
+                        <div class="row report-row p-3">
+                            <div class="col-sm-12 col-md-4">
+                                {{ entry.date }}
+                            </div>
+                            <div class="col-sm-6 col-md-4">
+                                {{ entry.time_worked }}
+                            </div>
+                            <div class="col-sm-6 col-md-4">
+                                {% if entry.project %}
+                                    {{ entry.project.name }}
+                                {% else %}
+                                    -
+                                {% endif %}
+                            </div>
+                        </div>
+                    {% endfor %}
+                </div>
+            </div>
         </div>
     </div>
 

+ 12 - 12
app/templates/tools/keypad.html

@@ -4,27 +4,27 @@
     <div class="card shadow-sm">
         <div class="card-body">
             <div class="row mb-3 justify-content-center g-0">
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="1" data-alt-value="!" class="btn btn-lg btn-light border keyboard-key">1</a></div>
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="2" data-alt-value="@" class="btn btn-lg btn-light border keyboard-key">2</a></div>
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="3" data-alt-value="#" class="btn btn-lg btn-light border keyboard-key">3</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="1" data-alt-value="!" class="btn btn-lg btn-light border border-secondary keyboard-key">1</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="2" data-alt-value="@" class="btn btn-lg btn-light border border-secondary keyboard-key">2</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="3" data-alt-value="#" class="btn btn-lg btn-light border border-secondary keyboard-key">3</a></div>
             </div>
 
             <div class="row mb-3 justify-content-center g-0">
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="4" data-alt-value="$" class="btn btn-lg btn-light border keyboard-key">4</a></div>
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="5" data-alt-value="%" class="btn btn-lg btn-light border keyboard-key">5</a></div>
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="6" data-alt-value="^" class="btn btn-lg btn-light border keyboard-key">6</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="4" data-alt-value="$" class="btn btn-lg btn-light border border-secondary keyboard-key">4</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="5" data-alt-value="%" class="btn btn-lg btn-light border border-secondary keyboard-key">5</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="6" data-alt-value="^" class="btn btn-lg btn-light border border-secondary keyboard-key">6</a></div>
             </div>
 
             <div class="row mb-3 justify-content-center g-0">
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="7" data-alt-value="&" class="btn btn-lg btn-light border keyboard-key">7</a></div>
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="8" data-alt-value="*" class="btn btn-lg btn-light border keyboard-key">8</a></div>
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="9" data-alt-value="(" class="btn btn-lg btn-light border keyboard-key">9</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="7" data-alt-value="&" class="btn btn-lg btn-light border border-secondary keyboard-key">7</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="8" data-alt-value="*" class="btn btn-lg btn-light border border-secondary keyboard-key">8</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="9" data-alt-value="(" class="btn btn-lg btn-light border border-secondary keyboard-key">9</a></div>
             </div>
 
             <div class="row mb-3 g-0">
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="0" data-alt-value=")" class="btn btn-lg btn-light border keyboard-key">0</a></div>
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="." data-alt-value=")" class="btn btn-lg btn-light border keyboard-key">.</a></div>
-                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" id="back-key" data-value="back" class="btn btn-lg btn-light border">Delete</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="0" data-alt-value=")" class="btn btn-lg btn-light border border-secondary keyboard-key">0</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" data-value="." data-alt-value=")" class="btn btn-lg btn-light border border-secondary keyboard-key">.</a></div>
+                <div class="col-3 d-grid gap-2 mx-auto"><a href="#!" id="back-key" data-value="back" class="btn btn-lg btn-light border border-secondary">Delete</a></div>
             </div>
         </div>
     </div>

+ 1 - 0
app/urls.py

@@ -18,6 +18,7 @@ from . import views
 
 urlpatterns = [
     path('', views.home, name='home'),
+    path('setup', views.setup, name='setup'),
     path('timesheet', views.timesheet, name="timesheet"),
     path('logout', views.logout_user, name="logout"),
     path('create/user', views.create_user, name="create_user"),

+ 100 - 24
app/views.py

@@ -1,25 +1,73 @@
 from django.shortcuts import render, redirect
-from . forms import LoginForm, CreateUserForm, TimeEntryForm
-from . models import User
+from . forms import LoginForm, CreateUserForm, TimeEntryForm, SettingsForm
+from . models import User, Setting, Entry
 from hashlib import sha256
+import datetime
 
 
 def hash_pin(pin):
     return sha256(pin.encode('utf-8')).hexdigest()
 
 
+def get_user(uid):
+    user = User.objects.get(id=uid)
+    return user
+
+
+def check_setup():
+    settings = Setting.objects.all()
+    if len(settings) > 0:
+        return True
+    else:
+        return False
+
+
 def logout_user(request):
-    del request.session['authenticated']
+    request.session['authenticated'] = False
     return redirect('home')
 
 
 def requires_auth(request):
-    return request.session.get('authenticated')
+    auth = request.session.get('authenticated', None)
+    if auth is True:
+        return True
+    else:
+        return False
 
 
-def get_user(uid):
-    user = User.objects.get(id=uid)
-    return user
+def setup(request):
+    if check_setup() is True:
+        return redirect('home')
+
+    form = SettingsForm()
+
+    if request.method == "POST":
+        form = SettingsForm(request.POST)
+        if form.is_valid():
+            data = form.cleaned_data
+
+            s = Setting()
+            s.setting = 'Max Daily Hours'
+            s.value = data['max_daily_hours']
+            s.save()
+
+            s = Setting()
+            s.setting = 'Session Timeout'
+            s.value = data['session_timeout']
+            s.save()
+
+            s = Setting()
+            s.setting = 'Allow Entry Edit'
+            s.value = data['allow_entry_edit']
+            s.save()
+
+            return redirect('home')
+
+    context = {
+        'form': form
+    }
+
+    return render(request, 'setup.html', context=context)
 
 
 def create_user(request):
@@ -47,6 +95,8 @@ def create_user(request):
 
 
 def home(request):
+    if check_setup() is False:
+        return redirect('setup')
     form = LoginForm
     login_error = False
     if request.method == "POST":
@@ -55,7 +105,6 @@ def home(request):
             data = form.cleaned_data
             pin = sha256(data['pin'].encode('utf-8')).hexdigest()
             user = User.objects.filter(pin=pin).first()
-            print(user)
             if user is None:
                 form.add_error('pin', 'Invalid login')
                 login_error = True
@@ -73,24 +122,51 @@ def home(request):
 
 
 def timesheet(request):
-    if requires_auth(request):
-        uid = request.session.get('uid')
-        user = get_user(uid)
+    if requires_auth(request) is False:
+        request.session['authenticated'] = False
+        return redirect('home')
 
-        form = TimeEntryForm()
+    uid = request.session.get('uid')
+    user = get_user(uid)
 
-        if request.method == "POST":
-            form = TimeEntryForm(request.POST)
-            if form.is_valid():
-                data = form.cleaned_data
-                print(data)
+    form = TimeEntryForm()
 
-        context = {
-            'user': user,
-            'form': form,
+    if request.method == "POST":
+        form = TimeEntryForm(request.POST)
+        if form.is_valid():
+            data = form.cleaned_data
+            print(data)
+            entry = Entry()
+            entry.user = user
+            entry.project = data['project']
+            entry.date = datetime.datetime.now().date()
+            entry.hours = data['hours']
+            entry.minutes = data['minutes']
+            entry.save()
+
+            form = TimeEntryForm()
+
+    entries = Entry.objects.filter(user__id=uid)
+
+    time_entries = list()
+    total_time_worked = 0
+    for entry in entries:
+        time_worked = float(entry.hours) + float(entry.minutes)
+        e = {
+            'date': entry.date,
+            'hours': entry.hours,
+            'minutes': entry.minutes,
+            'project': entry.project,
+            'time_worked': time_worked,
         }
+        time_entries.append(e)
+        total_time_worked = float(total_time_worked) + time_worked
 
-        return render(request, 'timesheet.html', context=context)
-    else:
-        request.session['authenticated'] = False
-        return redirect('home')
+    context = {
+        'user': user,
+        'form': form,
+        'entries': time_entries,
+        'total_time_worked': total_time_worked,
+    }
+
+    return render(request, 'timesheet.html', context=context)

+ 2 - 0
requirements.txt

@@ -1,4 +1,6 @@
 asgiref==3.3.1
+crispy-bootstrap5==0.2
 Django==3.1.6
+django-crispy-forms==1.11.0
 pytz==2021.1
 sqlparse==0.4.1

+ 4 - 0
static/app/css/app.css

@@ -6,4 +6,8 @@ body {
 
 .keyboard > button {
     font-size:4em;
+}
+
+.report-row:nth-of-type(odd) {
+    background: #e0e0e0;
 }