embed colour, and filter table abstract functions

This commit is contained in:
Corban-Lee Jones 2024-06-26 15:58:04 +01:00
parent 5a3b458f3e
commit 8864ac9cc0
11 changed files with 296 additions and 114 deletions

View File

@ -147,7 +147,7 @@ class SubscriptionSerializer_GET(DynamicModelSerializer):
model = Subscription
fields = (
"id", "name", "url", "guild_id", "channels_count", "creation_datetime", "extra_notes",
"filters", "article_title_mutators", "article_desc_mutators", "active"
"filters", "article_title_mutators", "article_desc_mutators", "embed_colour", "active"
)
@ -160,7 +160,7 @@ class SubscriptionSerializer_POST(DynamicModelSerializer):
model = Subscription
fields = (
"id", "name", "url", "guild_id", "channels_count", "creation_datetime", "extra_notes",
"filters", "article_title_mutators", "article_desc_mutators", "active"
"filters", "article_title_mutators", "article_desc_mutators", "embed_colour", "active"
)

View File

@ -189,7 +189,7 @@ class Subscription_ListView(generics.ListCreateAPIView):
filter_backends = [filters.SearchFilter, rest_filters.DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = [
"id", "name", "url", "guild_id", "creation_datetime", "extra_notes", "filters",
"article_title_mutators", "article_desc_mutators", "active"
"article_title_mutators", "article_desc_mutators", "embed_colour", "active"
]
search_fields = ["name", "extra_notes"]
ordering_fields = ["creation_datetime", "guild_id"]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-06-26 13:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0008_subscription_article_fetch_limit_and_more'),
]
operations = [
migrations.AddField(
model_name='subscription',
name='embed_colour',
field=models.CharField(default='3498db', max_length=6),
),
]

View File

@ -224,6 +224,12 @@ class Subscription(models.Model):
reset_article_fetch_limit = models.BooleanField(default=False)
embed_colour = models.CharField(
max_length=6,
default="3498db",
blank=True
)
active = models.BooleanField(default=True)
class Meta:

View File

