mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
feat(home): search-driven link library with mobile card layout
- /search/aliases/ now matches alias/url/description/text/tags and returns id, link_type, detail_url, edit_url, click_count for management actions - Library filter upgraded from client-side row filtering to live search cards: click card -> detail page, inline edit/delete, alias jumps - Mobile: table rows render as cards (kills 620px horizontal scroll), hidden cells stay hidden, no viewport overflow - Hero search no longer autofocuses on touch devices (no keyboard pop); desktop keeps autofocus via hover:fine media query - Nav bar unified to light frosted-glass (was dark green) across all pages - Fix hero-glow decorative overflow on mobile - Add tests/test_search_aliases.py (8 cases)
This commit is contained in:
+31
-2
@@ -8,6 +8,7 @@ from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from .models import Link, Page, Post, Bookmark
|
||||
from .search_backend import search_backend
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.core.cache import cache
|
||||
@@ -243,12 +244,40 @@ def search_api_v2(request):
|
||||
|
||||
def search_aliases(request):
|
||||
query = request.GET.get('q', '').strip()
|
||||
links = Link.objects.filter(alias__icontains=query).annotate(priority=Case(When(alias__iexact=query, then=Value(1)), When(alias__istartswith=query, then=Value(2)), default=Value(3), output_field=IntegerField())).order_by('priority', 'alias')[:10]
|
||||
qs = Link.objects.all()
|
||||
if query:
|
||||
qs = qs.filter(
|
||||
Q(alias__icontains=query)
|
||||
| Q(original_url__icontains=query)
|
||||
| Q(description__icontains=query)
|
||||
| Q(text__icontains=query)
|
||||
| Q(tags__name__icontains=query)
|
||||
).distinct()
|
||||
qs = qs.annotate(priority=Case(
|
||||
When(alias__iexact=query, then=Value(1)),
|
||||
When(alias__istartswith=query, then=Value(2)),
|
||||
default=Value(3), output_field=IntegerField(),
|
||||
)).order_by('priority', 'alias')
|
||||
else:
|
||||
# No query: return the most recently updated links (used by the
|
||||
# home-page library as the "recent links" default view).
|
||||
qs = qs.order_by('-updated_at')
|
||||
links = qs[:20]
|
||||
results = []
|
||||
for link in links:
|
||||
try:
|
||||
url = request.build_absolute_uri(f'/{link.alias}/')
|
||||
results.append({'alias': link.alias, 'url': url})
|
||||
results.append({
|
||||
'alias': link.alias,
|
||||
'url': url,
|
||||
'id': link.id,
|
||||
'link_type': link.link_type,
|
||||
'detail_url': reverse('link_detail', args=[link.id]),
|
||||
'edit_url': reverse('link_update', args=[link.id]),
|
||||
'click_count': link.click_count,
|
||||
'description': (link.description or '')[:160],
|
||||
'original_url': (link.original_url or '')[:180],
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating URL for alias {link.alias}: {str(e)}")
|
||||
continue
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
}
|
||||
.hero-glow {
|
||||
position: absolute;
|
||||
inset: -12% -6% auto;
|
||||
inset: -12% 0 auto;
|
||||
height: 340px;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
@@ -286,6 +286,70 @@
|
||||
.apple-btn-secondary:hover { background: #ececef; }
|
||||
.apple-btn-secondary[disabled] { opacity: 0.55; cursor: default; pointer-events: none; }
|
||||
|
||||
/* ---------- Search-driven result cards (replace the table while searching) ---------- */
|
||||
.search-card-list {
|
||||
display: flex; flex-direction: column; gap: 0.7rem;
|
||||
padding: 0.2rem 1.8rem 1.4rem;
|
||||
}
|
||||
.search-card {
|
||||
display: flex; align-items: center; gap: 1rem;
|
||||
background: #fff; border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-radius: 20px; box-shadow: var(--apple-shadow-card);
|
||||
padding: 0.85rem 1.15rem;
|
||||
transition: box-shadow 0.3s var(--apple-ease), transform 0.3s var(--apple-ease);
|
||||
}
|
||||
.search-card:hover { box-shadow: var(--apple-shadow-lift); transform: translateY(-1px); }
|
||||
.search-card-main { flex: 1; min-width: 0; cursor: pointer; border-radius: 12px; }
|
||||
.search-card-main:focus-visible { outline: 2px solid var(--apple-blue); outline-offset: 3px; }
|
||||
.search-card-title { display: flex; align-items: center; gap: 0.55rem; min-width: 0; }
|
||||
.search-card-title a.alias-link {
|
||||
font-size: 1.08rem; font-weight: 600;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.search-card-title a.alias-link:hover { text-decoration: underline; }
|
||||
.search-card-url {
|
||||
font-size: 0.82rem; color: var(--apple-gray); margin-top: 0.15rem;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.search-card-desc {
|
||||
font-size: 0.85rem; color: var(--apple-gray); margin-top: 0.2rem;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.search-card mark {
|
||||
background: rgba(0, 113, 227, 0.13); color: var(--apple-blue);
|
||||
border-radius: 4px; padding: 0 1px;
|
||||
}
|
||||
.search-card-count {
|
||||
font-size: 0.72rem; color: #c7c7cc; font-variant-numeric: tabular-nums;
|
||||
flex: none; white-space: nowrap;
|
||||
}
|
||||
.search-card-actions { display: flex; align-items: center; gap: 0.2rem; flex: none; }
|
||||
.search-empty-card { padding: 2.75rem 1.5rem; text-align: center; color: var(--apple-gray); }
|
||||
|
||||
/* ---------- Mobile: table rows become cards (kills the 620px horizontal scroll) ---------- */
|
||||
@media (max-width: 640px) {
|
||||
.table-scroll { overflow: visible; }
|
||||
.apple-table { min-width: 0; }
|
||||
.apple-table thead { display: none; }
|
||||
.apple-table tbody { display: flex; flex-direction: column; gap: 0.7rem; padding: 0 0.85rem 0.85rem; }
|
||||
.apple-table tbody tr {
|
||||
display: flex; align-items: center; gap: 0.8rem;
|
||||
background: #fff; border-radius: 18px;
|
||||
box-shadow: var(--apple-shadow-card);
|
||||
padding: 0.8rem 1rem;
|
||||
}
|
||||
.apple-table tbody td { display: block; padding: 0; border-bottom: none !important; }
|
||||
/* Tailwind's `hidden` helper must win over the block layout above, or the
|
||||
type/url/clicks cells reappear on mobile and blow the card wider than
|
||||
the viewport (verified: 564px row on a 390px screen). */
|
||||
.apple-table tbody td.hidden { display: none !important; }
|
||||
.apple-table tbody td:first-child { flex: none; }
|
||||
.apple-table tbody td:nth-child(2) { flex: 1; min-width: 0; }
|
||||
.apple-table tbody td:last-child { flex: none; }
|
||||
/* Empty / no-results rows (colspan) span the whole card width */
|
||||
.apple-table tbody tr td[colspan] { flex: 1; text-align: center; padding: 2.25rem 1rem; }
|
||||
}
|
||||
|
||||
/* ---------- First-screen dashboard (posts + images side by side) ---------- */
|
||||
.dash-grid {
|
||||
display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.25rem;
|
||||
@@ -585,7 +649,6 @@
|
||||
id="main-search-input"
|
||||
placeholder="{% trans 'Search aliases...' %}"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
role="combobox"
|
||||
aria-expanded="false"
|
||||
aria-controls="main-search-results"
|
||||
@@ -796,7 +859,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'delete_selected' %}">
|
||||
<div id="search-card-results" class="search-card-list" hidden aria-live="polite"></div>
|
||||
|
||||
<form method="post" action="{% url 'delete_selected' %}" id="library-form">
|
||||
{% csrf_token %}
|
||||
<div class="table-scroll">
|
||||
<table class="apple-table">
|
||||
@@ -1011,6 +1076,12 @@
|
||||
/* ---------- Hero search: alias autocomplete ---------- */
|
||||
setupMainSearch('main-search-input', 'main-search-results');
|
||||
|
||||
/* ---------- Desktop-only autofocus (never pop the keyboard on mobile) ---------- */
|
||||
const mainSearchInput = document.getElementById('main-search-input');
|
||||
if (mainSearchInput && window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
|
||||
mainSearchInput.focus();
|
||||
}
|
||||
|
||||
function setupMainSearch(inputId, resultsId) {
|
||||
const searchInput = document.getElementById(inputId);
|
||||
const searchResults = document.getElementById(resultsId);
|
||||
@@ -1145,33 +1216,166 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Link table client-side filter ---------- */
|
||||
const filterInput = document.getElementById('search-alias-input');
|
||||
const tbody = document.getElementById('links-table-body');
|
||||
if (filterInput && tbody) {
|
||||
filterInput.addEventListener('keyup', function () {
|
||||
const searchTerm = filterInput.value.toLowerCase().trim();
|
||||
let visibleCount = 0;
|
||||
/* ---------- Link library: search-driven management cards ---------- */
|
||||
const libFilter = document.getElementById('search-alias-input');
|
||||
const libForm = document.getElementById('library-form');
|
||||
const cardResults = document.getElementById('search-card-results');
|
||||
|
||||
tbody.querySelectorAll('.link-row').forEach(row => {
|
||||
const aliasElement = row.querySelector('td:nth-child(2) a');
|
||||
if (!aliasElement) return;
|
||||
const shouldShow = aliasElement.textContent.toLowerCase().includes(searchTerm);
|
||||
row.style.display = shouldShow ? '' : 'none';
|
||||
if (shouldShow) visibleCount++;
|
||||
});
|
||||
function debounce(fn, wait) {
|
||||
let t;
|
||||
return function (...args) { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), wait); };
|
||||
}
|
||||
const escapeHtml = s => s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
function highlightMatch(text, query) {
|
||||
const safe = escapeHtml(text || '');
|
||||
if (!query) return safe;
|
||||
try { return safe.replace(new RegExp('(' + escapeRegExp(escapeHtml(query)) + ')', 'gi'), '<mark>$1</mark>'); }
|
||||
catch (e) { return safe; }
|
||||
}
|
||||
|
||||
const noResultsRow = document.getElementById('no-results-row');
|
||||
if (visibleCount === 0) {
|
||||
if (!noResultsRow) {
|
||||
const newRow = document.createElement('tr');
|
||||
newRow.id = 'no-results-row';
|
||||
newRow.innerHTML = '<td colspan="6" style="text-align:center; padding: 2.5rem 1rem; color: var(--apple-gray);">{% trans "No matching links found" %}</td>';
|
||||
tbody.appendChild(newRow);
|
||||
}
|
||||
} else if (noResultsRow) {
|
||||
noResultsRow.remove();
|
||||
function badgeClassFor(item) {
|
||||
if (item.original_url && item.original_url.includes('{') && item.original_url.includes('}')) return 'badge-template';
|
||||
if (item.link_type === 'ACTION') return 'badge-action';
|
||||
if (item.link_type === 'LINK') return 'badge-link';
|
||||
return 'badge-custom';
|
||||
}
|
||||
function badgeHtmlFor(item) {
|
||||
if (item.original_url && item.original_url.includes('{') && item.original_url.includes('}')) return '<i class="fas fa-magic"></i>Template';
|
||||
if (item.link_type === 'ACTION') return '<i class="fas fa-bolt"></i>Action';
|
||||
if (item.link_type === 'LINK') return '<i class="fas fa-link"></i>Link';
|
||||
return '<i class="fas fa-code"></i>Custom';
|
||||
}
|
||||
|
||||
const eyeSvg = '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>';
|
||||
const pencilSvg = '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>';
|
||||
const trashSvg = '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>';
|
||||
|
||||
if (libFilter && cardResults && libForm) {
|
||||
let libFocus = -1;
|
||||
|
||||
function actionLink(href, color, svg, label) {
|
||||
const a = document.createElement('a');
|
||||
a.href = href;
|
||||
a.className = 'icon-btn ' + color;
|
||||
a.title = label;
|
||||
a.setAttribute('aria-label', label);
|
||||
a.innerHTML = svg;
|
||||
return a;
|
||||
}
|
||||
|
||||
function renderLibraryCards(data, query) {
|
||||
cardResults.innerHTML = '';
|
||||
libFocus = -1;
|
||||
|
||||
if (!data.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'search-empty-card';
|
||||
empty.textContent = '{% trans "No matching links found" %}';
|
||||
cardResults.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach((item, index) => {
|
||||
if (!item || typeof item !== 'object' || !('alias' in item)) return;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'search-card';
|
||||
|
||||
const main = document.createElement('div');
|
||||
main.className = 'search-card-main';
|
||||
main.setAttribute('role', 'button');
|
||||
main.setAttribute('tabindex', '0');
|
||||
main.setAttribute('aria-label', item.alias);
|
||||
main.dataset.detailUrl = item.detail_url || '';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'search-card-title';
|
||||
|
||||
const aliasLink = document.createElement('a');
|
||||
aliasLink.className = 'alias-link';
|
||||
aliasLink.href = item.url;
|
||||
aliasLink.target = '_blank';
|
||||
aliasLink.rel = 'noopener noreferrer';
|
||||
aliasLink.innerHTML = highlightMatch(item.alias, query);
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge ' + badgeClassFor(item);
|
||||
badge.innerHTML = badgeHtmlFor(item);
|
||||
|
||||
title.appendChild(aliasLink);
|
||||
title.appendChild(badge);
|
||||
|
||||
if (item.click_count) {
|
||||
const count = document.createElement('span');
|
||||
count.className = 'search-card-count';
|
||||
count.textContent = item.click_count + ' clicks';
|
||||
title.appendChild(count);
|
||||
}
|
||||
main.appendChild(title);
|
||||
|
||||
if (item.original_url) {
|
||||
const urlLine = document.createElement('div');
|
||||
urlLine.className = 'search-card-url';
|
||||
urlLine.textContent = item.original_url;
|
||||
main.appendChild(urlLine);
|
||||
}
|
||||
if (item.description) {
|
||||
const desc = document.createElement('div');
|
||||
desc.className = 'search-card-desc';
|
||||
desc.textContent = item.description;
|
||||
main.appendChild(desc);
|
||||
}
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'search-card-actions';
|
||||
actions.appendChild(actionLink(item.detail_url, 'blue', eyeSvg, '{% trans "View Details" %}'));
|
||||
actions.appendChild(actionLink(item.edit_url, 'green', pencilSvg, '{% trans "Edit" %}'));
|
||||
actions.appendChild(actionLink('/delete/' + item.id + '/', 'red', trashSvg, '{% trans "Delete" %}'));
|
||||
|
||||
card.appendChild(main);
|
||||
card.appendChild(actions);
|
||||
cardResults.appendChild(card);
|
||||
|
||||
main.addEventListener('click', () => {
|
||||
if (item.detail_url) window.location.href = item.detail_url;
|
||||
});
|
||||
main.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && item.detail_url) { e.preventDefault(); window.location.href = item.detail_url; }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setLibFocus(cards, index) {
|
||||
if (!cards.length) return;
|
||||
libFocus = ((index % cards.length) + cards.length) % cards.length;
|
||||
cards[libFocus].focus();
|
||||
}
|
||||
|
||||
libFilter.addEventListener('input', debounce(function () {
|
||||
const q = libFilter.value.trim();
|
||||
if (!q) {
|
||||
cardResults.hidden = true;
|
||||
libForm.hidden = false;
|
||||
return;
|
||||
}
|
||||
fetch(`/search/aliases/?q=${encodeURIComponent(q)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
renderLibraryCards(Array.isArray(data) ? data : [], q);
|
||||
cardResults.hidden = false;
|
||||
libForm.hidden = true;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error searching links:', error);
|
||||
});
|
||||
}, 250));
|
||||
|
||||
libFilter.addEventListener('keydown', function (e) {
|
||||
const cards = cardResults.querySelectorAll('.search-card-main');
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setLibFocus(cards, libFocus + 1); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); setLibFocus(cards, libFocus - 1); }
|
||||
else if (e.key === 'Enter' && libFocus > -1 && cards[libFocus]) { e.preventDefault(); cards[libFocus].click(); }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -94,13 +94,13 @@
|
||||
<script src="{% static 'js/common.js' %}"></script>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<nav id="main-nav" class="bg-custom-blue text-white p-4 fixed w-full top-0 z-50 nav-visible">
|
||||
<nav id="main-nav" class="p-4 fixed w-full top-0 z-50 nav-visible" style="background: rgba(255, 255, 255, 0.72); -webkit-backdrop-filter: blur(20px) saturate(180%); backdrop-filter: blur(20px) saturate(180%); border-bottom: 1px solid rgba(0, 0, 0, 0.06);">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<a href="{% url 'link_list' %}" class="text-xl font-bold">{% trans "GoLinks" %}</a>
|
||||
<a href="{% url 'link_list' %}" class="text-xl font-bold" style="color: #1d1d1f;">{% trans "GoLinks" %}</a>
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- "Menus" 下拉菜单 -->
|
||||
<div class="relative group">
|
||||
<button id="more-menu-button" class="flex items-center text-white hover:text-gray-200">
|
||||
<button id="more-menu-button" class="flex items-center" style="color: #1d1d1f;">
|
||||
<i class="fas fa-bars mr-2"></i>
|
||||
{% trans "Menu" %}
|
||||
</button>
|
||||
@@ -260,7 +260,7 @@
|
||||
</div>
|
||||
<!-- 语言选择器 -->
|
||||
<div class="relative">
|
||||
<button id="language-selector-button" class="flex items-center text-white hover:text-gray-200">
|
||||
<button id="language-selector-button" class="flex items-center" style="color: #1d1d1f;">
|
||||
<i class="fas fa-globe mr-2"></i>
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_language_info for LANGUAGE_CODE as current_language %}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Tests for the enhanced /search/aliases/ endpoint (search-driven library)."""
|
||||
import pytest
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
from links.models import Link, Tag
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_links(db):
|
||||
"""A small, deterministic link library for search tests."""
|
||||
l1 = Link.objects.create(
|
||||
alias="github", original_url="https://github.com", link_type="LINK", click_count=5,
|
||||
description="Code hosting",
|
||||
)
|
||||
l2 = Link.objects.create(
|
||||
alias="gitlab", original_url="https://gitlab.com", link_type="LINK", click_count=2,
|
||||
description="Another code host",
|
||||
)
|
||||
l3 = Link.objects.create(
|
||||
alias="docs", original_url="https://docs.example.com/{page,default=index}", link_type="LINK",
|
||||
click_count=0, description="Template link",
|
||||
)
|
||||
l4 = Link.objects.create(
|
||||
alias="notify", original_url="", link_type="ACTION",
|
||||
action_config={"action_type": "discord_send"}, click_count=0,
|
||||
)
|
||||
tag = Tag.objects.create(name="dev")
|
||||
l1.tags.add(tag)
|
||||
return [l1, l2, l3, l4]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_aliases_matches_alias_and_returns_enriched_fields(client, db_links):
|
||||
resp = client.get(reverse("search_aliases"), {"q": "git"})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
aliases = {item["alias"] for item in data}
|
||||
assert aliases == {"github", "gitlab"}
|
||||
|
||||
github = next(i for i in data if i["alias"] == "github")
|
||||
# Enriched management fields are present and correct
|
||||
assert github["id"] == db_links[0].id
|
||||
assert github["link_type"] == "LINK"
|
||||
assert github["detail_url"] == f"/link/{db_links[0].id}/"
|
||||
assert github["edit_url"] == f"/link/{db_links[0].id}/edit/"
|
||||
assert github["click_count"] == 5
|
||||
assert github["description"] == "Code hosting"
|
||||
assert github["original_url"] == "https://github.com"
|
||||
# Backwards-compatible fields (hero autocomplete relies on these)
|
||||
assert github["url"].endswith("/github/")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_aliases_matches_original_url(client, db_links):
|
||||
resp = client.get(reverse("search_aliases"), {"q": "gitlab.com"})
|
||||
assert resp.status_code == 200
|
||||
assert [i["alias"] for i in resp.json()] == ["gitlab"]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_aliases_matches_description(client, db_links):
|
||||
resp = client.get(reverse("search_aliases"), {"q": "template link"})
|
||||
assert resp.status_code == 200
|
||||
assert [i["alias"] for i in resp.json()] == ["docs"]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_aliases_matches_tag_name(client, db_links):
|
||||
resp = client.get(reverse("search_aliases"), {"q": "dev"})
|
||||
assert resp.status_code == 200
|
||||
assert [i["alias"] for i in resp.json()] == ["github"]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_aliases_priority_exact_first(client, db_links):
|
||||
resp = client.get(reverse("search_aliases"), {"q": "github"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()[0]["alias"] == "github"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_aliases_empty_query_returns_recent(client, db_links):
|
||||
# Touch l2 so it becomes the most recently updated (auto_now needs the
|
||||
# field explicitly listed in update_fields to refresh).
|
||||
db_links[1].click_count = 3
|
||||
db_links[1].save(update_fields=["click_count", "updated_at"])
|
||||
resp = client.get(reverse("search_aliases"))
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert len(data) >= 4
|
||||
assert data[0]["alias"] == "gitlab" # most recently updated first
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_aliases_action_type_surfaces(client, db_links):
|
||||
resp = client.get(reverse("search_aliases"), {"q": "notify"})
|
||||
assert resp.status_code == 200
|
||||
item = resp.json()[0]
|
||||
assert item["link_type"] == "ACTION"
|
||||
assert item["detail_url"] == f"/link/{db_links[3].id}/"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_search_aliases_no_results(client, db_links):
|
||||
resp = client.get(reverse("search_aliases"), {"q": "zzzznothing"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == []
|
||||
Reference in New Issue
Block a user