diff --git a/apps/api/serializers.py b/apps/api/serializers.py index a02119b..40d5ff7 100644 --- a/apps/api/serializers.py +++ b/apps/api/serializers.py @@ -125,7 +125,7 @@ class FilterSerializer(DynamicModelSerializer): class Meta: model = Filter - fields = ("id", "name", "keywords", "regex", "whitelist", "guild_id") + fields = ("id", "name", "matching_algorithm", "match", "is_insensitive", "is_whitelist", "guild_id") class ArticleMutatorSerializer(DynamicModelSerializer): diff --git a/apps/api/views.py b/apps/api/views.py index ddf40e1..108e93b 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -110,7 +110,7 @@ class Filter_ListView(generics.ListCreateAPIView): serializer_class = FilterSerializer filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter] - filterset_fields = ["id", "name", "keywords", "regex", "whitelist", "guild_id"] + filterset_fields = ["id", "name", "matching_algorithm", "match", "is_insensitive", "is_whitelist", "guild_id"] search_fields = ["name", "keywords", "regex"] def get_queryset(self): diff --git a/apps/home/admin.py b/apps/home/admin.py index 7c1a4f4..2b498a2 100644 --- a/apps/home/admin.py +++ b/apps/home/admin.py @@ -22,7 +22,7 @@ class SubChannelAdmin(admin.ModelAdmin): @admin.register(Filter) class FilterAdmin(admin.ModelAdmin): list_display = [ - "id", "name", "keywords", "regex", "whitelist", "guild_id" + "id", "name", "guild_id" ] diff --git a/apps/home/migrations/0015_alter_filter_options_remove_filter_keywords_and_more.py b/apps/home/migrations/0015_alter_filter_options_remove_filter_keywords_and_more.py new file mode 100644 index 0000000..1bbd443 --- /dev/null +++ b/apps/home/migrations/0015_alter_filter_options_remove_filter_keywords_and_more.py @@ -0,0 +1,67 @@ +# Generated by Django 5.0.4 on 2024-07-10 13:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0014_rename_published_theshold_subscription_published_threshold'), + ] + + operations = [ + migrations.AlterModelOptions( + name='filter', + options={'ordering': ('name',)}, + ), + migrations.RemoveField( + model_name='filter', + name='keywords', + ), + migrations.RemoveField( + model_name='filter', + name='regex', + ), + migrations.RemoveField( + model_name='filter', + name='whitelist', + ), + migrations.RemoveField( + model_name='subscription', + name='article_fetch_limit', + ), + migrations.RemoveField( + model_name='subscription', + name='reset_article_fetch_limit', + ), + migrations.AddField( + model_name='filter', + name='is_insensitive', + field=models.BooleanField(default=True, verbose_name='is insensitive'), + ), + migrations.AddField( + model_name='filter', + name='is_whitelist', + field=models.BooleanField(default=False, verbose_name='is whitelist'), + ), + migrations.AddField( + model_name='filter', + name='match', + field=models.CharField(blank=True, max_length=256, verbose_name='match'), + ), + migrations.AddField( + model_name='filter', + name='matching_algorithm', + field=models.PositiveIntegerField(choices=[(0, 'None'), (1, 'Any word'), (2, 'All words'), (3, 'Exact match'), (4, 'Regular expression'), (5, 'Fuzzy word'), (6, 'Automatic')], default=1, verbose_name='matching algorithm'), + ), + migrations.AlterField( + model_name='filter', + name='guild_id', + field=models.CharField(max_length=128, verbose_name='guild id'), + ), + migrations.AlterField( + model_name='filter', + name='name', + field=models.CharField(max_length=128, verbose_name='name'), + ), + ] diff --git a/apps/home/models.py b/apps/home/models.py index a3d03c1..d309a22 100644 --- a/apps/home/models.py +++ b/apps/home/models.py @@ -119,44 +119,49 @@ class SubChannel(models.Model): return self.channel_id +# using a brilliant matching model design from paperless-ngx src class Filter(models.Model): + MATCH_NONE = 0 + MATCH_ANY = 1 + MATCH_ALL = 2 + MATCH_LITERAL = 3 + MATCH_REGEX = 4 + MATCH_FUZZY = 5 + MATCH_AUTO = 6 + + MATCHING_ALGORITHMS = ( + # (MATCH_NONE, _("None")), + (MATCH_ANY, _("Any word")), + (MATCH_ALL, _("All words")), + (MATCH_LITERAL, _("Exact match")), + (MATCH_REGEX, _("Regular expression")), + (MATCH_FUZZY, _("Fuzzy word")), + # (MATCH_AUTO, _("Automatic")), + ) + id = models.AutoField(primary_key=True) - name = models.CharField( - max_length=32, - null=False, - blank=False + name = models.CharField(_("name"), max_length=128) + + match = models.CharField(_("match"), max_length=256, blank=True) + + matching_algorithm = models.PositiveIntegerField( + _("matching algorithm"), + choices=MATCHING_ALGORITHMS, + default=MATCH_ANY, ) - keywords = models.CharField( - max_length=128, - null=True, - blank=True - ) + is_insensitive = models.BooleanField(_("is insensitive"), default=True) - regex = models.CharField( - max_length=128, - null=True, - blank=True - ) - - whitelist = models.BooleanField(default=False) + is_whitelist = models.BooleanField(_("is whitelist"), default=False) # Have to use charfield instead of positiveBigIntegerField due to an Sqlite # issue that rounds down the value # https://github.com/sequelize/sequelize/issues/9335 - guild_id = models.CharField( - max_length=128 - ) + guild_id = models.CharField(_("guild id"), max_length=128) class Meta: - """ - Metadata for the Filter Model. - """ - - verbose_name = "filter" - verbose_name_plural = "filters" - get_latest_by = "id" + ordering = ("name",) constraints = [ models.UniqueConstraint( fields=["name", "guild_id"], @@ -164,8 +169,8 @@ class Filter(models.Model): ) ] - def __str__(self) -> str: - return self.name + def __str__(self): + return f"{self.guild_id} - {self.name}" class Subscription(models.Model): @@ -217,13 +222,6 @@ class Subscription(models.Model): blank=True, ) - article_fetch_limit = models.PositiveSmallIntegerField( - validators = [MaxValueValidator(1), MinValueValidator(10)], - default=10 - ) - - reset_article_fetch_limit = models.BooleanField(default=False) - embed_colour = models.CharField( max_length=6, default="3498db", diff --git a/apps/static/js/home/filters.js b/apps/static/js/home/filters.js index d909492..802d59e 100644 --- a/apps/static/js/home/filters.js +++ b/apps/static/js/home/filters.js @@ -39,49 +39,43 @@ function initFiltersTable() { } }, { - title: "Match", - // data: "keywords", - className: "text-start text-truncate", - render: function(data, type, row) { - return row.regex ? row.regex : row.keywords; + title: "Matching Algorithm", + data: "matching_algorithm", + render: function(data) { + switch (data) { + case 1: return "Any Word"; + case 2: return "All Words"; + case 3: return "Exact Match"; + case 4: return "Regular Expression"; + case 5: return "Fuzzy Match"; + default: + console.error(`unknown matching algorithm '${data}'`); + return data; + } } }, { - title: "Type", - // data: "regex", - render: function(data, type, row) { - return row.regex ? "Regex" : "Keywords"; + title: "Match", + data: "match" + }, + { + title: "Case Sensitivity", + data: "is_insensitive", + render: function(data) { + return data ? "Insensitive" : "Sensitive" } }, { title: "Control List", - data: "whitelist", + data: "is_whitelist", render: function(data) { - return data ? "whitelist" : "blacklist"; + return data ? "Whitelist" : "Blacklist"; } } ] }); } -$("#filterAdvancedMode").on("change", function() { - const advancedMode = $(this).prop("checked"); - setFilterAdvancedMode(advancedMode); -}); - -function setFilterAdvancedMode(advancedMode) { - if (advancedMode) { - $("#filterForm .simple-filtering").hide(); - $("#filterForm .advanced-filtering").show(); - $("#filterForm .advanced-filtering").val(""); - } - else { - $("#filterForm .advanced-filtering").hide(); - $("#filterForm .simple-filtering").show(); - $("#filterForm .simple-filtering").val(""); - } -} - $("#addFilterBtn").on("click", async function() { await showEditFilterModal(-1); }) @@ -90,11 +84,10 @@ async function showEditFilterModal(filterId) { if (filterId === -1) { $("#filterFormModal input, #filterFormModal textarea").val(""); + $("#filterFormModal input:checkbox").prop("checked", false); + $("#filterFormModal .form-create").show(); $("#filterFormModal .form-edit").hide(); - $("#filterAdvancedMode").prop("checked", false); - $("#filterAdvancedMode").trigger("change"); - $("#filterWhitelist").prop("checked", false); } else { const filter = filtersTable.row(function(idx, data, node) { @@ -102,14 +95,10 @@ async function showEditFilterModal(filterId) { }).data(); $("#filterName").val(filter.name); - $("#filterKeywords").val(filter.keywords); - $("#filterRegex").val(filter.regex); - - $("#filterAdvancedMode").prop("checked", filter.regex !== ""); - $("#filterAdvancedMode").trigger("change"); - - $("#filterWhitelist").prop("checked", filter.whitelist); - $("#filterWhitelist").trigger("change"); + $("#filterAlgorithm").val(filter.matching_algorithm); + $("#filterMatch").val(filter.match); + $("#filterWhitelist").prop("checked", filter.is_whitelist); + $("#filterInsensitive").prop("checked", filter.is_insensitive); $("#filterFormModal .form-create").hide(); $("#filterFormModal .form-edit").show(); @@ -122,22 +111,15 @@ async function showEditFilterModal(filterId) { $("#filterForm").on("submit", async function(event) { event.preventDefault(); - var advancedFiltering = $("#filterAdvancedMode").prop("checked"); - id = $("#filterId").val(); + var id = $("#filterId").val(); name = $("#filterName").val(); - keywords = advancedFiltering ? "" : $("#filterKeywords").val(); - regex = advancedFiltering ? $("#filterRegex").val() : ""; - whitelistMode = $("#filterWhitelist").prop("checked"); + algorithm = $("#filterAlgorithm option:selected").val(); + match = $("#filterMatch").val(); + isWhitelist = $("#filterWhitelist").prop("checked"); + isInsensitive = $("#filterInsensitive").prop("checked"); guildId = getCurrentlyActiveServer().guild_id; - if (advancedFiltering) { - keywords = ""; - } - else { - regex = "" - } - - var filterPrimaryKey = await saveFilter(id, name, keywords, regex, whitelistMode, guildId); + var filterPrimaryKey = await saveFilter(id, name, algorithm, match, isWhitelist, isInsensitive, guildId); if (filterPrimaryKey) { showToast("success", "Filter Saved", "Filter ID " + filterPrimaryKey); @@ -148,12 +130,13 @@ $("#filterForm").on("submit", async function(event) { $("#filterFormModal").modal("hide"); }); -async function saveFilter(id, name, keywords, regex, whitelistMode, guildId) { +async function saveFilter(id, name, algorithm, match, isWhitelist, isInsensitive, guildId) { var formData = new FormData(); formData.append("name", name); - formData.append("keywords", keywords); - formData.append("regex", regex); - formData.append("whitelist", whitelistMode) + formData.append("matching_algorithm", algorithm); + formData.append("match", match); + formData.append("is_whitelist", isWhitelist); + formData.append("is_insensitive", isInsensitive); formData.append("guild_id", guildId); var response; @@ -206,6 +189,44 @@ async function loadFilters(guildId, page=1, pageSize=null) { } } +$(document).ready(function() { + loadMatchingAlgorithms(); +}); + +function loadMatchingAlgorithms() { + // Disable input while options are loading + $("#filterAlgorithm").prop("disabled", true); + + // Delete existing options + $("#filterAlgorithm option").each(function() { + if ($(this).val()) + $(this).remove(); + }); + + // Clear select2 input + $("#filterAlgorithm").val("").change(); + + algorithms = [ + {id: "", name: "None"}, + {id: 1, name: "Any Words"}, + {id: 2, name: "All Words"}, + {id: 3, name: "Exact Match"}, + {id: 4, name: "Regular Expression"}, + {id: 5, name: "Fuzzy Match"}, + ] + + algorithms.forEach(algorithm => { + $("#filterAlgorithm").append($("