diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5dc4c32..204b60d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.2.2 +current_version = 0.3.0 commit = True tag = True diff --git a/CHANGELOG.md b/CHANGELOG.md index 9115d01..4ee2344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ -**unreleased** +**breaking changes v.0.3.0** + +- Enhancement: store guild settings in separate model +- Breaking: server `default_embed_colour` setting will be reset due to changes on how settings are stored +- Enhancement: moved server settinsg from tab to pop-out modal +- Enhancement: `active` flag added as server setting, soft-toggles activeness of associated subscriptions +- Fix: Bad text alignment for some table button links +- Fix & Enhancement: some small clean-up/patches here and there +- Fix: Incorrect margin on 'server settings' & 'new server' modal buttons +- Fix: Table "X Results" text now correctly plural only if `results > 1` **v0.2.2** diff --git a/apps/api/serializers.py b/apps/api/serializers.py index 473ee94..e827bd2 100644 --- a/apps/api/serializers.py +++ b/apps/api/serializers.py @@ -173,7 +173,7 @@ class SavedGuildSerializer(DynamicModelSerializer): class Meta: model = SavedGuilds - fields = ("id", "guild_id", "name", "icon", "added_by", "permissions", "default_embed_colour", "owner") + fields = ("id", "guild_id", "name", "icon", "added_by", "permissions", "owner") class GuildSettingsSerializer(DynamicModelSerializer): @@ -183,7 +183,7 @@ class GuildSettingsSerializer(DynamicModelSerializer): class Meta: model = GuildSettings - fields = ("id", "guild_id", "default_embed_colour") + fields = ("id", "guild_id", "default_embed_colour", "active") class TrackedContentSerializer_GET(DynamicModelSerializer): diff --git a/apps/api/views.py b/apps/api/views.py index 6d68d5f..5015346 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -405,7 +405,7 @@ class GuildSettings_ListView(generics.ListCreateAPIView): metadata_class = ExpandedMetadata filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter] - filterset_fields = ["id", "guild_id", "default_embed_colour"] + filterset_fields = ["id", "guild_id", "default_embed_colour", "active"] def get_queryset(self): if self.request.user.is_superuser: @@ -414,9 +414,7 @@ class GuildSettings_ListView(generics.ListCreateAPIView): return GuildSettings.objects.filter(added_by=self.request.user) def post(self, request): - - guild_id = request.data["guild_id"] - saved_guilds = SavedGuild.objects.filter(added_by=request.user) + saved_guilds = SavedGuilds.objects.filter(added_by=request.user) if not saved_guilds: return Response( @@ -445,7 +443,7 @@ class GuildSettings_ListView(generics.ListCreateAPIView): return (int(permissions) & 1 << 3) == 1 << 3 -class GuildSettings_DetailView(generics.RetrieveDestroyAPIView): +class GuildSettings_DetailView(generics.RetrieveUpdateDestroyAPIView): """ View to provide details on a particular GuildSettings model instances. @@ -462,7 +460,7 @@ class GuildSettings_DetailView(generics.RetrieveDestroyAPIView): if self.request.user.is_superuser: return GuildSettings.objects.all() - saved_guilds = SavedGuild.objects.filter(added_by=self.request.user) + saved_guilds = SavedGuilds.objects.filter(added_by=self.request.user) guild_ids = [guild.guild_id for guild in saved_guilds] return GuildSettings.objects.filter(guild_id__in=guild_ids) diff --git a/apps/home/admin.py b/apps/home/admin.py index 2b498a2..f6855fc 100644 --- a/apps/home/admin.py +++ b/apps/home/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin -from .models import Subscription, SavedGuilds, Filter, SubChannel, TrackedContent, ArticleMutator +from .models import Subscription, SavedGuilds, Filter, SubChannel, TrackedContent, ArticleMutator, GuildSettings @admin.register(Subscription) @@ -44,4 +44,10 @@ class SavedGuildAdmin(admin.ModelAdmin): class ArticleMutatorAdmin(admin.ModelAdmin): list_display = [ "id", "name", "value" - ] \ No newline at end of file + ] + +@admin.register(GuildSettings) +class GuildSettingsAdmin(admin.ModelAdmin): + list_display = [ + "id", "guild_id", "default_embed_colour", "active" + ] diff --git a/apps/home/migrations/0021_alter_guildsettings_options_and_more.py b/apps/home/migrations/0021_alter_guildsettings_options_and_more.py new file mode 100644 index 0000000..1ff9025 --- /dev/null +++ b/apps/home/migrations/0021_alter_guildsettings_options_and_more.py @@ -0,0 +1,122 @@ +# Generated by Django 5.0.4 on 2024-08-14 20:41 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0020_guildsettings'), + ] + + operations = [ + migrations.AlterModelOptions( + name='guildsettings', + options={'get_latest_by': 'id', 'verbose_name': 'guild settings', 'verbose_name_plural': 'guild settings'}, + ), + migrations.RemoveField( + model_name='savedguilds', + name='default_embed_colour', + ), + migrations.AddField( + model_name='guildsettings', + name='active', + field=models.BooleanField(default=True, help_text='Subscriptions of inactive guilds will also be treated as inactive', verbose_name='Active'), + ), + migrations.AlterField( + model_name='guildsettings', + name='guild_id', + field=models.CharField(help_text='Discord snowflake ID for the represented guild.', max_length=128, unique=True, verbose_name='guild id'), + ), + migrations.AlterField( + model_name='savedguilds', + name='id', + field=models.AutoField(primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='subscription', + name='active', + field=models.BooleanField(default=True, verbose_name='Active'), + ), + migrations.AlterField( + model_name='subscription', + name='article_fetch_image', + field=models.BooleanField(default=True, help_text='Will the resulting article have an image?', verbose_name='Fetch Article Images'), + ), + migrations.AlterField( + model_name='subscription', + name='creation_datetime', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Created At'), + ), + migrations.AlterField( + model_name='subscription', + name='embed_colour', + field=models.CharField(blank=True, default='3498db', max_length=6, verbose_name='Embed Colour'), + ), + migrations.AlterField( + model_name='subscription', + name='extra_notes', + field=models.CharField(blank=True, max_length=250, null=True, verbose_name='Extra Notes'), + ), + migrations.AlterField( + model_name='subscription', + name='guild_id', + field=models.CharField(max_length=128, verbose_name='Guild ID'), + ), + migrations.AlterField( + model_name='subscription', + name='name', + field=models.CharField(max_length=32, verbose_name='Name'), + ), + migrations.AlterField( + model_name='subscription', + name='published_threshold', + field=models.DateTimeField(blank=True, default=django.utils.timezone.now, verbose_name='Published Threshold'), + ), + migrations.AlterField( + model_name='subscription', + name='url', + field=models.URLField(verbose_name='URL'), + ), + migrations.AlterField( + model_name='trackedcontent', + name='blocked', + field=models.BooleanField(default=False, verbose_name='Blocked'), + ), + migrations.AlterField( + model_name='trackedcontent', + name='channel_id', + field=models.CharField(max_length=128, verbose_name='Channel ID'), + ), + migrations.AlterField( + model_name='trackedcontent', + name='creation_datetime', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Created At'), + ), + migrations.AlterField( + model_name='trackedcontent', + name='guid', + field=models.CharField(help_text='RSS provided GUID of the content', max_length=256, verbose_name='GUID'), + ), + migrations.AlterField( + model_name='trackedcontent', + name='id', + field=models.AutoField(primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='trackedcontent', + name='message_id', + field=models.CharField(max_length=128, verbose_name='Message ID'), + ), + migrations.AlterField( + model_name='trackedcontent', + name='title', + field=models.CharField(max_length=728, verbose_name='Title'), + ), + migrations.AlterField( + model_name='trackedcontent', + name='url', + field=models.URLField(verbose_name='URL'), + ), + ] diff --git a/apps/home/migrations/0022_populate_guild_settings.py b/apps/home/migrations/0022_populate_guild_settings.py new file mode 100644 index 0000000..e00e7e4 --- /dev/null +++ b/apps/home/migrations/0022_populate_guild_settings.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.4 on 2024-08-14 20:46 + +from django.db import migrations + + +def create_missing_guild_settings(apps, scheme_editor): + SavedGuilds = apps.get_model("home", "SavedGuilds") + GuildSettings = apps.get_model("home", "GuildSettings") + + for saved_guild in SavedGuilds.objects.all(): + GuildSettings.objects.get_or_create(guild_id=saved_guild.guild_id) + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0021_alter_guildsettings_options_and_more'), + ] + + operations = [ + migrations.RunPython(create_missing_guild_settings), + ] diff --git a/apps/home/models.py b/apps/home/models.py index 9d5138b..6c0fe2e 100644 --- a/apps/home/models.py +++ b/apps/home/models.py @@ -23,7 +23,8 @@ class GuildSettings(models.Model): guild_id = models.CharField( verbose_name=_("guild id"), max_length=128, - help_text=_("Discord snowflake ID for the represented guild.") + help_text=_("Discord snowflake ID for the represented guild."), + unique=True ) default_embed_colour = models.CharField( @@ -33,6 +34,21 @@ class GuildSettings(models.Model): blank=True ) + active = models.BooleanField( + verbose_name=_("Active"), + default=True, + help_text=_("Subscriptions of inactive guilds will also be treated as inactive") + ) + + class Meta: + """ + Metadata for the GuildSettings model. + """ + + verbose_name = "guild settings" + verbose_name_plural = "guild settings" + get_latest_by = "id" + class SavedGuilds(models.Model): """ @@ -79,12 +95,6 @@ class SavedGuilds(models.Model): help_text=_("Does the 'added by' user own this guild?") ) - default_embed_colour = models.CharField( - max_length=6, - default="3498db", - blank=True - ) - class Meta: """ Metadata for the SavedGuilds Model. @@ -109,6 +119,10 @@ class SavedGuilds(models.Model): def settings(self): return GuildSettings.objects.get(guild_id=self.guild_id) + def save(self, *args, **kwargs): + GuildSettings.objects.get_or_create(guild_id=self.guild_id) + super().save(*args, **kwargs) + class SubChannel(models.Model): """ diff --git a/apps/static/js/api.js b/apps/static/js/api.js index f3e1047..da3fd29 100644 --- a/apps/static/js/api.js +++ b/apps/static/js/api.js @@ -24,7 +24,6 @@ async function ajaxRequest(url, method, data) { } function makeQuerystring(filters, sort) { - console.log(JSON.stringify(filters, null, 4)) let querystring = "?"; for (key in filters) { querystring += `${key}=${filters[key]}&`; @@ -163,4 +162,14 @@ async function getTrackedContentOptions() { async function getMutators() { return await ajaxRequest("/api/article-mutator/?page_size=25", "GET"); -} \ No newline at end of file +} + +// guild settings + +async function getGuildSettings(guildId) { + return await ajaxRequest(`/api/guild-settings/?guild_id=${guildId}`, "GET"); +} + +async function editGuildSettings(id, formData) { + return await ajaxRequest(`/api/guild-settings/${id}/`, "PUT", formData); +} diff --git a/apps/static/js/home/content.js b/apps/static/js/home/content.js index 41d4752..0134449 100644 --- a/apps/static/js/home/content.js +++ b/apps/static/js/home/content.js @@ -46,7 +46,7 @@ async function initContentTable() { data: "title", className: "text-truncate", render: function(data, type, row) { - return `${data}` + return `${data}` } }, { @@ -54,7 +54,7 @@ async function initContentTable() { data: "subscription.name", className: "text-nowrap", render: function(data, type, row) { - return `` + return `` } }, { @@ -143,7 +143,7 @@ function resolveChannelNames(guildId) { $("").text(channel.text) .attr("href", href) .attr("target", "_blank") - .addClass("btn btn-link text-decoration-none text-nowrap") + .addClass("btn btn-link text-start text-decoration-none text-nowrap") ); } }); diff --git a/apps/static/js/home/filters.js b/apps/static/js/home/filters.js index 73d2ebc..38443e2 100644 --- a/apps/static/js/home/filters.js +++ b/apps/static/js/home/filters.js @@ -40,7 +40,7 @@ async function initFiltersTable() { title: "Name", data: "name", render: function(data, type, row) { - return `` + return `` } }, { diff --git a/apps/static/js/home/index.js b/apps/static/js/home/index.js index acf55ed..8e00c97 100644 --- a/apps/static/js/home/index.js +++ b/apps/static/js/home/index.js @@ -86,11 +86,30 @@ $(".colour-control-text").on("change", function() { }); function updateColourInput(id, hexString) { - hexString = hexString.toUpperCase(); + hexString = normaliseHexString(hexString.toUpperCase()); $(`#${id} .colour-picker`).val(hexString); $(`#${id} .colour-text`).val(hexString); } +function getColourInputVal(id, includeHashtag=true) { + const hexString = $(`#${id}Text`).val(); + return normaliseHexString(hexString, includeHashtag); +} + +function normaliseHexString(hexString, includeHashtag=true) { + console.debug(`normalising hex string '${hexString}' include hashtag '${includeHashtag}'`); + + // Remove any non-hex characters (e.g., additional hashtags) + hexString = hexString.replace(/[^A-F0-9]/gi, ''); + + // Ensure the hex string has a valid length of either 3, 6, or 8 characters + if (![3, 6, 8].includes(hexString.length)) { + throw new Error(`Invalid hex string length. Must be 3, 6, or 8 characters. hexString=${hexString}`); + } + + return includeHashtag ? `#${hexString}` : hexString; +} + $(document).ready(function() { $(".colour-input").each(function() { let id = $(this).attr("data-id") diff --git a/apps/static/js/home/settings.js b/apps/static/js/home/settings.js index 64b95ac..11514d6 100644 --- a/apps/static/js/home/settings.js +++ b/apps/static/js/home/settings.js @@ -1,20 +1,59 @@ -/** Here is how this should work - * - * Currently SavedGuild is created and destroyed on demand, it's not viable to store settings here - * instead lets create a new database obj `GuildSettings` to store these, an automated job can erase - * a `GuildSettings` instance if no matching `SavedGuilds` are found within x number of days. - * - * The tables will not be linked directly, but will share a common `guild_id` field to find - * each other, or the saved guild will have a foreign key for the related `GuildSettings`. -*/ - -$(document).on("selectedServerChange", async function() { - server = getCurrentlyActiveServer(); - updateColourInput("defaultEmbedColour", "#" + server.default_embed_colour); +$("#serverSettingsBtn").on("click", async function() { + await showServerSettingsModal(); }); -$("#serverSettingsForm").on("submit", function(e) { +async function showServerSettingsModal() { + const server = getCurrentlyActiveServer(); + var guildSettings; + + try { guildSettings = (await getGuildSettings(server.guild_id))[0] } + catch (error) { + console.error(error) + return; + } + + $("#guildSettingsId").val(guildSettings.id); + $("#guildSettingsGuildId").val(guildSettings.guild_id); + $("#guildSettingsActive").prop("checked", guildSettings.active); + updateColourInput("guildSettingsDefaultEmbedColour", guildSettings.default_embed_colour); + + $("#serverSettingsModal").modal("show"); +} + +$("#serverSettingsForm").on("submit", async function(e) { e.preventDefault(); - alert("not implemented"); -}) \ No newline at end of file + + var id = $("#guildSettingsId").val(); + guildId = $("#guildSettingsGuildId").val(); + active = $("#guildSettingsActive").prop("checked"); + defaultEmbedColour = getColourInputVal("guildSettingsDefaultEmbedColour", false); + + const pk = await saveGuildSettings(id, guildId, defaultEmbedColour, active); + + if (pk) { + showToast("success", "Server Settings Saved", "Primary Key: " + pk); + } + + updateDefaultSubEmbedColour(); + $("#serverSettingsModal").modal("hide"); + +}) + +async function saveGuildSettings(id, guildId, defaultEmbedColour, active) { + var formData = new FormData(); + formData.append("guild_id", guildId); + formData.append("default_embed_colour", defaultEmbedColour); + formData.append("active", active); + + var response; + try { + response = await editGuildSettings(id, formData); + } + catch (err) { + console.error(err); + return false; + } + + return response.id; +} diff --git a/apps/static/js/home/subscriptions.js b/apps/static/js/home/subscriptions.js index e262095..a0d2620 100644 --- a/apps/static/js/home/subscriptions.js +++ b/apps/static/js/home/subscriptions.js @@ -43,7 +43,7 @@ async function initSubscriptionTable() { data: "name", className: "text-truncate", render: function(data, type, row) { - return ``; + return ``; } }, { @@ -51,7 +51,7 @@ async function initSubscriptionTable() { data: "url", className: "text-truncate", render: function(data, type) { - return `${data}`; + return `${data}`; } }, { @@ -181,7 +181,6 @@ $("#addSubscriptionBtn").on("click", async function() { }); async function showEditSubModal(subId) { - if (subId === -1) { $("#subFormModal .form-create, #subAdvancedModal .form-create").show(); $("#subFormModal .form-edit, #subAdvancedModal .form-edit").hide(); @@ -374,7 +373,7 @@ $(document).on("selectedServerChange", async function() { let server = getCurrentlyActiveServer(); guildId = server.guild_id; - $("#subEmbedColour .colour-reset").attr("data-defaultcolour", "#" + server.default_embed_colour); + await updateDefaultSubEmbedColour(); await loadSubscriptions(guildId); await loadChannelOptions(guildId); @@ -382,6 +381,13 @@ $(document).on("selectedServerChange", async function() { await loadMutatorOptions(); }) +async function updateDefaultSubEmbedColour(settings=null) { + if (!settings){ + settings = (await getGuildSettings(guildId))[0] + } + $("#subEmbedColour .colour-reset").attr("data-defaultcolour", "#" + settings.default_embed_colour); +} + // #endregion diff --git a/apps/static/js/table.js b/apps/static/js/table.js index 6154536..9c607ed 100644 --- a/apps/static/js/table.js +++ b/apps/static/js/table.js @@ -321,7 +321,7 @@ function createTableControls(containingSelector, pageSizeId) {
-  Results +
@@ -382,7 +382,7 @@ function updateTableContainer(containerId, page, pageSize, itemsCount, totalItem // Updates the pagination text for a given pageInfoId function updateTablePaginationInfo(pageInfoId, showing, total) { $(`${pageInfoId} .pageinfo-showing`).text(showing); - $(`${pageInfoId} .pageinfo-total`).text(total); + $(`${pageInfoId} .pageinfo-total`).text(`${total} Result${total > 1 ? "s" : ""}`); } // Updates the pagination buttons for a given pageControlsId diff --git a/apps/templates/home/includes/servermodal.html b/apps/templates/home/includes/servermodal.html index c23b7a7..49b4ade 100644 --- a/apps/templates/home/includes/servermodal.html +++ b/apps/templates/home/includes/servermodal.html @@ -23,7 +23,7 @@ You must be an administrator, or own the selected server.

-
- + -
@@ -93,7 +96,7 @@
-
{% include "home/includes/settingstab.html" %}
+
@@ -104,6 +107,7 @@ {% include "home/includes/submodal.html" %} {% include "home/includes/filtermodal.html" %} {% include "home/includes/deletemodal.html" %} +{% include "home/includes/settingsmodal.html" %} {% endblock content %} {% block javascript %} diff --git a/core/settings.py b/core/settings.py index 6d176b1..f54435f 100644 --- a/core/settings.py +++ b/core/settings.py @@ -9,7 +9,7 @@ from django.utils import timezone log = logging.getLogger(__name__) -VERSION = "0.2.2" +VERSION = "0.3.0" # BASE_DIR is the root of the project, all paths should be constructed from it using pathlib BASE_DIR = Path(__file__).parent.parent