From 5288579760481f035198c1db1eadb7fecb96ffa9 Mon Sep 17 00:00:00 2001 From: Rick Lawson Date: Fri, 13 Jan 2023 10:40:55 -0500 Subject: [PATCH 1/8] Allow users to 'favorite' queries Issue #119 Heart at top of query view allows user to toggle favorite or not Added Favorites list to top menu --- explorer/migrations/0011_query_favorites.py | 27 ++++++++++ explorer/models.py | 15 +++++- explorer/static/explorer/js/explorer.js | 18 +++++++ explorer/templates/explorer/base.html | 3 ++ explorer/templates/explorer/query.html | 6 +++ .../templates/explorer/query_favorites.html | 25 ++++++++++ explorer/tests/test_views.py | 41 +++++++++++++++- explorer/urls.py | 5 +- explorer/views/__init__.py | 5 +- explorer/views/query_favorite.py | 49 +++++++++++++++++++ explorer/views/utils.py | 9 +++- 11 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 explorer/migrations/0011_query_favorites.py create mode 100644 explorer/templates/explorer/query_favorites.html create mode 100644 explorer/views/query_favorite.py diff --git a/explorer/migrations/0011_query_favorites.py b/explorer/migrations/0011_query_favorites.py new file mode 100644 index 00000000..302f3710 --- /dev/null +++ b/explorer/migrations/0011_query_favorites.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.16 on 2023-01-12 18:24 + +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), + ('explorer', '0010_sql_required'), + ] + + operations = [ + migrations.CreateModel( + name='QueryFavorite', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('query', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='explorer.query')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('query', 'user')}, + }, + ), + ] diff --git a/explorer/models.py b/explorer/models.py index e93cf0c8..30806491 100644 --- a/explorer/models.py +++ b/explorer/models.py @@ -13,7 +13,6 @@ shared_dict_update, swap_params, ) - MSG_FAILED_BLACKLIST = "Query failed the SQL blacklist: %s" logger = logging.getLogger(__name__) @@ -188,6 +187,20 @@ class Meta: ordering = ['-run_at'] +class QueryFavorite(models.Model): + query = models.ForeignKey( + Query, + on_delete=models.CASCADE + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE + ) + + class Meta: + unique_together = ['query', 'user'] + + class QueryResult: def __init__(self, sql, connection): diff --git a/explorer/static/explorer/js/explorer.js b/explorer/static/explorer/js/explorer.js index 7e7e0407..4462dcfc 100644 --- a/explorer/static/explorer/js/explorer.js +++ b/explorer/static/explorer/js/explorer.js @@ -102,6 +102,19 @@ ExplorerEditor.prototype.formatSql = function() { }.bind(this)); }; +ExplorerEditor.prototype.toggleFavorite = function() { + $.post("../favorite/" + this.queryId , { }, function(data) { + let is_favorite = data.is_favorite; + if(is_favorite) { + $("#query_favorite_toggle").removeClass("glyphicon-heart-empty").addClass("glyphicon-heart"); + }else{ + $("#query_favorite_toggle").removeClass("glyphicon-heart").addClass("glyphicon-heart-empty"); + } + }.bind(this), 'json').fail(function() { + alert( "error" ); + }); +}; + ExplorerEditor.prototype.showRows = function() { var rows = this.$rows.val(), $form = $("#editor"); @@ -144,6 +157,11 @@ ExplorerEditor.prototype.bind = function() { this.formatSql(); }.bind(this)); + $("#query_favorite_toggle").click(function(e) { + e.preventDefault(); + this.toggleFavorite(); + }.bind(this)); + $("#rows").keyup(function() { var curUrl = $("#fullscreen").attr("href"); var newUrl = curUrl.replace(/rows=\d+/, "rows=" + $("#rows").val()); diff --git a/explorer/templates/explorer/base.html b/explorer/templates/explorer/base.html index 2751eca0..01121813 100644 --- a/explorer/templates/explorer/base.html +++ b/explorer/templates/explorer/base.html @@ -51,6 +51,9 @@ {% trans "Logs" %} + + {% trans "Favorite Queries" %} + {% endblock %} diff --git a/explorer/templates/explorer/query.html b/explorer/templates/explorer/query.html index 4936a479..8d462c7c 100644 --- a/explorer/templates/explorer/query.html +++ b/explorer/templates/explorer/query.html @@ -8,7 +8,13 @@

