diff --git a/tin/apps/users/forms.py b/tin/apps/users/forms.py index 38c7ed1c..258653f9 100644 --- a/tin/apps/users/forms.py +++ b/tin/apps/users/forms.py @@ -2,7 +2,15 @@ from django import forms +from tin.apps.users.models import User + class UserMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, user): # pylint: disable=arguments-differ return f"{user.full_name} ({user.username})" + + +class ThemeForm(forms.ModelForm): + class Meta: + model = User + fields = ["dark_mode"] diff --git a/tin/apps/users/migrations/0003_user_dark_mode.py b/tin/apps/users/migrations/0003_user_dark_mode.py new file mode 100644 index 00000000..dba18dff --- /dev/null +++ b/tin/apps/users/migrations/0003_user_dark_mode.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.15 on 2024-09-04 12:28 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_remove_user_is_sysadmin'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='dark_mode', + field=models.PositiveIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(1)]), + ), + ] diff --git a/tin/apps/users/models.py b/tin/apps/users/models.py index a5e90d56..4701aa0a 100644 --- a/tin/apps/users/models.py +++ b/tin/apps/users/models.py @@ -5,6 +5,7 @@ import requests from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin from django.contrib.auth.models import UserManager as DjangoUserManager +from django.core.validators import MaxValueValidator from django.db import models from django.utils import timezone from social_django.utils import load_strategy @@ -30,6 +31,8 @@ class User(AbstractBaseUser, PermissionsMixin): is_teacher = models.BooleanField(default=False) is_student = models.BooleanField(default=False) date_joined = models.DateTimeField(default=timezone.now) + # 0 = Light mode, 1 = Dark Mode + dark_mode = models.PositiveIntegerField(default=0, validators=[MaxValueValidator(1)]) USERNAME_FIELD = "username" REQUIRED_FIELDS = ["email"] diff --git a/tin/apps/users/tests.py b/tin/apps/users/tests.py new file mode 100644 index 00000000..745ab947 --- /dev/null +++ b/tin/apps/users/tests.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import json + +from django.urls import reverse + +from tin.tests import login + + +@login("student") +def test_user_dark_mode(client, student): + assert student.dark_mode == 0 + response = client.post(reverse("users:theme"), {"dark_mode": 1}) + assert json.loads(response.content.decode("utf-8")).get("success") is True + student.refresh_from_db() + assert student.dark_mode == 1 diff --git a/tin/apps/users/urls.py b/tin/apps/users/urls.py new file mode 100644 index 00000000..5d5e3334 --- /dev/null +++ b/tin/apps/users/urls.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from django.urls import path + +from . import views + +app_name = "users" + +urlpatterns = [ + path("theme/", views.change_theme, name="theme"), +] diff --git a/tin/apps/users/views.py b/tin/apps/users/views.py new file mode 100644 index 00000000..e4dc6aa2 --- /dev/null +++ b/tin/apps/users/views.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from django import http +from django.contrib.auth.decorators import login_required + +from tin.apps.users.forms import ThemeForm + + +@login_required +def change_theme(request): + """Sets the color theme""" + if request.method == "POST": + form = ThemeForm(request.POST) + if form.is_valid(): + request.user.dark_mode = form.cleaned_data["dark_mode"] + request.user.save() + return http.JsonResponse({"success": True}) + else: + return http.JsonResponse( + {"success": False, "errors": form.errors.as_json()}, status=400 + ) + raise http.Http404 diff --git a/tin/static/css/base.css b/tin/static/css/base.css index b7c2a7f9..636a8e54 100644 --- a/tin/static/css/base.css +++ b/tin/static/css/base.css @@ -7,6 +7,7 @@ color: white; line-height: 40px; background: #4fab4f; + position: relative; } #nav ul { @@ -413,3 +414,19 @@ ul.errors { white-space: pre-wrap; } } + +#theme-toggle { + width: 30px; + height: 30px; +} + +.theme-toggle-button { + background: transparent; + border: none; + cursor: pointer; + padding: 0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-85%, 12%); +} diff --git a/tin/static/css/dark/base.css b/tin/static/css/dark/base.css new file mode 100644 index 00000000..1e307f0e --- /dev/null +++ b/tin/static/css/dark/base.css @@ -0,0 +1,32 @@ +#nav { + color: white; + background: #4fab4f; +} + +body { + background-color: #182c25; +} + +#footer { + background-color: #182c25; + border-top: #182c25; + color: white; +} + +#main { + color: white; +} + +ul#course-list > li a:not(.tin-btn), +ul#assignment-list > li a { + color: white; +} + +a:not(.tin-btn) { + color: white; +} + +table.has-border th, +table.has-border td { + border: 1px solid #ffffff; +} diff --git a/tin/static/css/dark/edit.css b/tin/static/css/dark/edit.css new file mode 100644 index 00000000..7dbe4021 --- /dev/null +++ b/tin/static/css/dark/edit.css @@ -0,0 +1,3 @@ +.content .field { + color: black; +} diff --git a/tin/templates/base.html b/tin/templates/base.html index 7629a206..d33f30b7 100644 --- a/tin/templates/base.html +++ b/tin/templates/base.html @@ -8,6 +8,10 @@ {% include "meta.html" %} + {% if request.user.dark_mode %} + + + {% endif %} @@ -44,8 +48,56 @@ $(".continuous-progress").css({height: "15px"}).progressbar({value: false}) }); + {% block head %}{% endblock %} +
+ + + Light mode + + + + + + + + + + + + + + + + Dark mode + + + + + + +
@@ -90,6 +142,13 @@ {% endif %}
  • Logout ({{ request.user.username }})
  • +
  • + +
  • {% endif %} diff --git a/tin/urls.py b/tin/urls.py index a7934de6..83a7f1d9 100644 --- a/tin/urls.py +++ b/tin/urls.py @@ -32,6 +32,7 @@ path("docs/", include("tin.apps.docs.urls", namespace="docs")), path("", include("tin.apps.auth.urls", namespace="auth")), path("", include("social_django.urls", namespace="social")), + path("", include("tin.apps.users.urls", namespace="users")), ] handler404 = handle_404_view