diff --git a/routes/products.py b/routes/products.py index 3758612..135ec4c 100644 --- a/routes/products.py +++ b/routes/products.py @@ -265,7 +265,9 @@ def products_list(): if supplier_filter: where.append("(s.supplier_name LIKE %s OR p.supplier_name_match LIKE %s)") params.extend([f"%{supplier_filter}%", f"%{supplier_filter}%"]) - if brand_filter: + if brand_filter == '__EMPTY__': + where.append("(p.brand IS NULL OR TRIM(p.brand)='')") + elif brand_filter: where.append("TRIM(p.brand)=%s") params.append(brand_filter) if category1_filter: @@ -405,10 +407,17 @@ def products_filters(): f"SELECT DISTINCT category1 FROM products WHERE {scope_clause}" "category1 IS NOT NULL AND category1 != '' ORDER BY category1" ) + empty_brand_count = query( + f"SELECT COUNT(*) AS cnt FROM products WHERE {scope_clause}" + "(brand IS NULL OR TRIM(brand) = '')" + )[0]['cnt'] + brand_values = [b['brand'] for b in brands] + if empty_brand_count: + brand_values.insert(0, '__EMPTY__') return jsonify({ "code": 0, "data": { - "brands": [b['brand'] for b in brands], + "brands": brand_values, "categories": [c['category1'] for c in categories] } }) @@ -2086,7 +2095,9 @@ def products_recycle_bin(): if supplier_filter: where.append("(p.supplier_name_match LIKE %s OR s.supplier_name LIKE %s)") params.extend([f"%{supplier_filter}%", f"%{supplier_filter}%"]) - if brand_filter: + if brand_filter == '__EMPTY__': + where.append("(p.brand IS NULL OR TRIM(p.brand)='')") + elif brand_filter: where.append("TRIM(p.brand)=%s") params.append(brand_filter) if product_type: diff --git a/static/js/app.js b/static/js/app.js index c4ead29..a901e1a 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -8322,7 +8322,8 @@ function loadProductBrandFilter(tab) { if (!document.body.contains(sel) || !res || res.code !== 0) return; var brands = (res.data && res.data.brands) || []; sel.innerHTML = '' + brands.map(function(brand) { - return ''; + var label = brand === '__EMPTY__' ? '(品牌为空)' : brand; + return ''; }).join(''); }); } diff --git a/tests/test_product_brand_filter.py b/tests/test_product_brand_filter.py index 6ba47a0..894c11b 100644 --- a/tests/test_product_brand_filter.py +++ b/tests/test_product_brand_filter.py @@ -18,6 +18,7 @@ class ProductBrandFilterStaticTest(unittest.TestCase): self.assertIn("loadProductBrandFilter('成品')", APP_JS) self.assertIn('loadProductBrandFilter(tab)', APP_JS) self.assertGreaterEqual(APP_JS.count("url += '&brand='"), 2) + self.assertIn("brand === '__EMPTY__' ? '(品牌为空)' : brand", APP_JS) class ProductBrandFilterApiTest(unittest.TestCase): @@ -35,16 +36,43 @@ class ProductBrandFilterApiTest(unittest.TestCase): self.assertIn('TRIM(p.brand)=%s', query.call_args_list[0].args[0]) self.assertEqual(query.call_args_list[0].args[1], ['无']) + def test_active_products_filter_empty_brand(self): + with mock.patch.object(products, 'query', side_effect=[[], [{'cnt': 0}], []]) as query: + response = self.client.get('/api/products/list?brand=__EMPTY__') + + self.assertEqual(response.get_json()['code'], 0) + self.assertIn("(p.brand IS NULL OR TRIM(p.brand)='')", query.call_args_list[1].args[0]) + self.assertEqual(query.call_args_list[1].args[1], []) + + def test_recycle_bin_filters_empty_brand(self): + with mock.patch.object(products, 'query', side_effect=[[{'cnt': 0}], []]) as query: + response = self.client.get('/api/products/recycle_bin?brand=__EMPTY__') + + self.assertEqual(response.get_json()['code'], 0) + self.assertIn("(p.brand IS NULL OR TRIM(p.brand)='')", query.call_args_list[0].args[0]) + self.assertEqual(query.call_args_list[0].args[1], []) + def test_filter_options_can_be_scoped_to_recycle_bin(self): with mock.patch.object(products, 'query', side_effect=[ [{'brand': '回收品牌'}], [{'category1': '分类'}], + [{'cnt': 2}], ]) as query: response = self.client.get('/api/products/filters?scope=recycle') - self.assertEqual(response.get_json()['data']['brands'], ['回收品牌']) + self.assertEqual(response.get_json()['data']['brands'], ['__EMPTY__', '回收品牌']) self.assertIn('deleted_at IS NOT NULL', query.call_args_list[0].args[0]) + def test_filter_options_omit_empty_brand_when_scope_has_none(self): + with mock.patch.object(products, 'query', side_effect=[ + [{'brand': '正常品牌'}], + [], + [{'cnt': 0}], + ]): + response = self.client.get('/api/products/filters?scope=active') + + self.assertEqual(response.get_json()['data']['brands'], ['正常品牌']) + if __name__ == '__main__': unittest.main()