{% if query %} {{ query.title }} + {% if is_favorite %} + + {% else %} + + {% endif %} {% if shared %}  shared{% endif %} + {% else %} {% trans "New Query" %} {% endif %} diff --git a/explorer/templates/explorer/query_favorites.html b/explorer/templates/explorer/query_favorites.html new file mode 100644 index 00000000..099845ec --- /dev/null +++ b/explorer/templates/explorer/query_favorites.html @@ -0,0 +1,25 @@ +{% extends "explorer/base.html" %} +{% load i18n %} + +{% block sql_explorer_content %} +

{% trans "Favorite Queries" %}

+
+ + + + + + + + + {% for favorite in favorites %} + + + + {% endfor %} + +
{% trans "Query" %}
+ {{ favorite.query.title }} +
+
+{% endblock %} diff --git a/explorer/tests/test_views.py b/explorer/tests/test_views.py index 3f41bd08..9378fd02 100644 --- a/explorer/tests/test_views.py +++ b/explorer/tests/test_views.py @@ -15,7 +15,7 @@ from explorer import app_settings from explorer.app_settings import EXPLORER_DEFAULT_CONNECTION as CONN from explorer.app_settings import EXPLORER_TOKEN -from explorer.models import MSG_FAILED_BLACKLIST, Query, QueryLog +from explorer.models import MSG_FAILED_BLACKLIST, Query, QueryLog, QueryFavorite from explorer.tests.factories import QueryLogFactory, SimpleQueryFactory from explorer.utils import user_can_see_query @@ -825,3 +825,42 @@ def test_email_calls_task(self, mocked_execute): **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} ) self.assertEqual(mocked_execute.delay.call_count, 1) + + +class TestQueryFavorites(TestCase): + + def setUp(self): + self.user = User.objects.create_superuser( + 'admin', 'admin@admin.com', 'pwd' + ) + self.client.login(username='admin', password='pwd') + self.q = SimpleQueryFactory(title='query for x, y') + QueryFavorite.objects.create(user=self.user, query=self.q) + + def test_returns_favorite_list(self): + resp = self.client.get( + reverse("query_favorites") + ) + self.assertContains(resp, 'query for x, y') + + +class TestQueryFavorite(TestCase): + + def setUp(self): + self.user = User.objects.create_superuser( + 'admin', 'admin@admin.com', 'pwd' + ) + self.client.login(username='admin', password='pwd') + self.q = SimpleQueryFactory(title='query for x, y') + + def test_toggle(self): + resp = self.client.post( + reverse("query_favorite", args=(self.q.id,)) + ) + resp = json.loads(resp.content.decode('utf-8')) + self.assertTrue(resp['is_favorite']) + resp = self.client.post( + reverse("query_favorite", args=(self.q.id,)) + ) + resp = json.loads(resp.content.decode('utf-8')) + self.assertFalse(resp['is_favorite']) diff --git a/explorer/urls.py b/explorer/urls.py index ed75ad37..fcd1f54c 100644 --- a/explorer/urls.py +++ b/explorer/urls.py @@ -2,7 +2,8 @@ from explorer.views import ( CreateQueryView, DeleteQueryView, DownloadFromSqlView, DownloadQueryView, EmailCsvQueryView, ListQueryLogView, - ListQueryView, PlayQueryView, QueryView, SchemaView, StreamQueryView, format_sql, + ListQueryView, PlayQueryView, QueryView, SchemaView, StreamQueryView, format_sql, QueryFavoritesView, + QueryFavoriteView ) @@ -34,5 +35,7 @@ ), path('logs/', ListQueryLogView.as_view(), name='explorer_logs'), path('format/', format_sql, name='format_sql'), + path('favorites/', QueryFavoritesView.as_view(), name='query_favorites'), + path('favorite/', QueryFavoriteView.as_view(), name='query_favorite'), path('', ListQueryView.as_view(), name='explorer_index'), ] diff --git a/explorer/views/__init__.py b/explorer/views/__init__.py index c30ed0b2..01071dd2 100644 --- a/explorer/views/__init__.py +++ b/explorer/views/__init__.py @@ -6,6 +6,7 @@ from .format_sql import format_sql from .list import ListQueryLogView, ListQueryView from .query import PlayQueryView, QueryView +from .query_favorite import QueryFavoritesView, QueryFavoriteView from .schema import SchemaView from .stream import StreamQueryView @@ -24,5 +25,7 @@ 'SafeLoginView', 'StreamQueryView', 'SchemaView', - 'format_sql' + 'format_sql', + 'QueryFavoritesView', + 'QueryFavoriteView', ] diff --git a/explorer/views/query_favorite.py b/explorer/views/query_favorite.py new file mode 100644 index 00000000..a7307e42 --- /dev/null +++ b/explorer/views/query_favorite.py @@ -0,0 +1,49 @@ +from django.http import JsonResponse +from django.shortcuts import get_object_or_404 +from django.views import View + +from explorer.models import QueryFavorite, Query +from explorer.views.auth import PermissionRequiredMixin +from explorer.views.mixins import ExplorerContextMixin + + +class QueryFavoritesView(PermissionRequiredMixin, ExplorerContextMixin, View): + permission_required = 'view_permission' + + def get(self, request): + user = request.user + favorites = QueryFavorite.objects.filter(user=user).select_related('user').select_related('query').order_by( + 'query__title') + return self.render_template( + f'explorer/query_favorites.html', {'favorites': favorites} + ) + + +class QueryFavoriteView(PermissionRequiredMixin, ExplorerContextMixin, View): + permission_required = 'view_permission' + + @staticmethod + def build_favorite_response(user, query): + is_favorite = QueryFavorite.objects.filter(user=user, query=query).exists() + data = { + 'status': 'success', + 'query_id': query.id, + 'is_favorite': is_favorite + } + return data + + def get(self, request, query_id): + user = request.user + query = get_object_or_404(Query, pk=query_id) + return JsonResponse(QueryFavoriteView.build_favorite_response(user, query)) + + def post(self, request, query_id): + # toggle favorite + user = request.user + query = get_object_or_404(Query, pk=query_id) + if QueryFavorite.objects.filter(user=user, query=query).exists(): + QueryFavorite.objects.filter(user=user, query=query).delete() + else: + QueryFavorite.objects.get_or_create(user=user, query=query) + return JsonResponse(QueryFavoriteView.build_favorite_response(user, query)) + diff --git a/explorer/views/utils.py b/explorer/views/utils.py index 2caa8460..62ffe06c 100644 --- a/explorer/views/utils.py +++ b/explorer/views/utils.py @@ -2,6 +2,7 @@ from explorer import app_settings from explorer.charts import get_line_chart, get_pie_chart +from explorer.models import QueryFavorite def query_viewmodel(request, query, title=None, form=None, message=None, @@ -35,6 +36,11 @@ def query_viewmodel(request, query, title=None, form=None, message=None, 'querylog_id': ql.id }) + user = request.user + is_favorite = False + if user.is_authenticated: + is_favorite = QueryFavorite.objects.filter(user=user, query=query).exists() + ret = { 'tasks_enabled': app_settings.ENABLE_TASKS, 'params': query.available_params(), @@ -58,6 +64,7 @@ def query_viewmodel(request, query, title=None, form=None, message=None, 'fullscreen_params': fullscreen_params.urlencode(), 'charts_enabled': app_settings.EXPLORER_CHARTS_ENABLED, 'pie_chart_svg': get_pie_chart(res) if app_settings.EXPLORER_CHARTS_ENABLED and has_valid_results else None, - 'line_chart_svg': get_line_chart(res) if app_settings.EXPLORER_CHARTS_ENABLED and has_valid_results else None + 'line_chart_svg': get_line_chart(res) if app_settings.EXPLORER_CHARTS_ENABLED and has_valid_results else None, + 'is_favorite': is_favorite } return ret From e1fa2239e3ce139a0cf74c756d1f5ddce6131fe9 Mon Sep 17 00:00:00 2001 From: Rick Lawson Date: Fri, 13 Jan 2023 10:43:41 -0500 Subject: [PATCH 2/8] Allow users to 'favorite' queries Issue #119 fix isort and flake8 issues --- explorer/migrations/0011_query_favorites.py | 2 +- explorer/models.py | 1 + explorer/tests/test_views.py | 2 +- explorer/urls.py | 4 ++-- explorer/views/query_favorite.py | 5 ++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/explorer/migrations/0011_query_favorites.py b/explorer/migrations/0011_query_favorites.py index 302f3710..64ded59a 100644 --- a/explorer/migrations/0011_query_favorites.py +++ b/explorer/migrations/0011_query_favorites.py @@ -1,8 +1,8 @@ # Generated by Django 3.2.16 on 2023-01-12 18:24 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/explorer/models.py b/explorer/models.py index 30806491..cc9a4cce 100644 --- a/explorer/models.py +++ b/explorer/models.py @@ -13,6 +13,7 @@ shared_dict_update, swap_params, ) + MSG_FAILED_BLACKLIST = "Query failed the SQL blacklist: %s" logger = logging.getLogger(__name__) diff --git a/explorer/tests/test_views.py b/explorer/tests/test_views.py index 9378fd02..8f8a6c8f 100644 --- a/explorer/tests/test_views.py +++ b/explorer/tests/test_views.py @@ -15,7 +15,7 @@ from explorer import app_settings from explorer.app_settings import EXPLORER_DEFAULT_CONNECTION as CONN from explorer.app_settings import EXPLORER_TOKEN -from explorer.models import MSG_FAILED_BLACKLIST, Query, QueryLog, QueryFavorite +from explorer.models import MSG_FAILED_BLACKLIST, Query, QueryFavorite, QueryLog from explorer.tests.factories import QueryLogFactory, SimpleQueryFactory from explorer.utils import user_can_see_query diff --git a/explorer/urls.py b/explorer/urls.py index fcd1f54c..ed057f62 100644 --- a/explorer/urls.py +++ b/explorer/urls.py @@ -2,8 +2,8 @@ from explorer.views import ( CreateQueryView, DeleteQueryView, DownloadFromSqlView, DownloadQueryView, EmailCsvQueryView, ListQueryLogView, - ListQueryView, PlayQueryView, QueryView, SchemaView, StreamQueryView, format_sql, QueryFavoritesView, - QueryFavoriteView + ListQueryView, PlayQueryView, QueryFavoritesView, QueryFavoriteView, QueryView, SchemaView, StreamQueryView, + format_sql, ) diff --git a/explorer/views/query_favorite.py b/explorer/views/query_favorite.py index a7307e42..2244c778 100644 --- a/explorer/views/query_favorite.py +++ b/explorer/views/query_favorite.py @@ -2,7 +2,7 @@ from django.shortcuts import get_object_or_404 from django.views import View -from explorer.models import QueryFavorite, Query +from explorer.models import Query, QueryFavorite from explorer.views.auth import PermissionRequiredMixin from explorer.views.mixins import ExplorerContextMixin @@ -15,7 +15,7 @@ def get(self, request): favorites = QueryFavorite.objects.filter(user=user).select_related('user').select_related('query').order_by( 'query__title') return self.render_template( - f'explorer/query_favorites.html', {'favorites': favorites} + 'explorer/query_favorites.html', {'favorites': favorites} ) @@ -46,4 +46,3 @@ def post(self, request, query_id): else: QueryFavorite.objects.get_or_create(user=user, query=query) return JsonResponse(QueryFavoriteView.build_favorite_response(user, query)) - From 2a5377a6d233ee3cebf8cd1e19067893ad13fc69 Mon Sep 17 00:00:00 2001 From: Richard Lawson Date: Sat, 14 Jan 2023 08:24:41 -0500 Subject: [PATCH 3/8] Update explorer/views/query_favorite.py Co-authored-by: Mark Walker --- explorer/views/query_favorite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/explorer/views/query_favorite.py b/explorer/views/query_favorite.py index 2244c778..a9687b44 100644 --- a/explorer/views/query_favorite.py +++ b/explorer/views/query_favorite.py @@ -12,7 +12,7 @@ class QueryFavoritesView(PermissionRequiredMixin, ExplorerContextMixin, View): def get(self, request): user = request.user - favorites = QueryFavorite.objects.filter(user=user).select_related('user').select_related('query').order_by( + favorites = QueryFavorite.objects.filter(user=user).select_related('query', 'user').order_by( 'query__title') return self.render_template( 'explorer/query_favorites.html', {'favorites': favorites} From edf4a922f0606d027c1d25045b5f15448beaa652 Mon Sep 17 00:00:00 2001 From: Rick Lawson Date: Sat, 14 Jan 2023 08:31:03 -0500 Subject: [PATCH 4/8] Allow users to 'favorite' queries Issue #119 Incorporate review feedback --- explorer/views/query_favorite.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/explorer/views/query_favorite.py b/explorer/views/query_favorite.py index a9687b44..7dd142d0 100644 --- a/explorer/views/query_favorite.py +++ b/explorer/views/query_favorite.py @@ -1,8 +1,7 @@ from django.http import JsonResponse -from django.shortcuts import get_object_or_404 from django.views import View -from explorer.models import Query, QueryFavorite +from explorer.models import QueryFavorite from explorer.views.auth import PermissionRequiredMixin from explorer.views.mixins import ExplorerContextMixin @@ -11,8 +10,7 @@ class QueryFavoritesView(PermissionRequiredMixin, ExplorerContextMixin, View): permission_required = 'view_permission' def get(self, request): - user = request.user - favorites = QueryFavorite.objects.filter(user=user).select_related('query', 'user').order_by( + favorites = QueryFavorite.objects.filter(user=request.user).select_related('query', 'user').order_by( 'query__title') return self.render_template( 'explorer/query_favorites.html', {'favorites': favorites} @@ -23,26 +21,22 @@ class QueryFavoriteView(PermissionRequiredMixin, ExplorerContextMixin, View): permission_required = 'view_permission' @staticmethod - def build_favorite_response(user, query): - is_favorite = QueryFavorite.objects.filter(user=user, query=query).exists() + def build_favorite_response(user, query_id): + is_favorite = QueryFavorite.objects.filter(user=user, query_id=query_id).exists() data = { 'status': 'success', - 'query_id': query.id, + 'query_id': query_id, 'is_favorite': is_favorite } return data def get(self, request, query_id): - user = request.user - query = get_object_or_404(Query, pk=query_id) - return JsonResponse(QueryFavoriteView.build_favorite_response(user, query)) + return JsonResponse(QueryFavoriteView.build_favorite_response(request.user, query_id)) def post(self, request, query_id): # toggle favorite - user = request.user - query = get_object_or_404(Query, pk=query_id) - if QueryFavorite.objects.filter(user=user, query=query).exists(): - QueryFavorite.objects.filter(user=user, query=query).delete() + if QueryFavorite.objects.filter(user=request.user, query_id=query_id).exists(): + QueryFavorite.objects.filter(user=request.user, query_id=query_id).delete() else: - QueryFavorite.objects.get_or_create(user=user, query=query) - return JsonResponse(QueryFavoriteView.build_favorite_response(user, query)) + QueryFavorite.objects.get_or_create(user=request.user, query_id=query_id) + return JsonResponse(QueryFavoriteView.build_favorite_response(request.user, query_id)) From 88c28855e135225af9d13a079ee27c32c94979e0 Mon Sep 17 00:00:00 2001 From: Richard Lawson Date: Sat, 14 Jan 2023 21:45:46 -0500 Subject: [PATCH 5/8] Update explorer/templates/explorer/query_favorites.html Co-authored-by: Mark Walker --- explorer/templates/explorer/query_favorites.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/explorer/templates/explorer/query_favorites.html b/explorer/templates/explorer/query_favorites.html index 099845ec..7f96e40f 100644 --- a/explorer/templates/explorer/query_favorites.html +++ b/explorer/templates/explorer/query_favorites.html @@ -18,6 +18,13 @@

