From b5bbdc9dfbf3959ebb5ffd3a31c679c273d236b1 Mon Sep 17 00:00:00 2001 From: Katie Worton Date: Tue, 21 Nov 2023 19:10:52 +0000 Subject: [PATCH] core/models,frontend/user_settings: Add user preference for failures only Add support for a user preference for the "failures only" field in the build results screen. When the user is logged in, they can set a user preference in their profile for this "failures only" field. The default for this field is for the "failures only" tickbox to be checked. Signed-off-by: Katie Worton --- squad/core/migrations/0169_userpreferences.py | 38 +++++++++++++++++ squad/core/models.py | 5 +++ .../templates/squad/user_settings/base.jinja2 | 1 + .../user_settings/user_preferences.jinja2 | 12 ++++++ squad/frontend/user_settings.py | 27 +++++++++++- squad/frontend/views.py | 20 ++++++++- test/frontend/test_tests.py | 42 ++++++++++++++++++- 7 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 squad/core/migrations/0169_userpreferences.py create mode 100644 squad/frontend/templates/squad/user_settings/user_preferences.jinja2 diff --git a/squad/core/migrations/0169_userpreferences.py b/squad/core/migrations/0169_userpreferences.py new file mode 100644 index 000000000..b9d5de60f --- /dev/null +++ b/squad/core/migrations/0169_userpreferences.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.4 on 2023-11-21 17:54 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("core", "0168_add_group_settings"), + ] + + operations = [ + migrations.CreateModel( + name="UserPreferences", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("display_failures_only", models.BooleanField(default=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/squad/core/models.py b/squad/core/models.py index 7a6576501..b583c0387 100644 --- a/squad/core/models.py +++ b/squad/core/models.py @@ -248,6 +248,11 @@ class Meta: proxy = True +class UserPreferences(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + display_failures_only = models.BooleanField(default=True) + + class ProjectManager(models.Manager): def accessible_to(self, user): diff --git a/squad/frontend/templates/squad/user_settings/base.jinja2 b/squad/frontend/templates/squad/user_settings/base.jinja2 index 390b634de..9fb664219 100644 --- a/squad/frontend/templates/squad/user_settings/base.jinja2 +++ b/squad/frontend/templates/squad/user_settings/base.jinja2 @@ -13,6 +13,7 @@
  • {{ _('Personal projects') }}
  • {{ _('API token') }}
  • {{ _('Subscriptions') }}
  • +
  • {{ _('User Preferences') }}
  • diff --git a/squad/frontend/templates/squad/user_settings/user_preferences.jinja2 b/squad/frontend/templates/squad/user_settings/user_preferences.jinja2 new file mode 100644 index 000000000..4f267aeb0 --- /dev/null +++ b/squad/frontend/templates/squad/user_settings/user_preferences.jinja2 @@ -0,0 +1,12 @@ +{% extends "squad/user_settings/base.jinja2" %} +{% block settings %} +

    {{ _('User Preferences') }}

    + +
    + {{ csrf_input }} + {{crispy(form)}} + +
    + +{% endblock %} + diff --git a/squad/frontend/user_settings.py b/squad/frontend/user_settings.py index 42023a379..e783ea54e 100644 --- a/squad/frontend/user_settings.py +++ b/squad/frontend/user_settings.py @@ -13,7 +13,8 @@ from rest_framework.authtoken.models import Token -from squad.core.models import Group, Project, Subscription, UserNamespace +from squad.core.models import Group, Project, Subscription, UserNamespace, UserPreferences +from squad.frontend.views import get_user_preferences logger = logging.getLogger() @@ -30,6 +31,29 @@ class Meta: fields = ['first_name', 'last_name', 'email'] +class UserPreferencesForm(forms.ModelForm): + class Meta: + model = UserPreferences + fields = ['display_failures_only'] + + +@login_required +def user_preferences(request): + preferences = get_user_preferences(request.user) + if request.method == "POST": + form = UserPreferencesForm(request.POST, instance=preferences) + if form.is_valid(): + form.save() + return redirect(request.path) + else: + form = UserPreferencesForm(instance=preferences) + + context = { + 'form': form, + } + return render(request, 'squad/user_settings/user_preferences.jinja2', context) + + @login_required def profile(request): if request.method == "POST": @@ -126,6 +150,7 @@ def projects(request): url('^profile/$', profile, name='settings-profile'), url('^api-token/$', api_token, name='settings-api-token'), url('^subscriptions/$', subscriptions, name='settings-subscriptions'), + url('^user-preferences/$', user_preferences, name='settings-user-preferences'), url(r'^remove-subscription/(?P\d+)$', remove_subscription, name='settings-subscription-remove'), url(r'^remove-subscription/$', remove_subscription, name='settings-subscription-remove-post'), url('^projects/$', projects, name='settings-projects'), diff --git a/squad/frontend/views.py b/squad/frontend/views.py index 37b698050..e4134d775 100644 --- a/squad/frontend/views.py +++ b/squad/frontend/views.py @@ -12,7 +12,7 @@ from squad.ci.models import TestJob from squad.core.models import Group, Metric, ProjectStatus, Status, MetricThreshold, KnownIssue, Test -from squad.core.models import Build, Subscription, TestRun, SuiteMetadata +from squad.core.models import Build, Subscription, TestRun, SuiteMetadata, UserPreferences from squad.core.queries import get_metric_data, test_confidence from squad.frontend.queries import get_metrics_list from squad.frontend.utils import file_type, alphanum_sort @@ -29,6 +29,18 @@ def __init__(self, date, days): super(BuildDeleted, self).__init__(msg) +def get_user_preferences(user): + if user.is_authenticated: + try: + preferences = UserPreferences.objects.get(user=user) + except UserPreferences.DoesNotExist: + preferences = UserPreferences.objects.create(user=user) + else: + return None + + return preferences + + def get_build(project, version): if version == 'latest-finished': status = ProjectStatus.objects.prefetch_related('build').filter( @@ -362,7 +374,11 @@ def build(request, group_slug, project_slug, version): project = request.project build = get_build(project, version) - failures_only = request.GET.get('failures_only', 'true') + user_default_failures_only = "" + user_preferences = get_user_preferences(request.user) + if user_preferences: + user_default_failures_only = str(user_preferences.display_failures_only).lower() + failures_only = request.GET.get('failures_only', user_default_failures_only) if failures_only not in ['true', 'false']: failures_only = 'true' diff --git a/test/frontend/test_tests.py b/test/frontend/test_tests.py index cf43228ab..8c9ca4b7c 100644 --- a/test/frontend/test_tests.py +++ b/test/frontend/test_tests.py @@ -4,6 +4,8 @@ from django.test import Client import json +from squad.frontend.views import get_user_preferences + tests_file = { ("suite1/test%d" % i): "fail" for i in range(0, 50) } @@ -148,18 +150,54 @@ def setUp(self): def test_table_layout(self): response = self.client.get('/mygroup/myproject/build/1/?results_layout=table') + self.assertIn("onclick=\"window.location = \'?results_layout=table&failures_only=false#test-results'\"", response.content.decode("utf-8")) self.assertEqual(200, response.status_code) - def test_table_layout_failures_only(self): + def test_table_layout_failures_only_false(self): response = self.client.get('/mygroup/myproject/build/1/?results_layout=table&failures_only=false') + self.assertIn("onclick=\"window.location = \'?results_layout=table&failures_only=true#test-results'\"", response.content.decode("utf-8")) + self.assertEqual(200, response.status_code) + + def test_table_layout_failures_only_true(self): + response = self.client.get('/mygroup/myproject/build/1/?results_layout=table&failures_only=true') + self.assertIn("onclick=\"window.location = \'?results_layout=table&failures_only=false#test-results'\"", response.content.decode("utf-8")) self.assertEqual(200, response.status_code) def test_envbox_layout(self): response = self.client.get('/mygroup/myproject/build/1/?results_layout=envbox') + self.assertIn("onclick=\"window.location = \'?results_layout=envbox&failures_only=false#test-results'\"", response.content.decode("utf-8")) self.assertEqual(200, response.status_code) - def test_envbox_layout_failures_only(self): + def test_envbox_layout_failures_only_false(self): response = self.client.get('/mygroup/myproject/build/1/?results_layout=envbox&failures_only=false') + self.assertIn("onclick=\"window.location = \'?results_layout=envbox&failures_only=true#test-results'\"", response.content.decode("utf-8")) + self.assertEqual(200, response.status_code) + + def test_envbox_layout_failures_only_true(self): + response = self.client.get('/mygroup/myproject/build/1/?results_layout=envbox&failures_only=true') + self.assertIn("onclick=\"window.location = \'?results_layout=envbox&failures_only=false#test-results'\"", response.content.decode("utf-8")) + self.assertEqual(200, response.status_code) + + def test_failures_only_user_preference_false(self): + self.user = models.User.objects.create(username='theuser') + self.client = Client() + self.client.force_login(self.user) + self.user_preferences = get_user_preferences(user=self.user) + self.user_preferences.display_failures_only = False + self.user_preferences.save() + response = self.client.get('/mygroup/myproject/build/1/') + self.assertIn("onclick=\"window.location = \'?failures_only=true#test-results'\"", response.content.decode("utf-8")) + self.assertEqual(200, response.status_code) + + def test_failures_only_user_preference_true(self): + self.user = models.User.objects.create(username='theuser') + self.client = Client() + self.client.force_login(self.user) + self.user_preferences = get_user_preferences(user=self.user) + self.user_preferences.display_failures_only = True + self.user_preferences.save() + response = self.client.get('/mygroup/myproject/build/1/') + self.assertIn("onclick=\"window.location = \'?failures_only=false#test-results'\"", response.content.decode("utf-8")) self.assertEqual(200, response.status_code) def test_suitebox_layout(self):