Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Favorite queries #520

Merged
merged 11 commits into from
Jan 21, 2023
27 changes: 27 additions & 0 deletions explorer/migrations/0011_query_favorites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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


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')},
},
),
]
22 changes: 22 additions & 0 deletions explorer/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -188,6 +194,22 @@ class Meta:
ordering = ['-run_at']


class QueryFavorite(models.Model):
query = models.ForeignKey(
Query,
on_delete=models.CASCADE,
related_name='favorites'
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='favorites'
)

class Meta:
unique_together = ['query', 'user']


class QueryResult:

def __init__(self, sql, connection):
Expand Down
21 changes: 21 additions & 0 deletions explorer/templates/explorer/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
<li{% if not query and view_name == 'explorer_logs' %} class="active" {% endif %}>
<a href="{% url 'explorer_logs' %}">{% trans "Logs" %}</a>
</li>
<li{% if not query and view_name == 'query_favorites' %} class="active" {% endif %}>
<a href="{% url 'query_favorites' %}">{% trans "Favorite Queries" %}</a>
</li>
{% endblock %}
</ul>
</div>
Expand Down Expand Up @@ -86,6 +89,24 @@
<script src="{% static 'explorer/js/pivot.min.js' %}"></script>
<script src="{% static 'explorer/js/xlsx.mini.min.js' %}"></script>
{% block sql_explorer_scripts %}{% endblock %}
<script>
lawson89 marked this conversation as resolved.
Show resolved Hide resolved
let toggle_favorite = function () {
let queryId = $(this).data('id');
let favoriteUrl = "{% url 'query_favorite' 0 %}";
$.post(favoriteUrl.replace('0', queryId), {}, 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);
</script>
</body>

</html>
2 changes: 2 additions & 0 deletions explorer/templates/explorer/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
<h2>
{% if query %}
{{ query.title }}
{% query_favorite_button query.id is_favorite %}
{% if shared %}<small>&nbsp;&nbsp;shared</small>{% endif %}

{% else %}
{% trans "New Query" %}
{% endif %}
Expand Down
8 changes: 8 additions & 0 deletions explorer/templates/explorer/query_favorite_button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% if is_favorite %}
<i class="glyphicon glyphicon-heart text-danger query_favorite_toggle" data-id="{{ query_id }}"
lawson89 marked this conversation as resolved.
Show resolved Hide resolved
style="cursor: pointer;"></i>
lawson89 marked this conversation as resolved.
Show resolved Hide resolved
{% else %}
<i class="glyphicon glyphicon-heart-empty text-danger query_favorite_toggle" data-id="{{ query_id }}"
lawson89 marked this conversation as resolved.
Show resolved Hide resolved
style="cursor: pointer;"></i>
{% endif %}

32 changes: 32 additions & 0 deletions explorer/templates/explorer/query_favorites.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% extends "explorer/base.html" %}
{% load i18n %}

{% block sql_explorer_content %}
<h2>{% trans "Favorite Queries" %}</h2>
<div class="table-responsive">
<table class="table table-striped query-list">
<thead>
<tr>
<th>{% trans "Query" %}</th>
</tr>
</thead>

<tbody>
{% for favorite in favorites %}
<tr>
<th>
<a href="{% url 'query_detail' favorite.query.id %}">{{ favorite.query.title }}</a>
</th>
</tr>

{% empty %}
<tr>
<th>
{% trans "No favorite queries added yet." %}
</th>
</tr>
{% endfor %}
lawson89 marked this conversation as resolved.
Show resolved Hide resolved
</tbody>
</table>
</div>
{% endblock %}
3 changes: 3 additions & 0 deletions explorer/templates/explorer/query_list.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{% extends "explorer/base.html" %}
{% load i18n static %}
lawson89 marked this conversation as resolved.
Show resolved Hide resolved
{% load explorer_tags i18n %}
lawson89 marked this conversation as resolved.
Show resolved Hide resolved

{% block sql_explorer_content %}
{% if recent_queries|length > 0 %}
Expand Down Expand Up @@ -56,6 +57,7 @@ <h3>{% trans "All Queries" %}</h3>
<th>{% trans "Play" %}</th>
<th>{% trans "Delete" %}</th>
{% endif %}
<th>{% trans "Favorite" %}</th>
<th>{% trans "Run Count" %}</th>
</tr>
</thead>
Expand Down Expand Up @@ -105,6 +107,7 @@ <h3>{% trans "All Queries" %}</h3>
</a>
</td>
{% endif %}
<td> {% query_favorite_button object.id object.is_favorite %}</td>
<td>{{ object.run_count }}</td>
{% endif %}
</tr>
Expand Down
8 changes: 8 additions & 0 deletions explorer/templatetags/explorer_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ def export_buttons(query=None):
'exporters': exporters,
'query': query,
}


@register.inclusion_tag('explorer/query_favorite_button.html', takes_context=True)
lawson89 marked this conversation as resolved.
Show resolved Hide resolved
def query_favorite_button(context, query_id, is_favorite):
lawson89 marked this conversation as resolved.
Show resolved Hide resolved
return {
'query_id': query_id,
'is_favorite': is_favorite
lawson89 marked this conversation as resolved.
Show resolved Hide resolved
}
41 changes: 40 additions & 1 deletion explorer/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, QueryFavorite, QueryLog
from explorer.tests.factories import QueryLogFactory, SimpleQueryFactory
from explorer.utils import user_can_see_query

Expand Down Expand Up @@ -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'])
5 changes: 4 additions & 1 deletion explorer/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from explorer.views import (
CreateQueryView, DeleteQueryView, DownloadFromSqlView, DownloadQueryView, EmailCsvQueryView, ListQueryLogView,
ListQueryView, PlayQueryView, QueryView, SchemaView, StreamQueryView, format_sql,
ListQueryView, PlayQueryView, QueryFavoritesView, QueryFavoriteView, QueryView, SchemaView, StreamQueryView,
format_sql,
)


Expand Down Expand Up @@ -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/<int:query_id>', QueryFavoriteView.as_view(), name='query_favorite'),
path('', ListQueryView.as_view(), name='explorer_index'),
]
5 changes: 4 additions & 1 deletion explorer/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -24,5 +25,7 @@
'SafeLoginView',
'StreamQueryView',
'SchemaView',
'format_sql'
'format_sql',
'QueryFavoritesView',
'QueryFavoriteView',
]
9 changes: 5 additions & 4 deletions explorer/views/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion explorer/views/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
lawson89 marked this conversation as resolved.
Show resolved Hide resolved
query.params = url_get_params(request)
form = QueryForm(
request.POST if len(request.POST) else None,
Expand Down
42 changes: 42 additions & 0 deletions explorer/views/query_favorite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from django.http import JsonResponse
from django.views import View

from explorer.models import QueryFavorite
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):
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}
)


class QueryFavoriteView(PermissionRequiredMixin, ExplorerContextMixin, View):
permission_required = 'view_permission'

@staticmethod
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,
'is_favorite': is_favorite
}
return data

def get(self, request, query_id):
return JsonResponse(QueryFavoriteView.build_favorite_response(request.user, query_id))

def post(self, request, query_id):
# toggle favorite
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=request.user, query_id=query_id)
return JsonResponse(QueryFavoriteView.build_favorite_response(request.user, query_id))
9 changes: 8 additions & 1 deletion explorer/views/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand All @@ -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