{% trans "Favorite Queries" %}

{{ favorite.query.title }} + + {% empty %} + + + {% trans "No favorite queries added yet." %} + + {% endfor %} From 61ebf5a0fbfce585d9cc64253eb56e871d2b857f Mon Sep 17 00:00:00 2001 From: Rick Lawson Date: Sat, 14 Jan 2023 21:49:47 -0500 Subject: [PATCH 6/8] Allow users to 'favorite' queries Issue #119 Make favorite icon red to standout --- explorer/templates/explorer/query.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/explorer/templates/explorer/query.html b/explorer/templates/explorer/query.html index 8d462c7c..f68aa702 100644 --- a/explorer/templates/explorer/query.html +++ b/explorer/templates/explorer/query.html @@ -9,9 +9,9 @@

{% if query %} {{ query.title }} {% if is_favorite %} - + {% else %} - + {% endif %} {% if shared %}  shared{% endif %} From a0adcb9a7b458da93e097593922705eae93a5d4e Mon Sep 17 00:00:00 2001 From: Rick Lawson Date: Wed, 18 Jan 2023 15:19:00 -0500 Subject: [PATCH 7/8] Allow users to 'favorite' queries Issue #119 Make favorite icon a tag Reuse on front page of query list --- explorer/models.py | 12 ++++++++++-- explorer/static/explorer/js/explorer.js | 18 ------------------ explorer/templates/explorer/base.html | 18 ++++++++++++++++++ explorer/templates/explorer/query.html | 6 +----- .../explorer/query_favorite_button.html | 8 ++++++++ explorer/templates/explorer/query_list.html | 3 +++ explorer/templatetags/explorer_tags.py | 8 ++++++++ explorer/views/list.py | 9 +++++---- explorer/views/query.py | 2 +- 9 files changed, 54 insertions(+), 30 deletions(-) create mode 100644 explorer/templates/explorer/query_favorite_button.html diff --git a/explorer/models.py b/explorer/models.py index cc9a4cce..eafa7846 100644 --- a/explorer/models.py +++ b/explorer/models.py @@ -154,6 +154,12 @@ def snapshots(self): ) for o in objects_s ] + def is_favorite(self, user): + if user.is_authenticated: + return self.favorites.filter(user_id=user.id).exists() + else: + return False + class SnapShot: @@ -191,11 +197,13 @@ class Meta: class QueryFavorite(models.Model): query = models.ForeignKey( Query, - on_delete=models.CASCADE + on_delete=models.CASCADE, + related_name='favorites' ) user = models.ForeignKey( settings.AUTH_USER_MODEL, - on_delete=models.CASCADE + on_delete=models.CASCADE, + related_name='favorites' ) class Meta: diff --git a/explorer/static/explorer/js/explorer.js b/explorer/static/explorer/js/explorer.js index 4462dcfc..7e7e0407 100644 --- a/explorer/static/explorer/js/explorer.js +++ b/explorer/static/explorer/js/explorer.js @@ -102,19 +102,6 @@ ExplorerEditor.prototype.formatSql = function() { }.bind(this)); }; -ExplorerEditor.prototype.toggleFavorite = function() { - $.post("../favorite/" + this.queryId , { }, function(data) { - let is_favorite = data.is_favorite; - if(is_favorite) { - $("#query_favorite_toggle").removeClass("glyphicon-heart-empty").addClass("glyphicon-heart"); - }else{ - $("#query_favorite_toggle").removeClass("glyphicon-heart").addClass("glyphicon-heart-empty"); - } - }.bind(this), 'json').fail(function() { - alert( "error" ); - }); -}; - ExplorerEditor.prototype.showRows = function() { var rows = this.$rows.val(), $form = $("#editor"); @@ -157,11 +144,6 @@ ExplorerEditor.prototype.bind = function() { this.formatSql(); }.bind(this)); - $("#query_favorite_toggle").click(function(e) { - e.preventDefault(); - this.toggleFavorite(); - }.bind(this)); - $("#rows").keyup(function() { var curUrl = $("#fullscreen").attr("href"); var newUrl = curUrl.replace(/rows=\d+/, "rows=" + $("#rows").val()); diff --git a/explorer/templates/explorer/base.html b/explorer/templates/explorer/base.html index 01121813..e9ab6e5d 100644 --- a/explorer/templates/explorer/base.html +++ b/explorer/templates/explorer/base.html @@ -89,6 +89,24 @@ {% block sql_explorer_scripts %}{% endblock %} + diff --git a/explorer/templates/explorer/query.html b/explorer/templates/explorer/query.html index f68aa702..5467e64d 100644 --- a/explorer/templates/explorer/query.html +++ b/explorer/templates/explorer/query.html @@ -8,11 +8,7 @@

