Merge pull request 'v0.3.0' (#44) from staging into master
All checks were successful
Build and Push Docker Image / build (push) Successful in 24s

Reviewed-on: https://gitea.corbz.dev/corbz/PYRSS-Website/pulls/44
This commit is contained in:
Corban-Lee Jones 2024-08-16 18:51:49 +00:00
commit 5237f6fbf9
19 changed files with 340 additions and 54 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.2.2
current_version = 0.3.0
commit = True
tag = True

View File

@ -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**

View File

@ -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):

View File

@ -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)

View File

@ -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"
]
]
@admin.register(GuildSettings)
class GuildSettingsAdmin(admin.ModelAdmin):
list_display = [
"id", "guild_id", "default_embed_colour", "active"
]

View File

@ -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'),
),
]

View File

@ -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),
]

View File

@ -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):
"""

View File

@ -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");
}
}
// 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);
}

View File

@ -46,7 +46,7 @@ async function initContentTable() {
data: "title",
className: "text-truncate",
render: function(data, type, row) {
return `<a href="${row.url}" class="btn btn-link text-decoration-none" target="_blank">${data}</a>`
return `<a href="${row.url}" class="btn btn-link text-start text-decoration-none" target="_blank">${data}</a>`
}
},
{
@ -54,7 +54,7 @@ async function initContentTable() {
data: "subscription.name",
className: "text-nowrap",
render: function(data, type, row) {
return `<button type="button" onclick="goToSubscription(${row.subscription.id})" class="btn btn-link text-decoration-none">${data}</button>`
return `<button type="button" onclick="goToSubscription(${row.subscription.id})" class="btn btn-link text-start text-decoration-none">${data}</button>`
}
},
{
@ -143,7 +143,7 @@ function resolveChannelNames(guildId) {
$("<a>").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")
);
}
});

View File

@ -40,7 +40,7 @@ async function initFiltersTable() {
title: "Name",
data: "name",
render: function(data, type, row) {
return `<button type="button" onclick="showEditFilterModal(${row.id})" class="btn btn-link text-decoration-none">${data}</button>`
return `<button type="button" onclick="showEditFilterModal(${row.id})" class="btn btn-link text-start text-decoration-none">${data}</button>`
}
},
{

View File

@ -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")

View File

@ -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");
})
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;
}

View File

@ -43,7 +43,7 @@ async function initSubscriptionTable() {
data: "name",
className: "text-truncate",
render: function(data, type, row) {
return `<button type="button" onclick="showEditSubModal(${row.id})" class="btn btn-link text-decoration-none">${data}</button>`;
return `<button type="button" onclick="showEditSubModal(${row.id})" class="btn btn-link text-start text-decoration-none">${data}</button>`;
}
},
{
@ -51,7 +51,7 @@ async function initSubscriptionTable() {
data: "url",
className: "text-truncate",
render: function(data, type) {
return `<a href="${data}" class="btn btn-link text-decoration-none" target="_blank">${data}</a>`;
return `<a href="${data}" class="btn btn-link text-start text-decoration-none" target="_blank">${data}</a>`;
}
},
{
@ -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

View File

@ -321,7 +321,7 @@ function createTableControls(containingSelector, pageSizeId) {
<div class="table-controls row mb-3 px-3">
<div class="col-lg-2">
<div class="table-page-info d-flex justify-content-start align-items-center mx-auto">
<span class="pageinfo-total"></span>&nbsp;Results
<span class="pageinfo-total"></span>
</div>
</div>
<div class="col-lg-8">
@ -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

View File

@ -23,7 +23,7 @@
You must be an administrator, or own the selected server.
</p>
</div>
<div class="modal-footer">
<div class="modal-footer px-4">
<button type="submit" class="btn btn-primary rounded-1 me-0">Submit</button>
<button type="button" class="btn btn-secondary rounded-1 ms-3" data-bs-dismiss="modal">Cancel</button>
</div>

View File

@ -0,0 +1,38 @@
<div id="serverSettingsModal" class="modal fade" data-bs-backdrop="static" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content rounded-1">
<form id="serverSettingsForm" class="mb-0" novalidate>
<div class="modal-header">
<h5 class="modal-title ms-2">
<span class="resolve-to-server-name"></span> Settings
</h5>
</div>
<div class="modal-body p-4">
<input type="hidden" id="guildSettingsId" name="guildSettingsId">
<input type="hidden" id="guildSettingsGuildId" name="guildSettingsGuildId">
<div class="mb-4">
<div class="colour-input"
data-id="guildSettingsDefaultEmbedColour"
data-label="Default Embed Colour"
data-helptext="Default colour of each embed in Discord."
data-defaultcolour="#3498db">
</div>
</div>
<div>
<div class="form-switch ps-0">
<label for="guildSettingsActive" class="form-check-label mb-2">Server Active?</label>
<br>
<input type="checkbox" name="guildSettingsActive" id="guildSettingsActive" class="form-check-input ms-0 mt-0">
<br>
<div class="form-text">Is this server active?</div>
</div>
</div>
</div>
<div class="modal-footer px-4">
<button type="submit" class="btn btn-primary rounded-1 ms-3 me-0">Save Changes</button>
<button type="button" class="btn btn-secondary rounded-1 ms-3 me-0" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -49,7 +49,10 @@
<h5 class="mb-0 resolve-to-server-id text-truncate text-body-secondary"></h5>
</div>
<div class="ms-auto">
<button type="button" id="deleteSelectedServerBtn" class="btn btn-outline-danger rounded-1 ms-3" data-bs-toggle="tooltip" data-bs-title="Close this server">
<button type="button" id="serverSettingsBtn" class="btn btn-outline-secondary rounded-1 ms-3" data-bs-toggle="tooltip" data-bs-title="Server settings">
<i class="bi bi-gear"></i>
</button>
<button type="button" id="deleteSelectedServerBtn" class="btn btn-outline-danger rounded-1 ms-3" data-bs-toggle="tooltip" data-bs-title="Close server">
<i class="bi bi-x-lg"></i>
</button>
<button type="button" id="backToSelectServer" class="btn btn-outline-secondary rounded-1 ms-3" data-bs-toggle="tooltip" data-bs-title="Go back">
@ -82,9 +85,9 @@
<li class="nav-item" role="presentation">
<button id="contentTab" class="nav-link" data-bs-toggle="tab" data-bs-target="#contentTabPane" type="button" aria-controls="contentTabPane" aria-selected="false">Tracked Content</button>
</li>
<li class="nav-item ms-auto" role="presentation">
<!-- <li class="nav-item ms-auto" role="presentation">
<button id="settingsTab" class="nav-link" data-bs-toggle="tab" data-bs-target="#settingsTabPane" type="button" aria-controls="settingsTabPane" aria-selected="false">Settings</button>
</li>
</li> -->
</ul>
</div>
@ -93,7 +96,7 @@
<div id="subscriptionsTabPane" class="tab-pane fade" role="tabpanel" aria-labelledby="subscriptionsTab" tabindex="0"></div>
<div id="filtersTabPane" class="tab-pane fade includes-table includes-table-controls includes-table-search" role="tabpanel" aria-labelledby="filtersTab" tabindex="0"> </div>
<div id="contentTabPane" class="tab-pane fade" role="tabpanel" aria-labelledby="contentTab" tabindex="0"></div>
<div id="settingsTabPane" class="tab-pane fade" role="tabpanel" aria-labelledby="settingsTab" tabindex="0">{% include "home/includes/settingstab.html" %}</div>
<!-- <div id="settingsTabPane" class="tab-pane fade" role="tabpanel" aria-labelledby="settingsTab" tabindex="0">{% include "home/includes/settingstab.html" %}</div> -->
</div>
</div>
</div>
@ -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 %}

View File

@ -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