@ -30,6 +30,18 @@
transition: border-radius .15s ease-in;
}
/* widths */
.mw-10rem {
max-width: 10rem;
}
.col-switch-width {
width: 3.5rem;
min-width: 3.5rem;
max-width: 3.5rem;
}
/* tables */
.table {
@ -43,4 +55,15 @@
.table.dataTable > tbody > tr.selected a {
color: var(--bs-link-color) !important;
}
/* Fuck ugly <td> height fix */
td {
height: 1px;
}
@-moz-document url-prefix() {
.fix_height {
height: 100%;
}
}

View File

@ -24,13 +24,17 @@ function initContentTable() {
title: '<input type="checkbox" class="form-check-input table-select-all" />',
data: null,
orderable: false,
className: "text-center col-1",
className: "text-center col-switch-width",
render: function() {
return '<input type="checkbox" class="form-check-input table-select-row" />'
}
},
{ data: "id", visible: false },
{ title: "GUID", data: "guid", visible: false },
{
title: "GUID",
data: "guid",
className: "text-truncate mw-10rem",
},
{
title: "Name",
data: "title",
@ -60,9 +64,25 @@ function initContentTable() {
data: "creation_datetime",
className: "text-nowrap",
render: function(data, type) {
return `<small>${new Date(data).toISOString().replace('T', ' · ').replace(/\.\d+Z$/, '')}</small>`;
// return new Date(data).toISOString().split("T")[0];
let dateTime = new Date(data);
let dateTimeString = formatDate(dateTime);
return $(`
<small data-bs-trigger="hover focus"
data-bs-toggle="popover"
data-bs-content="${dateTimeString}">
${dateTime.toISOString().split("T")[0]}
</small>
`).popover()[0];
}
},
{
orderable: false,
className: "p-0",
render: function(data, type, row) {
return `<div class="h-100" style="background-color: #${row.subscription.embed_colour}; width: .25rem;">&nbsp;</div>`
}
}
]
});
}
@ -108,7 +128,10 @@ async function loadContent(guildId, page=1, pageSize=null) {
try {
const content = await getTrackedContent(guildId, null, page, pageSize);
contentTable.rows.add(content.results).draw(false);
handleContentPagination(page, pageSize, content.count, content.next, content.previous);
updateTablePagination("#contentPagination", page, pageSize, content.count, content.next, content.previous);
updateTablePaginationInfo("#contentTablePageInfo", content.results.length, content.count);
$("#contentTable thead .table-select-all").prop("disabled", content.results.length === 0);
}
catch (err) {
@ -121,60 +144,3 @@ $(document).on("selectedServerChange", async function() {
const activeServer = getCurrentlyActiveServer();
await loadContent(activeServer.guild_id);
});
$("#contentTablePageSize").on("change", async function() {
const page = 1; // reset to page 1 to ensure the page exists.
const pageSize = $(this).val();
loadContent(getCurrentlyActiveServer().guild_id, page, pageSize);
});
function handleContentPagination(currentPage, pageSize, totalItems, nextExists, prevExists) {
$("#contentPagination").attr("data-page", currentPage);
// Remove existing page-specific buttons
$("#contentPagination .page-pick").remove();
// Determine states of 'previous page' and 'next page' buttons
$("#contentPagination .page-prev").toggleClass("disabled", !prevExists).attr("tabindex", prevExists ? "" : "-1");
$("#contentPagination .page-next").toggleClass("disabled", !nextExists).attr("tabindex", nextExists ? "" : "-1");
// Calculate amount of pages to account for
const pages = Math.max(Math.ceil(totalItems / pageSize), 1);
// Create a button for each page
for (let i = 1; i < pages + 1; i++) {
let pageItem = $("<li>").addClass("page-item");
let pageLink = $("<button>")
.attr("type", "button")
.attr("data-page", i)
.addClass("page-link page-pick")
.text(i)
pageItem.append(pageLink);
// Insert the new page button before the 'next page' button
$("#contentPagination .pagination .page-next").parent().before(pageItem);
}
// Disable the button for the current page
$(`#contentPagination .pagination .page-pick[data-page="${currentPage}"]`).addClass("disabled").attr("tabindex", -1);
}
$("#contentPagination .pagination").on("click", ".page-link", async function() {
let wantedPage;
let currentPage = parseInt($("#contentPagination").attr("data-page"));
if ($(this).hasClass("page-prev"))
wantedPage = currentPage - 1
else if ($(this).hasClass("page-next"))
wantedPage = currentPage + 1;
else
wantedPage = $(this).attr("data-page");
console.debug("moving to page: " + wantedPage);
await loadContent(getCurrentlyActiveServer().guild_id, wantedPage)
});

View File

@ -25,7 +25,7 @@ function initFiltersTable() {
title: '<input type="checkbox" class="form-check-input table-select-all" />',
data: null,
orderable: false,
className: "text-center col-1",
className: "text-center col-switch-width",
render: function() {
return '<input type="checkbox" class="form-check-input table-select-row" />'
}
@ -39,23 +39,22 @@ function initFiltersTable() {
}
},
{
title: "Keywords",
data: "keywords",
render: function(data, type) {
if (!data) return "-";
return data;
}
},
{
title: "Regex",
data: "regex",
render: function(data, type) {
if (!data) return "-";
return data;
title: "Match",
// data: "keywords",
className: "text-start text-truncate",
render: function(data, type, row) {
return row.regex ? row.regex : row.keywords;
}
},
{
title: "Type",
// data: "regex",
render: function(data, type, row) {
return row.regex ? "Regex" : "Keywords";
}
},
{
title: "Control List",
data: "whitelist",
render: function(data) {
return data ? "whitelist" : "blacklist";
@ -180,20 +179,28 @@ $("#refreshFilterBtn").on("click", async function() {
loadFilters(getCurrentlyActiveServer().guild_id);
});
async function loadFilters(guildId) {
async function loadFilters(guildId, page=1, pageSize=null) {
if (!guildId)
return;
if (!pageSize)
pageSize = $("#filtersTablePageSize").val();
$("#deleteSelectedFiltersBtn").prop("disabled", true);
clearExistingFilterRows();
try {
const filters = await getFilters(guildId);
filtersTable.rows.add(filters.results).draw(false);
updateTablePagination("#filtersPagination", page, pageSize, filters.count, filters.next, filters.previous);
updateTablePaginationInfo("#filtersTablePageInfo", filters.results.length, filters.count);
$("#filtersTable thead .table-select-all").prop("disabled", filters.results.length === 0);
}
catch (err) {
console.error(err)
console.error(JSON.stringify(err, null, 4));
showToast("danger", `Error Loading Filters: HTTP ${err.status}`, err.responseJSON.message, 15000);
}

View File

@ -14,6 +14,15 @@ $(document).ready(async function() {
await bindTablePagination("#subPagination", loadSubscriptions);
await bindTablePaginationResizer("#subTablePageSize", loadSubscriptions);
// Bind pagination filtersTable
await bindTablePagination("#filtersPagination", loadFilters);
await bindTablePaginationResizer("#filtersTablePageSize", loadFilters);
// Bind pagination - contentTable
await bindTablePagination("#contentPagination", loadContent);
await bindTablePaginationResizer("#contentTablePageSize", loadContent);
await loadSavedGuilds();
await loadServerOptions();
});
@ -72,3 +81,47 @@ function determineSelectAllState(tableSelector, tableObject, deleteButtonSelecto
$(deleteButtonSelector).prop("disabled", !(checked || indeterminate) || !(allRowsCount > 0));
}
function formatDate(date) {
// Array of weekday names
const weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
// Array of month names
const months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
// Get individual components
let hours = String(date.getHours()).padStart(2, '0');
let minutes = String(date.getMinutes()).padStart(2, '0');
let seconds = String(date.getSeconds()).padStart(2, '0');
let dayOfWeek = weekdays[date.getDay()];
let dayOfMonth = date.getDate();
let month = months[date.getMonth()];
let year = date.getFullYear();
// Format day with ordinal suffix
let dayOfMonthSuffix;
if (dayOfMonth % 10 === 1 && dayOfMonth !== 11) {
dayOfMonthSuffix = dayOfMonth + "st";
} else if (dayOfMonth % 10 === 2 && dayOfMonth !== 12) {
dayOfMonthSuffix = dayOfMonth + "nd";
} else if (dayOfMonth % 10 === 3 && dayOfMonth !== 13) {
dayOfMonthSuffix = dayOfMonth + "rd";
} else {
dayOfMonthSuffix = dayOfMonth + "th";
}
// `${hours}:${minutes}:${seconds} · `
return `${dayOfWeek}, ${dayOfMonthSuffix} ${month}, ${year}`;
}
function genHexString(len) {
let output = '';
for (let i = 0; i < len; ++i) {
output += (Math.floor(Math.random() * 16)).toString(16);
}
return output;
}

View File

@ -17,7 +17,7 @@ function initSubscriptionTable() {
{
targets: 0,
checkboxes: { selectRow: true }
}
},
],
columns: [
{
@ -25,7 +25,7 @@ function initSubscriptionTable() {
title: '<input type="checkbox" class="form-check-input table-select-all" />',
data: null,
orderable: false,
className: "text-center",
className: "text-center col-switch-width",
render: function() {
return '<input type="checkbox" class="form-check-input table-select-row" />'
}
@ -51,7 +51,7 @@ function initSubscriptionTable() {
title: "Channels",
data: "channels_count",
render: function(data) {
return `<span class="badge badge-secondary">${data}</span>`;
return `<span class="badge text-bg-secondary">${data}</span>`;
}
},
// {
@ -64,9 +64,17 @@ function initSubscriptionTable() {
{
title: "Created",
data: "creation_datetime",
className: "small",
render: function(data, type) {
return new Date(data).toISOString().split("T")[0];
// return new Date(data).toISOString().split("T")[0];
let dateTime = new Date(data);
let dateTimeString = formatDate(dateTime);
return $(`
<small data-bs-trigger="hover focus"
data-bs-toggle="popover"
data-bs-content="${dateTimeString}">
${dateTime.toISOString().split("T")[0]}
</small>
`).popover()[0];
}
},
{
@ -77,7 +85,13 @@ function initSubscriptionTable() {
render: function(data, type) {
if (!data) return "";
return $(`
<i class="bi bi-chat-left-text" data-bs-trigger="hover focus" data-bs-toggle="popover" data-bs-title="Extra Notes" data-bs-content="${data}"></i>`).popover()[0];
<i class="bi bi-chat-left-text"
data-bs-trigger="hover focus"
data-bs-toggle="popover"
data-bs-title="Extra Notes"
data-bs-content="${data}">
</i>
`).popover()[0];
}
},
{
@ -88,6 +102,13 @@ function initSubscriptionTable() {
render: function(data, type) {
return `<input type="checkbox" class="sub-toggle-active form-check-input ms-0" ${data ? "checked" : ""} />`
}
},
{
orderable: false,
className: "p-0",
render: function(data, type, row) {
return `<div class="h-100" style="background-color: #${row.embed_colour}; width: .25rem;">&nbsp;</div>`
}
}
]
});
@ -124,6 +145,7 @@ $("#subTable").on("change", ".sub-toggle-active", async function () {
title: sub.article_title_mutators.map(mutator => mutator.id),
desc: sub.article_desc_mutators.map(mutator => mutator.id)
},
sub.embed_colour,
active,
handleErrorMsg=false
);
@ -167,8 +189,10 @@ async function showEditSubModal(subId) {
$("#subTitleMutators").val("").change();
$("#subDescMutators").val("").change();
$("#subActive").prop("checked", true);
$("#subImagePreview img").attr("src", "").hide();
$("#subImagePreview small").show();
// $("#subImagePreview img").attr("src", "").hide();
// $("#subImagePreview small").show();
$("#subResetEmbedColour").click();
}
else {
$("#subFormModal .form-create, #subAdvancedModal .form-create").hide();
@ -195,6 +219,8 @@ async function showEditSubModal(subId) {
$("#subFilters").val("").change();
$("#subFilters").val(subscription.filters).change();
subSetHexColour(`#${subscription.embed_colour}`);
}
$("#subId").val(subId);
@ -215,9 +241,10 @@ $("#subForm").on("submit", async function(event) {
title: $("#subTitleMutators option:selected").toArray().map(mutator => parseInt(mutator.value)),
desc: $("#subDescMutators option:selected").toArray().map(mutator => parseInt(mutator.value))
}
subEmbedColour = $("#subEmbedColour").val().split("#")[1];
active = $("#subActive").prop("checked");
var subPrimaryKey = await saveSubscription(id, name, url, guildId, extraNotes, subFilters, subMutators, active);
var subPrimaryKey = await saveSubscription(id, name, url, guildId, extraNotes, subFilters, subMutators, subEmbedColour, active);
if (!subPrimaryKey) {
alert("prevented /subscriptions/false/subchannels");
@ -236,7 +263,7 @@ $("#subForm").on("submit", async function(event) {
$("#subFormModal").modal("hide");
});
async function saveSubscription(id, name, url, guildId, extraNotes, filters, mutators, active, handleErrorMsg=true) {
async function saveSubscription(id, name, url, guildId, extraNotes, filters, mutators, embedColour, active, handleErrorMsg=true) {
var formData = new FormData();
formData.append("name", name);
formData.append("url", url);
@ -245,6 +272,7 @@ async function saveSubscription(id, name, url, guildId, extraNotes, filters, mut
filters.forEach(filter => formData.append("filters", filter));
mutators.title.forEach(mutator => formData.append("article_title_mutators", mutator));
mutators.desc.forEach(mutator => formData.append("article_desc_mutators", mutator));
formData.append("embed_colour", embedColour);
formData.append("active", active);
var response;
@ -315,11 +343,14 @@ async function loadSubscriptions(guildId, page=1, pageSize=null) {
console.debug("loading subs, " + subscriptions.results.length + " found");
}
catch (err) {
console.error(err)
console.error(JSON.stringify(err, null, 4));
showToast("danger", `Error Loading Subscriptions: HTTP ${err.status}`, err.responseJSON.message, 15000);
}
}
// #region Server Change Event Handler
$(document).on("selectedServerChange", async function() {
// Hide alerts
@ -332,6 +363,9 @@ $(document).on("selectedServerChange", async function() {
await loadMutatorOptions();
})
// #endregion
// Dev button (to be removed)
// auto fills dummy data into form fields to quickly create test subs
$("#devGenerateSub").on("click", function() {
@ -375,6 +409,9 @@ $("#devGenerateSub").on("click", function() {
showToast("info", "Generated", "Picked random sub data");
});
// #region Delete Subscription Buttons
// Delete button on the 'edit subscription' modal
$("#deleteEditSub").on("click", async function() {
const subId = $("#subId").val();
@ -397,6 +434,12 @@ $("#deleteSelectedSubscriptionsBtn").on("click", async function() {
await loadSubscriptions(getCurrentlyActiveServer().guild_id);
})
// #endregion
// #region Load Modal Options
async function loadChannelOptions(guildId) {
// Disable input while options are loading
@ -531,6 +574,10 @@ async function loadFilterOptions(guildId) {
}
// #endregion
// #region Bot Not in Server Alert
function showServerJoinAlert() {
const guildId = getCurrentlyActiveServer().guild_id;
const inviteUrl = `https://discord.com/oauth2/authorize
@ -542,11 +589,31 @@ function showServerJoinAlert() {
$("#serverJoinAlert a.alert-link").attr("href", inviteUrl);
$("#serverJoinAlert").show();
}
// #endregion
$("#subImage").on("change", function () {
const [file] = $("#subImage")[0].files;
if (file) {
$("#subImagePreview small").hide();
$("#subImagePreview img").attr("src", URL.createObjectURL(file)).show();
}
});
// #region Colour Controls
$("#subEmbedColour").on("change", function() {
$("#subEmbedColourText").val($(this).val());
});
$("#subEmbedColourText").on("change", function() {
$("#subEmbedColour").val($(this).val());
});
function subSetHexColour(hexString) {
$("#subEmbedColour").val(hexString);
$("#subEmbedColourText").val(hexString);
}
$(document).ready(() => {$("#subResetEmbedColour").click();});
$("#subResetEmbedColour").on("click", function() {
subSetHexColour("#3498db");
});
$("#subRandomEmbedColour").on("click", function() {
subSetHexColour(`#${genHexString(6)}`);
});
// #endregion

View File

@ -39,11 +39,6 @@
<select name="subFilters" id="subFilters" class="select-2" multiple data-dropdownparent="#subFormModal" tabindex="4"></select>
<div class="form-text">Filters to apply to this subscription's content.</div>
</div>
<!-- <div>
<label for="subMutators" class="form-label">Article Mutators</label>
<select name="subMutators" id="subMutators" class="select-2" multiple data-dropdownparent="#subFormModal" tabindex="6"></select>
<div class="form-text">Apply mutators to subscription articles.</div>
</div> -->
<div class="form-switch mb-4 ps-0">
<label for="subActive" class="form-check-label mb-2">Active</label>
<br>
@ -97,19 +92,37 @@
</div>
</div>
<div class="col-lg-6 pe-lg-4 d-none">
<label for="" class="form-label">Article Fetch Limit</label>
<input type="number" id="subFetchLimit" class="form-control rounded-1" max="10" min="1">
<div class="form-text">Limit the number of articles fetched every cycle.</div>
<div class="mb-4">
<label for="" class="form-label">Article Fetch Limit</label>
<input type="number" id="subFetchLimit" class="form-control rounded-1" max="10" min="1">
<div class="form-text">Limit the number of articles fetched every cycle.</div>
</div>
</div>
<div class="col-lg-6 ps-lg-4 d-none">
<div class="form-switch ps-0">
<div class="form-switch ps-0 mb-4">
<label for="subResetFetchLimit" class="form-check-label mb-2">Max Fetch Limit after the First Cycle</label>
<br>
<input type="checkbox" id="subResetFetchLimit" name="subResetFetchLimit" class="form-check-input ms-0 mt-0" tabindex="6">
<br>
<div class="form-text">Sets the Fetch Limit to 10 after the first cycle. Helps with initial spam.</div>
</div>
</div>
</div>
<div class="col-lg-6 pe-lg-4">
<div>
<label for="subEmbedColour" class="form-label">Embed Colour</label>
<div class="input-group">
<input type="color" name="subEmbedColour" id="subEmbedColour" class="form-control-color input-group-text">
<input type="text" name="subEmbedColourText" id="subEmbedColourText" class="form-control">
<button type="button" id="subResetEmbedColour" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-title="Reset Colour">
<i class="bi bi-arrow-clockwise"></i>
</button>
<button type="button" id="subRandomEmbedColour" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-title="Random Colour">
<i class="bi bi-dice-5"></i>
</button>
</div>
<div class="form-text">Colour of each article's embed in Discord.</div>
</div>
</div>
</div>
</div>
<div class="modal-footer px-4">

View File

@ -127,22 +127,47 @@
<select name="subTablePageSize" id="subTablePageSize" class="select-2">
<option value="10" selected>10&emsp;</option>
<option value="25">25&emsp;</option>
<option value="50">50&emsp;</option>
<option value="100">100&emsp;</option>
<!-- <option value="50">50&emsp;</option>
<option value="100">100&emsp;</option> -->
</select>
</div>
</div>
</div>
<div id="filtersTabPane" class="tab-pane fade" role="tabpanel" aria-labelledby="filtersTab" tabindex="0">
<div class="table-responsive mt-3">
<div class="table-responsive my-3 px-3">
<table id="filtersTable" class="table table-hover align-middle"></table>
</div>
<div class="table-controls d-flex mb-3 px-3">
<nav id="filtersPagination">
<ul class="pagination mb-0">
<li class="page-item">
<button type="button" class="page-link page-prev rounded-start-1">Previous</button>
</li>
<li class="page-item">
<button type="button" class="page-link page-next rounded-end-1">Next</button>
</li>
</ul>
</nav>
<div id="filtersTablePageInfo" class="d-flex align-items-center mx-auto">
showing&nbsp;<span class="pageinfo-showing"></span>
&nbsp;of&nbsp;<span class="pageinfo-total"></span>
</div>
<div class="d-flex">
<label for="filtersTablePageSize" class="form-label align-self-center mb-0 me-2">Per Page</label>
<select name="filtersTablePageSize" id="filtersTablePageSize" class="select-2">
<option value="10" selected>10&emsp;</option>
<option value="25">25&emsp;</option>
<!-- <option value="50">50&emsp;</option>
<option value="100">100&emsp;</option> -->
</select>
</div>
</div>
</div>
<div id="contentTabPane" class="tab-pane fade" role="tabpanel" aria-labelledby="contentTab" tabindex="0">
<div class="table-responsive my-3">
<div class="table-responsive my-3 px-3">
<table id="contentTable" class="table table-hover align-middle"></table>
</div>
<div class="table-controls d-flex mb-3">
<div class="table-controls d-flex mb-3 px-3">
<nav id="contentPagination">
<ul class="pagination mb-0">
<li class="page-item">
@ -153,13 +178,17 @@
</li>
</ul>
</nav>
<div class="d-flex ms-auto">
<div id="contentTablePageInfo" class="d-flex align-items-center mx-auto">
showing&nbsp;<span class="pageinfo-showing"></span>
&nbsp;of&nbsp;<span class="pageinfo-total"></span>
</div>
<div class="d-flex">
<label for="contentTablePageSize" class="form-label align-self-center mb-0 me-2">Per Page</label>
<select name="contentTablePageSize" id="contentTablePageSize" class="select-2">
<option value="10" selected>10&emsp;</option>
<option value="25">25&emsp;</option>
<option value="50">50&emsp;</option>
<option value="100">100&emsp;</option>
<!-- <option value="50">50&emsp;</option>
<option value="100">100&emsp;</option> -->
</select>
</div>
</div>