{% if query %} {{ query.title }} - {% if is_favorite %} - - {% else %} - - {% endif %} + {% query_favorite_button query.id is_favorite %} {% if shared %}  shared{% endif %} {% else %} diff --git a/explorer/templates/explorer/query_favorite_button.html b/explorer/templates/explorer/query_favorite_button.html new file mode 100644 index 00000000..d4748e85 --- /dev/null +++ b/explorer/templates/explorer/query_favorite_button.html @@ -0,0 +1,8 @@ +{% if is_favorite %} + +{% else %} + +{% endif %} + diff --git a/explorer/templates/explorer/query_list.html b/explorer/templates/explorer/query_list.html index c0092f89..8b01d2f0 100644 --- a/explorer/templates/explorer/query_list.html +++ b/explorer/templates/explorer/query_list.html @@ -1,5 +1,6 @@ {% extends "explorer/base.html" %} {% load i18n static %} +{% load explorer_tags i18n %} {% block sql_explorer_content %} {% if recent_queries|length > 0 %} @@ -56,6 +57,7 @@

{% trans "All Queries" %}

{% trans "Play" %} {% trans "Delete" %} {% endif %} + {% trans "Favorite" %} {% trans "Run Count" %} @@ -105,6 +107,7 @@

{% trans "All Queries" %}

