Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tag Edit UI & Plugin System #7766

Merged
merged 49 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
9242823
scaffold infogami type for tags
JaydenTeoh Apr 5, 2023
08edf65
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 5, 2023
b78d1a7
draft addtag plugin
JaydenTeoh Apr 9, 2023
cda8e3f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 9, 2023
6c9bef3
edit comments
JaydenTeoh Apr 9, 2023
363f3e1
added html form to add tags
JaydenTeoh Apr 12, 2023
7fa6881
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 12, 2023
2cb43e2
fix test errors
JaydenTeoh Apr 12, 2023
1a4fe2d
fixing form post request
JaydenTeoh Apr 13, 2023
3e70598
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 13, 2023
dc8d267
created a tag via html form
JaydenTeoh Apr 13, 2023
849ea8a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 13, 2023
34f7e8c
fix test error
JaydenTeoh Apr 13, 2023
5b91c86
add functionality to fetch Tag data to be used to enrich subject page
JaydenTeoh Apr 19, 2023
a7454ed
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 19, 2023
892a7a3
fix lint errors
JaydenTeoh Apr 19, 2023
f03e6c8
add plugins field and libarian permission to tags
JaydenTeoh May 22, 2023
ab6cde4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 22, 2023
8b2ff40
scaffold tags
JaydenTeoh May 23, 2023
d6bb972
fix edit page for Tags
JaydenTeoh May 23, 2023
20e734c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 23, 2023
6a598a1
implement tag main page
JaydenTeoh May 25, 2023
abd8a60
remove unnecessary js
JaydenTeoh May 25, 2023
3e0faec
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2023
74659a5
add add/edit tag button in subject page for librarians
JaydenTeoh May 25, 2023
710d362
move publishing history to bottom of subject page
JaydenTeoh May 25, 2023
4bf2739
simplify ternary
mekarpeles May 25, 2023
bf3c28f
improve add/edit tags UI
JaydenTeoh May 27, 2023
f17f748
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2023
94e7980
fix linter errors
JaydenTeoh May 27, 2023
709cdac
fix py linter errors
JaydenTeoh May 27, 2023
a923bac
add plugins input validation and minor styling
JaydenTeoh May 28, 2023
49d4440
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 28, 2023
bca8f59
fix js lint error
JaydenTeoh May 28, 2023
714f33a
add more validation for tags input forms and also styling for subject…
JaydenTeoh May 28, 2023
adc4b82
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 28, 2023
fd74065
fix incorrect Tag class function reference
JaydenTeoh Jun 18, 2023
f9c56e8
change subject page tag edit button
JaydenTeoh Jun 27, 2023
ff7a36d
change edit tag button on subject page
JaydenTeoh Jul 6, 2023
b1d0adc
rebased to master
JaydenTeoh Jul 13, 2023
b9a1c88
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 13, 2023
d420594
work on related subjects carousel
JaydenTeoh Jul 19, 2023
bb03c67
implement custom related subjects query carousel
JaydenTeoh Jul 23, 2023
8f9d095
remove unnecessary query string line
JaydenTeoh Jul 26, 2023
fefb14e
remove inline styles and move python functions in separate file
JaydenTeoh Aug 13, 2023
e0ef20d
Merge branch 'master' into feat/add-tags-type
mekarpeles Aug 16, 2023
ce608d7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 16, 2023
ab65801
Merge branch 'master' into feat/add-tags-type
mekarpeles Aug 23, 2023
ca63ba2
Merge branch 'tag-views' into feat/add-tags-type
jimchamp Oct 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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