Skip to content
39 changes: 39 additions & 0 deletions openlibrary/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime, timedelta
import logging
import web
import json
import requests
from typing import Any
from collections import defaultdict
Expand Down Expand Up @@ -1116,6 +1117,42 @@ def get_default_cover(self):
return Image(web.ctx.site, "b", cover_id)


# TODO: expand on tag model
class Tag(Thing):
"""Class to represent /type/tag objects in OL."""

@classmethod
def get_tag(cls, tag_name, tag_type):
"""Returns a Tag object for a given tag name and tag type."""
q = {'type': '/type/tag', 'name': tag_name, 'tag_type': tag_type}
match = list(web.ctx.site.things(q))
return match[0] if match else None

@classmethod
def create_tag(cls, tag_name, tag_description, tag_type, tag_plugins):
"""Creates a new Tag object."""
key = web.ctx.site.new_key('/type/tag')
web.ctx.path = key
web.ctx.site.save(
{
'key': key,
'name': tag_name,
'tag_description': tag_description,
'tag_type': tag_type,
'tag_plugins': json.loads(tag_plugins or "[]"),
'type': dict(key='/type/tag'),
},
comment='New Tag',
)
return key

def url(self, suffix="", **params):
return self.get_url(suffix, **params)

def get_url_suffix(self):
return self.name or "unnamed"


@dataclass
class LoggedBooksData:
"""
Expand Down Expand Up @@ -1164,6 +1201,7 @@ def register_models():
client.register_thing_class('/type/user', User)
client.register_thing_class('/type/list', List)
client.register_thing_class('/type/usergroup', UserGroup)
client.register_thing_class('/type/tag', Tag)


def register_types():
Expand All @@ -1174,6 +1212,7 @@ def register_types():
types.register_type('^/books/[^/]*$', '/type/edition')
types.register_type('^/works/[^/]*$', '/type/work')
types.register_type('^/languages/[^/]*$', '/type/language')
types.register_type('^/tags/[^/]*$', '/type/tag')

types.register_type('^/usergroup/[^/]*$', '/type/usergroup')
types.register_type('^/permission/[^/]*$', '/type/permission')
Expand Down
2 changes: 2 additions & 0 deletions openlibrary/core/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ def get_schema():
schema.add_table_group('work', '/type/work', datatypes)
schema.add_table_group('publisher', '/type/publisher', datatypes)
schema.add_table_group('subject', '/type/subject', datatypes)
schema.add_table_group('tag', '/type/tag', datatypes)

schema.add_seq('/type/edition', '/books/OL%dM')
schema.add_seq('/type/author', '/authors/OL%dA')

schema.add_seq('/type/work', '/works/OL%dW')
schema.add_seq('/type/publisher', '/publishers/OL%dP')
schema.add_seq('/type/tag', '/tags/OL%dT')

_sql = schema.sql

Expand Down
60 changes: 60 additions & 0 deletions openlibrary/plugins/openlibrary/types/tag.type
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"name": "Tag",
"key": "/type/tag",
"kind": "regular",
"created": {
"type": "/type/datetime",
"value": "2023-04-05T22:27:36.162339"
},
"last_modified": {
"type": "/type/datetime",
"value": "2023-04-05T22:37:06.504291"
},
"latest_revision": 3,
"type": {
"key": "/type/type"
},
"properties": [
{
"expected_type": {
"key": "/type/string",
},
"name": "name",
"type": {
"key": "/type/property"
},
"unique": true
},
{
"expected_type": {
"key": "/type/string",
},
"name": "tag_description",
"type": {
"key": "/type/property"
},
"unique": true
},
{
"expected_type": {
"key": "/type/string",
},
"name": "tag_plugins",
"type": {
"key": "/type/property"
},
"unique": true
},
{
"expected_type": {
"key": "/type/string",
},
"name": "tag_type",
"type": {
"key": "/type/property"
},
"unique": true
},
],
"revision": 3
}
148 changes: 148 additions & 0 deletions openlibrary/plugins/upstream/addtag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Handlers for adding and editing tags."""

import web
import json

from typing import NoReturn

from infogami.core.db import ValidationException
from infogami.infobase import common
from infogami.utils.view import add_flash_message, public
from infogami.infobase.client import ClientException
from infogami.utils import delegate

from openlibrary.plugins.openlibrary.processors import urlsafe
from openlibrary.i18n import gettext as _
import logging

from openlibrary.plugins.upstream import spamcheck, utils
from openlibrary.plugins.upstream.models import Tag
from openlibrary.plugins.upstream.addbook import get_recaptcha, safe_seeother, trim_doc
from openlibrary.plugins.upstream.utils import render_template