{% endif %} + {% query_favorite_button object.id object.is_favorite %} {{ object.run_count }} {% endif %} diff --git a/explorer/templatetags/explorer_tags.py b/explorer/templatetags/explorer_tags.py index a42f4713..84d969cc 100644 --- a/explorer/templatetags/explorer_tags.py +++ b/explorer/templatetags/explorer_tags.py @@ -17,3 +17,11 @@ def export_buttons(query=None): 'exporters': exporters, 'query': query, } + + +@register.inclusion_tag('explorer/query_favorite_button.html', takes_context=True) +def query_favorite_button(context, query_id, is_favorite): + return { + 'query_id': query_id, + 'is_favorite': is_favorite + } diff --git a/explorer/views/list.py b/explorer/views/list.py index 7e32d71b..3df7e222 100644 --- a/explorer/views/list.py +++ b/explorer/views/list.py @@ -5,14 +5,13 @@ from django.views.generic import ListView from explorer import app_settings -from explorer.models import Query, QueryLog +from explorer.models import Query, QueryFavorite, QueryLog from explorer.utils import allowed_query_pks, url_get_query_id from explorer.views.auth import PermissionRequiredMixin from explorer.views.mixins import ExplorerContextMixin class ListQueryView(PermissionRequiredMixin, ExplorerContextMixin, ListView): - permission_required = 'view_permission_list' model = Query @@ -88,6 +87,8 @@ def _build_queries_and_headers(self): pattern = re.compile(r'[\W_]+') headers = Counter([q.title.split(' - ')[0] for q in self.object_list]) + query_favorites_for_user = QueryFavorite.objects.filter(user_id=self.request.user.id).values_list('query_id', + flat=True) for q in self.object_list: model_dict = model_to_dict(q) @@ -111,14 +112,14 @@ def _build_queries_and_headers(self): 'is_header': False, 'run_count': q.querylog_set.count(), 'created_by_user': - str(q.created_by_user) if q.created_by_user else None + str(q.created_by_user) if q.created_by_user else None, + 'is_favorite': q.id in query_favorites_for_user }) dict_list.append(model_dict) return dict_list class ListQueryLogView(PermissionRequiredMixin, ExplorerContextMixin, ListView): - context_object_name = "recent_logs" model = QueryLog paginate_by = 20 diff --git a/explorer/views/query.py b/explorer/views/query.py index 76078745..23c27156 100644 --- a/explorer/views/query.py +++ b/explorer/views/query.py @@ -138,7 +138,7 @@ def post(self, request, query_id): @staticmethod def get_instance_and_form(request, query_id): - query = get_object_or_404(Query, pk=query_id) + query = get_object_or_404(Query.objects.prefetch_related('favorites'), pk=query_id) query.params = url_get_params(request) form = QueryForm( request.POST if len(request.POST) else None, From 23968491937e6c0dd9e5c2734fb76aa5e5f85899 Mon Sep 17 00:00:00 2001 From: Rick Lawson Date: Fri, 20 Jan 2023 05:32:27 -0500 Subject: [PATCH 8/8] Allow users to 'favorite' queries Issue #119 Changes from code review --- explorer/static/explorer/css/explorer.css | 10 ++++++++- explorer/static/explorer/js/favorites.js | 16 ++++++++++++++ explorer/templates/explorer/base.html | 21 ++----------------- explorer/templates/explorer/query.html | 2 +- .../explorer/query_favorite_button.html | 6 ++---- .../templates/explorer/query_favorites.html | 2 +- explorer/templates/explorer/query_list.html | 5 ++--- explorer/templatetags/explorer_tags.py | 7 ++++--- 8 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 explorer/static/explorer/js/favorites.js diff --git a/explorer/static/explorer/css/explorer.css b/explorer/static/explorer/css/explorer.css index b541ef1d..f697ff7b 100644 --- a/explorer/static/explorer/css/explorer.css +++ b/explorer/static/explorer/css/explorer.css @@ -173,4 +173,12 @@ a#fullscreen { #sql_toggle { padding-left: 1rem; color: #18bc9c; -} \ No newline at end of file +} + +.query_favorite_toggle { + cursor: pointer; +} + +.query_favourite_detail { + float: right; +} diff --git a/explorer/static/explorer/js/favorites.js b/explorer/static/explorer/js/favorites.js new file mode 100644 index 00000000..5ca6fb24 --- /dev/null +++ b/explorer/static/explorer/js/favorites.js @@ -0,0 +1,16 @@ +let toggle_favorite = function () { + let queryId = $(this).data('id'); + let favoriteUrl = $(this).data('url'); + $.post(favoriteUrl, {}, function (data) { + let is_favorite = data.is_favorite; + let selector = '.query_favorite_toggle[data-id=' + queryId + ']'; + if (is_favorite) { + $(selector).removeClass("glyphicon-heart-empty").addClass("glyphicon-heart"); + } else { + $(selector).removeClass("glyphicon-heart").addClass("glyphicon-heart-empty"); + } + }.bind(this), 'json').fail(function () { + alert("error"); + }); +} +$('.query_favorite_toggle').click(toggle_favorite); diff --git a/explorer/templates/explorer/base.html b/explorer/templates/explorer/base.html index e9ab6e5d..a36ed058 100644 --- a/explorer/templates/explorer/base.html +++ b/explorer/templates/explorer/base.html @@ -87,26 +87,9 @@ - + + {% block sql_explorer_scripts %}{% endblock %} - diff --git a/explorer/templates/explorer/query.html b/explorer/templates/explorer/query.html index 5467e64d..bd5912f5 100644 --- a/explorer/templates/explorer/query.html +++ b/explorer/templates/explorer/query.html @@ -8,7 +8,7 @@

{% if query %} {{ query.title }} - {% query_favorite_button query.id is_favorite %} + {% query_favorite_button query.id is_favorite 'query_favorite_toggle query_favourite_detail'%} {% if shared %}  shared{% endif %} {% else %} diff --git a/explorer/templates/explorer/query_favorite_button.html b/explorer/templates/explorer/query_favorite_button.html index d4748e85..e90677f3 100644 --- a/explorer/templates/explorer/query_favorite_button.html +++ b/explorer/templates/explorer/query_favorite_button.html @@ -1,8 +1,6 @@ {% if is_favorite %} - + {% else %} - + {% endif %} diff --git a/explorer/templates/explorer/query_favorites.html b/explorer/templates/explorer/query_favorites.html index 7f96e40f..274d9394 100644 --- a/explorer/templates/explorer/query_favorites.html +++ b/explorer/templates/explorer/query_favorites.html @@ -19,7 +19,7 @@

{% trans "Favorite Queries" %}

- {% empty %} + {% empty %} {% trans "No favorite queries added yet." %} diff --git a/explorer/templates/explorer/query_list.html b/explorer/templates/explorer/query_list.html index 8b01d2f0..6368a939 100644 --- a/explorer/templates/explorer/query_list.html +++ b/explorer/templates/explorer/query_list.html @@ -1,6 +1,5 @@ {% extends "explorer/base.html" %} -{% load i18n static %} -{% load explorer_tags i18n %} +{% load explorer_tags i18n static %} {% block sql_explorer_content %} {% if recent_queries|length > 0 %} @@ -107,7 +106,7 @@

{% trans "All Queries" %}

{% endif %} - {% query_favorite_button object.id object.is_favorite %} + {% query_favorite_button object.id object.is_favorite 'query_favorite_toggle' %} {{ object.run_count }} {% endif %} diff --git a/explorer/templatetags/explorer_tags.py b/explorer/templatetags/explorer_tags.py index 84d969cc..b9f0d86f 100644 --- a/explorer/templatetags/explorer_tags.py +++ b/explorer/templatetags/explorer_tags.py @@ -19,9 +19,10 @@ def export_buttons(query=None): } -@register.inclusion_tag('explorer/query_favorite_button.html', takes_context=True) -def query_favorite_button(context, query_id, is_favorite): +@register.inclusion_tag('explorer/query_favorite_button.html') +def query_favorite_button(query_id, is_favorite, extra_classes): return { 'query_id': query_id, - 'is_favorite': is_favorite + 'is_favorite': is_favorite, + 'extra_classes': extra_classes }