Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
26 changes: 26 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Lint
on: [push, pull_request]

jobs:
lint:
name: Lint
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8

- name: Install Python Packages
run: |
pip install --upgrade pip
pip install tox

- name: Run isort
run: tox -e isort

- name: Run Black
run: tox -e black
1 change: 0 additions & 1 deletion api/admin/admin_authentication_provider.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

class AdminAuthenticationProvider(object):
def __init__(self, integration):
self.integration = integration
Expand Down
82 changes: 53 additions & 29 deletions api/admin/announcement_list_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@
from flask_babel import lazy_gettext as _

from api.admin.validator import Validator

from core.util.problem_detail import ProblemDetail
from core.problem_details import *
from core.util.problem_detail import ProblemDetail


class AnnouncementListValidator(Validator):

DATE_FORMAT = '%Y-%m-%d'
DATE_FORMAT = "%Y-%m-%d"

def __init__(self, maximum_announcements=3, minimum_announcement_length=15,
maximum_announcement_length=350, default_duration_days=60):
def __init__(
self,
maximum_announcements=3,
minimum_announcement_length=15,
maximum_announcement_length=350,
default_duration_days=60,
):
super(AnnouncementListValidator, self).__init__()
self.maximum_announcements = maximum_announcements
self.minimum_announcement_length = minimum_announcement_length
Expand All @@ -26,8 +30,10 @@ def __init__(self, maximum_announcements=3, minimum_announcement_length=15,
def validate_announcements(self, announcements):
validated_announcements = []
bad_format = INVALID_INPUT.detailed(
_("Invalid announcement list format: %(announcements)r",
announcements=announcements)
_(
"Invalid announcement list format: %(announcements)r",
announcements=announcements,
)
)
if isinstance(announcements, (bytes, str)):
try:
Expand All @@ -38,16 +44,18 @@ def validate_announcements(self, announcements):
return bad_format
if len(announcements) > self.maximum_announcements:
return INVALID_INPUT.detailed(
_("Too many announcements: maximum is %(maximum)d",
maximum=self.maximum_announcements)
_(
"Too many announcements: maximum is %(maximum)d",
maximum=self.maximum_announcements,
)
)

seen_ids = set()
for announcement in announcements:
validated = self.validate_announcement(announcement)
if isinstance(validated, ProblemDetail):
return validated
id = validated['id']
id = validated["id"]
if id in seen_ids:
return INVALID_INPUT.detailed(_("Duplicate announcement ID: %s" % id))
seen_ids.add(id)
Expand All @@ -58,46 +66,47 @@ def validate_announcement(self, announcement):
validated = dict()
if not isinstance(announcement, dict):
return INVALID_INPUT.detailed(
_("Invalid announcement format: %(announcement)r", announcement=announcement)
_(
"Invalid announcement format: %(announcement)r",
announcement=announcement,
)
)

validated['id'] = announcement.get('id', str(uuid.uuid4()))
validated["id"] = announcement.get("id", str(uuid.uuid4()))

for required_field in ('content',):
for required_field in ("content",):
if not required_field in announcement:
return INVALID_INPUT.detailed(
_("Missing required field: %(field)s", field=required_field)
)

# Validate the content of the announcement.
content = announcement['content']
content = announcement["content"]
content = self.validate_length(
content, self.minimum_announcement_length, self.maximum_announcement_length
)
if isinstance(content, ProblemDetail):
return content
validated['content'] = content
validated["content"] = content

# Validate the dates associated with the announcement
today_local = datetime.date.today()

start = self.validate_date(
'start', announcement.get('start', today_local)
)
start = self.validate_date("start", announcement.get("start", today_local))
if isinstance(start, ProblemDetail):
return start
validated['start'] = start
validated["start"] = start

default_finish = start + datetime.timedelta(days=self.default_duration_days)
day_after_start = start + datetime.timedelta(days=1)
finish = self.validate_date(
'finish',
announcement.get('finish', default_finish),
"finish",
announcement.get("finish", default_finish),
minimum=day_after_start,
)
if isinstance(finish, ProblemDetail):
return finish
validated['finish'] = finish
validated["finish"] = finish

# That's it!
return validated
Expand All @@ -114,14 +123,22 @@ def validate_length(self, value, minimum, maximum):
"""
if len(value) < minimum:
return INVALID_INPUT.detailed(
_('Value too short (%(length)d versus %(limit)d characters): %(value)s',
length=len(value), limit=minimum, value=value)
_(
"Value too short (%(length)d versus %(limit)d characters): %(value)s",
length=len(value),
limit=minimum,
value=value,
)
)

if len(value) > maximum:
return INVALID_INPUT.detailed(
_('Value too long (%(length)d versus %(limit)d characters): %(value)s',
length=len(value), limit=maximum, value=value)
_(
"Value too long (%(length)d versus %(limit)d characters): %(value)s",
length=len(value),
limit=maximum,
value=value,
)
)
return value

Expand All @@ -145,21 +162,28 @@ def validate_date(cls, field, value, minimum=None):
value = value.replace(tzinfo=dateutil.tz.tzlocal())
except ValueError as e:
return INVALID_INPUT.detailed(
_("Value for %(field)s is not a date: %(date)s", field=field, date=value)
_(
"Value for %(field)s is not a date: %(date)s",
field=field,
date=value,
)
)
if isinstance(value, datetime.datetime):
value = value.date()
if isinstance(minimum, datetime.datetime):
minimum = minimum.date()
if minimum and value < minimum:
return INVALID_INPUT.detailed(
_("Value for %(field)s must be no earlier than %(minimum)s",
field=field, minimum=minimum.strftime(cls.DATE_FORMAT)
_(
"Value for %(field)s must be no earlier than %(minimum)s",
field=field,
minimum=minimum.strftime(cls.DATE_FORMAT),
)
)
return value

def format_as_string(self, value):
"""Format the output of validate_announcements for storage in ConfigurationSetting.value"""
from ..announcements import Announcements

return json.dumps([x.json_ready for x in Announcements(value).announcements])
70 changes: 41 additions & 29 deletions api/admin/config.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,54 @@
from enum import Enum
import os
from enum import Enum
from urllib.parse import urljoin


class OperationalMode(str, Enum):
production = 'production'
development = 'development'
production = "production"
development = "development"


class Configuration:

APP_NAME = 'Palace Collection Manager'
PACKAGE_NAME = '@thepalaceproject/circulation-admin'
PACKAGE_VERSION = '0.0.6'
APP_NAME = "Palace Collection Manager"
PACKAGE_NAME = "@thepalaceproject/circulation-admin"
PACKAGE_VERSION = "0.0.6"

STATIC_ASSETS = {
'admin_js': 'circulation-admin.js',
'admin_css': 'circulation-admin.css',
'admin_logo': 'PalaceCollectionManagerLogo.svg',
"admin_js": "circulation-admin.js",
"admin_css": "circulation-admin.css",
"admin_logo": "PalaceCollectionManagerLogo.svg",
}

# For proper operation, `package_url` MUST end with a slash ('/') and
# `asset_rel_url` MUST NOT begin with one.
PACKAGE_TEMPLATES = {
OperationalMode.production: {
'package_url': 'https://cdn.jsdelivr.net/npm/{name}@{version}/',
'asset_rel_url': 'dist/{filename}'
"package_url": "https://cdn.jsdelivr.net/npm/{name}@{version}/",
"asset_rel_url": "dist/{filename}",
},
OperationalMode.development: {
'package_url': '/admin/',
'asset_rel_url': 'static/{filename}',
"package_url": "/admin/",
"asset_rel_url": "static/{filename}",
},
}

DEVELOPMENT_MODE_PACKAGE_TEMPLATE = 'node_modules/{name}'
STATIC_ASSETS_REL_PATH = 'dist'
DEVELOPMENT_MODE_PACKAGE_TEMPLATE = "node_modules/{name}"
STATIC_ASSETS_REL_PATH = "dist"

ADMIN_DIRECTORY = os.path.abspath(os.path.dirname(__file__))

# Environment variables that contain admin client package information.
ENV_ADMIN_UI_PACKAGE_NAME = 'TPP_CIRCULATION_ADMIN_PACKAGE_NAME'
ENV_ADMIN_UI_PACKAGE_VERSION = 'TPP_CIRCULATION_ADMIN_PACKAGE_VERSION'
ENV_ADMIN_UI_PACKAGE_NAME = "TPP_CIRCULATION_ADMIN_PACKAGE_NAME"
ENV_ADMIN_UI_PACKAGE_VERSION = "TPP_CIRCULATION_ADMIN_PACKAGE_VERSION"

@classmethod
def operational_mode(cls) -> OperationalMode:
return (OperationalMode.development
if os.path.isdir(cls.package_development_directory())
else OperationalMode.production)
return (
OperationalMode.development
if os.path.isdir(cls.package_development_directory())
else OperationalMode.production
)

@classmethod
def _package_name(cls) -> str:
Expand All @@ -58,7 +60,9 @@ def _package_name(cls) -> str:
return os.environ.get(cls.ENV_ADMIN_UI_PACKAGE_NAME) or cls.PACKAGE_NAME

@classmethod
def lookup_asset_url(cls, key: str, *, _operational_mode: OperationalMode = None) -> str:
def lookup_asset_url(
cls, key: str, *, _operational_mode: OperationalMode = None
) -> str:
"""Get the URL for the asset_type.

:param key: The key used to lookup an asset's filename. If the key is
Expand All @@ -72,8 +76,12 @@ def lookup_asset_url(cls, key: str, *, _operational_mode: OperationalMode = None
"""
operational_mode = _operational_mode or cls.operational_mode()
filename = cls.STATIC_ASSETS.get(key, key)
return urljoin(cls.package_url(_operational_mode=operational_mode),
cls.PACKAGE_TEMPLATES[operational_mode]['asset_rel_url'].format(filename=filename))
return urljoin(
cls.package_url(_operational_mode=operational_mode),
cls.PACKAGE_TEMPLATES[operational_mode]["asset_rel_url"].format(
filename=filename
),
)

@classmethod
def package_url(cls, *, _operational_mode: OperationalMode = None) -> str:
Expand All @@ -88,11 +96,13 @@ def package_url(cls, *, _operational_mode: OperationalMode = None) -> str:
:rtype: str
"""
operational_mode = _operational_mode or cls.operational_mode()
version = (os.environ.get(cls.ENV_ADMIN_UI_PACKAGE_VERSION) or cls.PACKAGE_VERSION)
template = cls.PACKAGE_TEMPLATES[operational_mode]['package_url']
version = (
os.environ.get(cls.ENV_ADMIN_UI_PACKAGE_VERSION) or cls.PACKAGE_VERSION
)
template = cls.PACKAGE_TEMPLATES[operational_mode]["package_url"]
url = template.format(name=cls._package_name(), version=version)
if not url.endswith('/'):
url += '/'
if not url.endswith("/"):
url += "/"
return url

@classmethod
Expand All @@ -105,8 +115,10 @@ def package_development_directory(cls, *, _base_dir: str = None) -> str:
:rtype: str
"""
base_dir = _base_dir or cls.ADMIN_DIRECTORY
return os.path.join(base_dir,
cls.DEVELOPMENT_MODE_PACKAGE_TEMPLATE.format(name=cls._package_name()))
return os.path.join(
base_dir,
cls.DEVELOPMENT_MODE_PACKAGE_TEMPLATE.format(name=cls._package_name()),
)

@classmethod
def static_files_directory(cls, *, _base_dir: str = None) -> str:
Expand Down
Loading