logger = logging.getLogger("openlibrary.tag")


class addtag(delegate.page):
path = '/tag/add'

def GET(self):
"""Main user interface for adding a tag to Open Library."""

if not self.has_permission():
raise common.PermissionDenied(message='Permission denied to add tags')

return render_template('tag/add', recaptcha=get_recaptcha())

def has_permission(self) -> bool:
"""
Can a tag be added?
"""
return web.ctx.user and (
web.ctx.user.is_usergroup_member('/usergroup/super-librarians')
)

def POST(self):
i = web.input(
tag_name="",
tag_type="",
tag_description="",
tag_plugins="",
)

if spamcheck.is_spam(i, allow_privileged_edits=True):
return render_template(
"message.html", "Oops", 'Something went wrong. Please try again later.'
)

if not web.ctx.site.get_user():
recap = get_recaptcha()
if recap and not recap.validate():
return render_template(
'message.html',
'Recaptcha solution was incorrect',
'Please <a href="javascript:history.back()">go back</a> and try again.',
)

i = utils.unflatten(i)
match = self.find_match(i) # returns None or Tag (if match found)

return self.tag_match(match) if match else self.no_match(i)

def find_match(self, i: web.utils.Storage):
"""
Tries to find an existing tag that matches the data provided by the user.
"""

return Tag.get_tag(i.tag_name, i.tag_type)

def tag_match(self, match: list) -> NoReturn:
"""
Action for when an existing tag has been found.
Redirect user to the found tag's edit page to add any missing details.
"""
tag = web.ctx.site.get(match)
raise safe_seeother(tag.key + "/edit")

def no_match(self, i: web.utils.Storage) -> NoReturn:
"""
Action to take when no tags are found.
Creates a new Tag.
Redirects the user to the tag's home page
"""
key = Tag.create_tag(i.tag_name, i.tag_description, i.tag_type, i.tag_plugins)
raise safe_seeother(key)


class tag_edit(delegate.page):
path = r"(/tags/OL\d+T)/edit"

def GET(self, key):
if not web.ctx.site.can_write(key):
return render_template(
"permission_denied",
web.ctx.fullpath,
"Permission denied to edit " + key + ".",
)

tag = web.ctx.site.get(key)
if tag is None:
raise web.notfound()

return render_template('type/tag/edit', tag)

def POST(self, key):
tag = web.ctx.site.get(key)
if tag is None:
raise web.notfound()

i = web.input(_comment=None)
formdata = self.process_input(i)
try:
if not formdata:
raise web.badrequest()
elif "_delete" in i:
tag = web.ctx.site.new(
key, {"key": key, "type": {"key": "/type/delete"}}
)
tag._save(comment=i._comment)
raise safe_seeother(key)
else:
tag.update(formdata)
tag._save(comment=i._comment)
raise safe_seeother(key)
except (ClientException, ValidationException) as e:
add_flash_message('error', str(e))
return render_template("type/tag/edit", tag)

def process_input(self, i):
i = utils.unflatten(i)
if i.tag_plugins:
i.tag_plugins = json.loads(i.tag_plugins)
tag = trim_doc(i)
return tag


def setup():
"""Do required setup."""
pass
3 changes: 2 additions & 1 deletion openlibrary/plugins/upstream/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from openlibrary import accounts

from openlibrary.plugins.upstream import addbook, covers, models, utils
from openlibrary.plugins.upstream import addbook, addtag, covers, models, utils
from openlibrary.plugins.upstream import spamcheck
from openlibrary.plugins.upstream import merge_authors
from openlibrary.plugins.upstream import edits
Expand Down Expand Up @@ -384,6 +384,7 @@ def setup():
models.setup()
utils.setup()
addbook.setup()
addtag.setup()
covers.setup()
merge_authors.setup()
# merge_works.setup() # ILE code
Expand Down
7 changes: 7 additions & 0 deletions openlibrary/plugins/upstream/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,12 @@ def get_seed(self, seed):
return models.Seed(self.get_list(), seed)


class Tag(models.Tag):
"""Class to represent /type/tag objects in Open Library."""

pass


def setup():
models.register_models()

Expand All @@ -1018,6 +1024,7 @@ def setup():
client.register_thing_class('/type/place', SubjectPlace)
client.register_thing_class('/type/person', SubjectPerson)
client.register_thing_class('/type/user', User)
client.register_thing_class('/type/tag', Tag)

client.register_changeset_class(None, Changeset) # set the default class
client.register_changeset_class('merge-authors', MergeAuthors)
Expand Down