Skip to content

Commit

Permalink
Merge pull request #7766 from JaydenTeoh/feat/add-tags-type
Browse files Browse the repository at this point in the history
Tag Edit UI & Plugin System
  • Loading branch information
jimchamp authored Oct 20, 2023
2 parents 01f6c6d + ca63ba2 commit 8109233
Show file tree
Hide file tree
Showing 21 changed files with 524 additions and 27 deletions.
2 changes: 1 addition & 1 deletion openlibrary/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ def create(
'key': key,
'name': tag_name,
'tag_description': tag_description,
'tag_type': tag_type,
'tag_type': tag_type or [],
'tag_plugins': json.loads(tag_plugins or "[]"),
'type': {"key": '/type/tag'},
},
Expand Down
25 changes: 25 additions & 0 deletions openlibrary/core/tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
For processing Tags
"""

import json

from infogami.utils.view import public


@public
def process_plugins_data(data):
plugin_type = list(data.keys())[0]
# Split the string into key-value pairs
parameters = data[plugin_type].split(',')

# Create a dictionary to store the formatted parameters
plugin_data = {}

# Iterate through the pairs and extract the key-value information
for pair in parameters:
key, value = pair.split('=')
key = key.strip()
plugin_data[key] = eval(value)

return plugin_type, plugin_data
5 changes: 5 additions & 0 deletions openlibrary/macros/Plugins.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
$def with (plugins)

$for plugin in plugins:
$ plugin_type, plugin_data = process_plugins_data(plugin)
$:macros[plugin_type](**plugin_data)
18 changes: 18 additions & 0 deletions openlibrary/macros/RelatedSubjects.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
$def with(subjects)

$# Takes following parameters
$# * subjects (str) -- A string containing a comma-separated list of subjects

$ subject_filters = subjects.split('+')

<body id="related-subjects">
<div id="related-subject-filters">
<label><input type="checkbox" class="select-all"> Select All</label>
$for filter in subject_filters:
<label><input type="checkbox" class="subject-filter"> $filter</label>
</div>
<div id="related-subjects-carousel">
$ query_string = get_related_subjects_query()
$:macros.QueryCarousel(query=query_string, title=_("You might also like"), key="related-subjects-carousel")
</div>
</body>
3 changes: 2 additions & 1 deletion openlibrary/plugins/openlibrary/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@
h for h in infogami._install_hooks if h.__name__ != 'movefiles'
]

from openlibrary.plugins.openlibrary import lists, bulk_tag
from openlibrary.plugins.openlibrary import tags, lists, bulk_tag

lists.setup()
tags.setup()
bulk_tag.setup()

logger = logging.getLogger('openlibrary')
Expand Down
23 changes: 23 additions & 0 deletions openlibrary/plugins/openlibrary/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,29 @@ jQuery(function () {
.then(module => module.initAddBookImport());
}

if (document.getElementById('addtag')) {
import(/* webpackChunkName: "plugins_form" */ './plugins_form.js')
.then(module => {
module.initPluginsForm();
module.initAddTagForm();
});
}

if (document.getElementById('edittag')) {
import(/* webpackChunkName: "plugins_form" */ './plugins_form.js')
.then(module => {
module.initPluginsForm();
module.initEditTagForm();
});
}

if (document.getElementById('related-subjects')) {
import(/* webpackChunkName: "plugins_form" */ './related_subjects.js')
.then(module => {
module.initRelatedSubjectsCarousel();
});
}

if (document.getElementById('autofill-dev-credentials')) {
document.getElementById('username').value = 'openlibrary@example.com'
document.getElementById('password').value = 'admin123'
Expand Down
177 changes: 177 additions & 0 deletions openlibrary/plugins/openlibrary/js/plugins_form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* Functionality for Tags form
*/
import 'jquery-ui/ui/widgets/sortable';

const pluginsTypesList = ['RelatedSubjects', 'QueryCarousel', 'ListCarousel']

function checkRequiredFields() {
const nameInput = document.getElementById('tag_name');
const descriptionInput = document.getElementById('tag_description');
const tagType = document.getElementById('tag_type');
if (!nameInput.value) {
nameInput.focus({focusVisible: true});
throw new Error('Name is required');
}
if (!descriptionInput.value) {
descriptionInput.focus({focusVisible: true});
throw new Error('Description is required');
}
if (!tagType.value) {
tagType.focus({focusVisible: true});
throw new Error('Tag type is required');
}
}

export function initPluginsForm() {
document.querySelector('.addPluginBtn').addEventListener('click', function() {
const newRow = `
<tr class="plugins-input-row">
<td>
<select id="plugins_type" class="select-tag-plugins-container">
<option value="">Select Plugin</option>
${pluginsTypesList.map(pluginType => `<option value="${pluginType}">${pluginType}</option>`).join('')}
</select>
</td>
<td><textarea id="plugin_data_input"></textarea></td>
<td><span class="delete-plugin-btn">[X]</span></td>
<td><span class="drag-handle">☰</span></td>
</tr>`
document
.getElementById('pluginsFormRows')
.insertAdjacentHTML('beforeEnd', newRow);
initDeletePluginbtns(); // Reinitialize the delete-row-buttons' onclick listener
});

// Make the table rows draggable
$('#pluginsFormRows').sortable({
handle: '.drag-handle'
});

initDeletePluginbtns();
}

// Handle plugin deletion
function initDeletePluginbtns() {
document.querySelectorAll('.delete-plugin-btn').forEach(function(row) {
row.addEventListener('click', function() {
row.closest('.plugins-input-row').remove();
});
});
}

export function initAddTagForm() {
document
.getElementById('addtag')
.addEventListener('submit', function(e) {
e.preventDefault();
clearPluginsAndInputErrors();
try {
checkRequiredFields();
} catch (e) {
return;
}
let pluginsData = [];
try {
pluginsData = getPluginsData();
} catch (e) {
return;
}
const pluginsInput = document.getElementById('tag_plugins');
pluginsInput.value = JSON.stringify(pluginsData);
// Submit the form
this.submit();
});
}

export function initEditTagForm() {
document
.getElementById('edittag')
.addEventListener('submit', function(e) {
e.preventDefault();
clearPluginsAndInputErrors();
try {
checkRequiredFields();
} catch (e) {
return;
}
let pluginsData = [];
try {
pluginsData = getPluginsData();
} catch (e) {
return;
}
const pluginsInput = document.getElementById('tag_plugins');
pluginsInput.value = JSON.stringify(pluginsData);
// Submit the form
this.submit();
});
}

function getPluginsData() {
const formData = [];
document.querySelectorAll('.plugins-input-row').forEach(function(row) {
const pluginsType = row.querySelector('#plugins_type').value;
const dataInput = row.querySelector('#plugin_data_input').value;

const newPlugin = {}
newPlugin[pluginsType] = dataInput
const error = parseAndValidatePluginsData(newPlugin);
if (error) {
const errorDiv = document.getElementById('plugin_errors');
errorDiv.classList.remove('hidden');
errorDiv.textContent = error;
row.classList.add('invalid-tag-plugins-error');
throw new Error(error);
}

formData.push(newPlugin);
});

return formData;
}

function clearPluginsAndInputErrors() {
const nameInput = document.getElementById('tag_name');
const descriptionInput = document.getElementById('tag_description');
const tagType = document.getElementById('tag_type');
nameInput.focus({focusVisible: false});
descriptionInput.focus({focusVisible: false});
tagType.focus({focusVisible: false});
const errorDiv = document.getElementById('plugin_errors');
errorDiv.classList.add('hidden');
document.querySelectorAll('.plugins-input-row').forEach(function(row) {
row.classList.remove('invalid-tag-plugins-error');
});
}

function parseAndValidatePluginsData(plugin) {
const validInputRegex = /^[\w\s]+=(?:'[^']*'|"[^"]*"|\w+)$/;
const pluginType = Object.keys(plugin)[0];
const pluginData = plugin[pluginType];
if (!pluginType) {
return 'Plugin type is required';
}
if (!pluginsTypesList.includes(pluginType)) {
return `Invalid plugin type: ${pluginType}`;
}
if (!pluginData) {
return 'Plugin parameters are required';
}
const keyValuePairs = pluginData.split(', ');
for (const pair of keyValuePairs) {
if (!pair.includes('=')) {
return 'Missing equal sign: Each parameter should be in the form of \'key=value\'';
}
const splitResults = pair.split('=');
if (splitResults.length !== 2) {
return 'Too many equal signs: Each parameter should be in the form of \'key=value\'';
}
const value = splitResults[1];

if (!validInputRegex.test(pair)) {
return `Invalid parameters: ${value}`;
}
}
return null;
}
29 changes: 29 additions & 0 deletions openlibrary/plugins/openlibrary/js/related_subjects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* global render_subjects_carousel */

import { render } from 'less';

export function initRelatedSubjectsCarousel() {
const subjectCheckboxes = document.querySelectorAll('.subject-filter');
subjectCheckboxes.forEach((checkbox) => {
checkbox.addEventListener('change', renderSubjectsCarousel);
})
}

function generateQuery() {
const selectedSubjects = [];
const checkboxes = document.querySelectorAll('.subject-filter:checked');
checkboxes.forEach((checkbox) => {
const subject = checkbox.parentNode.textContent.trim();
selectedSubjects.push(subject);
});
const generatedString = selectedSubjects.join('&');
return generatedString;
}

function renderSubjectsCarousel() {
const queryString = generateQuery();
const url = new URL(window.location.href);
url.searchParams.set('subjects', queryString);
window.history.replaceState(null, null, url);
$('#related-subjects-carousel').load(`${window.location.href} #related-subjects-carousel`)
}
45 changes: 45 additions & 0 deletions openlibrary/plugins/openlibrary/tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from infogami.utils.view import public
import web
import json


@public
def load_plugin_json(plugins_str):
return json.loads(plugins_str)


@public
def display_plugins_data(data):
plugin_type = list(data.keys())[0]
# Split the string into key-value pairs
parameters = data[plugin_type].split(',')

# Create a dictionary to store the formatted parameters
plugin_fields = []

# Iterate through the pairs and extract the key-value information
for pair in parameters:
try:
key, value = pair.split('=', 1)
key = key.strip()
plugin_fields.append(f'{key}={value}')
except ValueError:
plugin_fields.append(pair)

plugin_data = ', '.join(plugin_fields)

return plugin_type, plugin_data


@public
def get_tag_types():
return ["subject", "work", "collection"]


@public
def get_plugin_types():
return ["RelatedSubjects", "QueryCarousel", "ListCarousel"]


def setup():
pass
9 changes: 7 additions & 2 deletions openlibrary/plugins/upstream/addbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import logging

from openlibrary.plugins.upstream import spamcheck, utils
from openlibrary.plugins.upstream.models import Author, Edition, Work
from openlibrary.plugins.upstream.models import Author, Edition, Work, Tag
from openlibrary.plugins.upstream.utils import render_template, fuzzy_find

from openlibrary.plugins.upstream.account import as_admin
Expand Down Expand Up @@ -110,7 +110,12 @@ def new_doc(type_: Literal["/type/work"], **data) -> Work:
...


def new_doc(type_: str, **data) -> Author | Edition | Work:
@overload
def new_doc(type_: Literal["/type/tag"], **data) -> Tag:
...


def new_doc(type_: str, **data) -> Author | Edition | Work | Tag:
"""
Create an new OL doc item.
:param str type_: object type e.g. /type/edition
Expand Down
1 change: 1 addition & 0 deletions openlibrary/plugins/upstream/addtag.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def has_permission(self) -> bool:
"""
Can a tag be added?
"""

user = web.ctx.site.get_user()
return user and (user.is_usergroup_member('/usergroup/super-librarians'))

Expand Down
Loading

0 comments on commit 8109233

Please sign in to comment.