diff --git a/accounts/migrations/0003_user_preferences.py b/accounts/migrations/0003_user_preferences.py
new file mode 100644
index 0000000..505a3a2
--- /dev/null
+++ b/accounts/migrations/0003_user_preferences.py
@@ -0,0 +1,25 @@
+# Generated by Django 4.2.7 on 2023-11-29 02:51
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("preferences", "__first__"),
+ ("accounts", "0002_alter_user_table"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="user",
+ name="preferences",
+ field=models.OneToOneField(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="preferences.preferences",
+ ),
+ ),
+ ]
diff --git a/accounts/models.py b/accounts/models.py
index b756e6d..6f616bf 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -1,6 +1,7 @@
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _
-
+from django.db import models
+from preferences.models import Preferences
class User(AbstractUser):
class Meta:
@@ -10,3 +11,8 @@ class Meta:
def __str__(self):
return self.username
+
+ #store user preferences in a one to one mapping
+ preferences = models.OneToOneField(Preferences, on_delete=models.CASCADE, blank=True, null=True)
+
+
diff --git a/core/settings.py b/core/settings.py
index 894d1d4..62dd1e7 100644
--- a/core/settings.py
+++ b/core/settings.py
@@ -49,6 +49,7 @@
"homes",
"residents",
"work",
+ "preferences"
]
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
diff --git a/core/urls.py b/core/urls.py
index 7324fd3..8695127 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -47,5 +47,9 @@
"work/",
include("work.urls"),
),
+ path(
+ "preferences/",
+ include("preferences.urls"),
+ ),
path("", TemplateView.as_view(template_name="home.html"), name="home"),
]
diff --git a/manage.py b/manage.py
index 4729767..9b91b5d 100755
--- a/manage.py
+++ b/manage.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
"""Django's command-line utility for administrative tasks."""
import os
import sys
diff --git a/preferences/forms.py b/preferences/forms.py
new file mode 100644
index 0000000..d3be361
--- /dev/null
+++ b/preferences/forms.py
@@ -0,0 +1,14 @@
+from django import forms
+from django.forms import ModelForm
+from preferences.models import Preferences
+
+class PreferencesForm (ModelForm):
+ class Meta:
+ model = Preferences
+ fields = ('Language', 'Mode')
+ widgets = {
+ 'Language': forms.Select(attrs={'class': 'form-control'}),
+ 'Mode': forms.Select(attrs={'class': 'form-control'}),
+ }
+ labels = {'Language': 'Preferred Language',
+ 'Mode': 'Preferred Mode'}
\ No newline at end of file
diff --git a/preferences/models.py b/preferences/models.py
new file mode 100644
index 0000000..3c40397
--- /dev/null
+++ b/preferences/models.py
@@ -0,0 +1,17 @@
+from django.db import models
+
+from django.utils.translation import gettext_lazy as _
+from django.conf import settings
+
+# for user preferences
+class Preferences (models.Model):
+ class LanguageTextChoices(TextChoices):
+ ENGLISH = "english", _("English")
+ SUOMI = "suomi", _("Suomi")
+
+ class ColorModeTextChoices(TextChoices):
+ DARK = "dark", _("Dark")
+ LIGHT = "light", _("Light")
+
+ language = models.CharField (max_length = 30, blank=True, choices=LanguageTextChoices)
+ color_mode = models.CharField (max_length = 30, blank=True, choices=ColorModeTextChoices)
diff --git a/preferences/templates/preferences.html b/preferences/templates/preferences.html
new file mode 100644
index 0000000..459a3cd
--- /dev/null
+++ b/preferences/templates/preferences.html
@@ -0,0 +1,46 @@
+{% extends 'base.html' %}
+
+{% load i18n %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
+
+
+
+
Your Current Preferences
+
+ {% if not request.user.preferences %}
+
No current preferences. Set some!
+ {% else %}
+ {% for name, value in fields %}
+ {% if name != "id" %}
+ {{ name }} : {{ value }}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+
+
+
+
+
+{% endblock content %}
+
diff --git a/preferences/urls.py b/preferences/urls.py
new file mode 100644
index 0000000..9056a19
--- /dev/null
+++ b/preferences/urls.py
@@ -0,0 +1,10 @@
+from django.urls import path
+from .views import setPreferences
+
+urlpatterns = [
+ path(
+ "",
+ setPreferences,
+ name="preferences-view",
+ ),
+]
diff --git a/preferences/views.py b/preferences/views.py
new file mode 100644
index 0000000..3e9977c
--- /dev/null
+++ b/preferences/views.py
@@ -0,0 +1,43 @@
+from django.shortcuts import render, redirect, reverse
+from django.contrib.auth.decorators import login_required
+from preferences.forms import PreferencesForm
+from preferences.models import Preferences
+from django.core import serializers
+
+# this function creates a blank preference model form on GET request
+# and returns a context with the form and the fields from the preference
+# object tagged to the current user
+# on non GET request we update the database to save the new preferences
+@login_required
+def setPreferences (request):
+ context = {}
+ if request.method == 'GET':
+ context['form'] = PreferencesForm ()
+ # null check for preferences field
+ if request.user.preferences:
+ fields = [(field.name, field.value_to_string(request.user.preferences)) for field in Preferences._meta.fields]
+ context['fields'] = fields
+ return render(request, '../templates/preferences.html', context)
+
+ # for any other request type that is not GET
+ form = PreferencesForm(request.POST)
+ context['form'] = form
+
+ if not form.is_valid():
+ return render(request, '../templates/preferences.html', context)
+
+ preferences = Preferences(
+ Language = form.cleaned_data['Language'],
+ Mode = form.cleaned_data['Mode'],
+ )
+
+ preferences.save ()
+
+ request.user.preferences = preferences
+ request.user.save ()
+
+ # extract the fields to be displayed from the preferences
+ fields = [(field.name, field.value_to_string(request.user.preferences)) for field in Preferences._meta.fields]
+ context['fields'] = fields
+
+ return render(request, '../templates/preferences.html', context)
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
index a9fa700..dd2ccdf 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -11,7 +11,7 @@
{% block extra_css %}{% endblock extra_css %}
-
+
{% include "navigation.html" %}
@@ -24,3 +24,4 @@
+
diff --git a/templates/navigation.html b/templates/navigation.html
index 11befa7..43b1413 100644
--- a/templates/navigation.html
+++ b/templates/navigation.html
@@ -40,6 +40,12 @@
{% translate "Residents" %}
+
+
+
+ {% translate "Preferences" %}
+
+
@@ -88,3 +116,39 @@
+
+
diff --git a/templates/tests/toggle_tests.js b/templates/tests/toggle_tests.js
new file mode 100644
index 0000000..760e24e
--- /dev/null
+++ b/templates/tests/toggle_tests.js
@@ -0,0 +1,61 @@
+
+/*
+To run these tests, do the following, add testToggle() to the eventListeners
+for the dropdown so that this function runs every time a user uses the toggle.
+If the console raises errors, then the tests do not pass.
+*/
+
+function testToggle() {
+ const currTheme = document.documentElement.getAttribute('data-bs-theme');
+
+ // SCENARIO 1: dark mode should change to light mode
+ if (currTheme === 'dark') {
+ // check if data-bs-theme attribute actually changes to dark mode
+ toggleTheme('light');
+ assert(document.documentElement.getAttribute('data-bs-theme') === 'light');
+
+ // check if the text label correctly switches
+ assert(document.getElementById('theme-label').innerHTML === 'Light')
+
+ // check if the correct option is highlighted in the dropdown
+ changeActiveStatus(lightMode, darkMode);
+ assert('active' in document.getElementById('light-dropdown').getAttribute('class'))
+ assert(!('active' in document.getElementById('dark-dropdown').getAttribute('class')))
+ }
+
+ // SCENARIO 2: dark mode should remain if "dark" is selected
+ if (currTheme === 'dark') {
+ toggleTheme('dark');
+ assert(document.documentElement.getAttribute('data-bs-theme') === 'dark');
+
+ assert(document.getElementById('theme-label').innerHTML === 'Dark')
+
+ changeActiveStatus(lightMode, darkMode);
+ assert(!('active' in document.getElementById('light-dropdown').getAttribute('class')))
+ assert('active' in document.getElementById('dark-dropdown').getAttribute('class'))
+ }
+
+ // SCENARIO 3: light mode should change to dark mode
+ if (currTheme === 'light') {
+ toggleTheme('dark');
+ assert(document.documentElement.getAttribute('data-bs-theme') === 'dark');
+
+ assert(document.getElementById('theme-label').innerHTML === 'Dark')
+
+ changeActiveStatus(darkMode, lightMode);
+ assert('active' in document.getElementById('dark-dropdown').getAttribute('class'))
+ assert(!('active' in document.getElementById('light-dropdown').getAttribute('class')))
+ }
+
+ // SCENARIO 4: light mode should remain if "light" is selected
+ if (currTheme === 'light') {
+ toggleTheme('light');
+ assert(document.documentElement.getAttribute('data-bs-theme') === 'light');
+
+ assert(document.getElementById('theme-label').innerHTML === 'Light')
+
+ changeActiveStatus(lightMode, lightMode);
+ assert('active' in document.getElementById('light-dropdown').getAttribute('class'))
+ assert(!('active' in document.getElementById('dark-dropdown').getAttribute('class')))
+ }
+}