diff --git a/migrations/env.py b/migrations/env.py index 2afcdb7..1a8b006 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -75,6 +75,7 @@ def process_revision_directives(context, revision, directives): target_metadata=target_metadata, process_revision_directives=process_revision_directives, render_as_batch=True, + compare_type=True, **current_app.extensions['migrate'].configure_args) try: diff --git a/migrations/versions/2181bdb161da_.py b/migrations/versions/2181bdb161da_.py deleted file mode 100644 index 1b1255f..0000000 --- a/migrations/versions/2181bdb161da_.py +++ /dev/null @@ -1,44 +0,0 @@ -"""empty message - -Revision ID: 2181bdb161da -Revises: 29d8df81268e -Create Date: 2018-07-25 15:03:47.179134 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '2181bdb161da' -down_revision = '29d8df81268e' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('Settings', - sa.Column('user', sa.String(length=190), nullable=False), - sa.Column('settings', sa.Text(), nullable=True), - sa.PrimaryKeyConstraint('user', name=op.f('pk_Settings')) - ) - with op.batch_alter_table('Settings', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_Settings_user'), ['user'], unique=False) - - with op.batch_alter_table('Item', schema=None) as batch_op: - batch_op.add_column(sa.Column('update_name_from_schema', sa.Boolean(), nullable=False)) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('Item', schema=None) as batch_op: - batch_op.drop_column('update_name_from_schema') - - with op.batch_alter_table('Settings', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_Settings_user')) - - op.drop_table('Settings') - # ### end Alembic commands ### diff --git a/migrations/versions/70ab97044985_.py b/migrations/versions/70ab97044985_.py deleted file mode 100644 index 07eb092..0000000 --- a/migrations/versions/70ab97044985_.py +++ /dev/null @@ -1,32 +0,0 @@ -"""empty message - -Revision ID: 70ab97044985 -Revises: 73e290cb35a9 -Create Date: 2018-09-14 03:07:21.961058 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '70ab97044985' -down_revision = '73e290cb35a9' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('File', schema=None) as batch_op: - batch_op.add_column(sa.Column('visible_for', sa.String(length=190), nullable=True)) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('File', schema=None) as batch_op: - batch_op.drop_column('visible_for') - - # ### end Alembic commands ### diff --git a/migrations/versions/73e290cb35a9_.py b/migrations/versions/73e290cb35a9_.py deleted file mode 100644 index 7f12e2a..0000000 --- a/migrations/versions/73e290cb35a9_.py +++ /dev/null @@ -1,34 +0,0 @@ -"""empty message - -Revision ID: 73e290cb35a9 -Revises: 2181bdb161da -Create Date: 2018-08-22 14:10:26.041878 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '73e290cb35a9' -down_revision = '2181bdb161da' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('Item', schema=None) as batch_op: - batch_op.create_unique_constraint('_name_type_id_uc', ['name', 'type_id']) - batch_op.drop_constraint('uq_Item_name', type_='unique') - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('Item', schema=None) as batch_op: - batch_op.create_unique_constraint('uq_Item_name', ['name']) - batch_op.drop_constraint('_name_type_id_uc', type_='unique') - - # ### end Alembic commands ### diff --git a/migrations/versions/29d8df81268e_.py b/migrations/versions/cd02e7f9639a_.py similarity index 84% rename from migrations/versions/29d8df81268e_.py rename to migrations/versions/cd02e7f9639a_.py index cbee9e6..b326f32 100644 --- a/migrations/versions/29d8df81268e_.py +++ b/migrations/versions/cd02e7f9639a_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 29d8df81268e -Revises: -Create Date: 2018-07-16 16:11:00.902328 +Revision ID: cd02e7f9639a +Revises: +Create Date: 2019-06-14 17:51:42.375806 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '29d8df81268e' +revision = 'cd02e7f9639a' down_revision = None branch_labels = None depends_on = None @@ -24,7 +24,7 @@ def upgrade(): sa.Column('type', sa.String(length=190), nullable=True), sa.Column('jsonschema', sa.Text(), nullable=True), sa.Column('visible_for', sa.String(length=190), nullable=True), - sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('deleted_time', sa.Integer(), nullable=True), sa.PrimaryKeyConstraint('id', name=op.f('pk_AttributeDefinition')), sa.UniqueConstraint('name', name=op.f('uq_AttributeDefinition_name')) ) @@ -44,7 +44,7 @@ def upgrade(): sa.Column('name_schema', sa.String(length=190), nullable=True), sa.Column('lendable', sa.Boolean(), nullable=True), sa.Column('lending_duration', sa.Integer(), nullable=True), - sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('deleted_time', sa.Integer(), nullable=True), sa.Column('visible_for', sa.String(length=190), nullable=True), sa.Column('how_to', sa.Text(), nullable=True), sa.PrimaryKeyConstraint('id', name=op.f('pk_ItemType')), @@ -54,15 +54,23 @@ def upgrade(): sa.Column('id', sa.Integer(), nullable=False), sa.Column('moderator', sa.String(length=190), nullable=True), sa.Column('user', sa.String(length=190), nullable=True), - sa.Column('date', sa.DateTime(), nullable=True), + sa.Column('date', sa.Integer(), nullable=True), sa.Column('deposit', sa.String(length=190), nullable=True), sa.PrimaryKeyConstraint('id', name=op.f('pk_Lending')) ) + op.create_table('Settings', + sa.Column('user', sa.String(length=190), nullable=False), + sa.Column('settings', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('user', name=op.f('pk_Settings')) + ) + with op.batch_alter_table('Settings', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_Settings_user'), ['user'], unique=False) + op.create_table('Tag', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=190), nullable=True), sa.Column('lending_duration', sa.Integer(), nullable=True), - sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('deleted_time', sa.Integer(), nullable=True), sa.Column('visible_for', sa.String(length=190), nullable=True), sa.PrimaryKeyConstraint('id', name=op.f('pk_Tag')), sa.UniqueConstraint('name', name=op.f('uq_Tag_name')) @@ -70,7 +78,7 @@ def upgrade(): op.create_table('BlacklistToItemType', sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('item_type_id', sa.Integer(), nullable=False), - sa.Column('end_time', sa.DateTime(), nullable=True), + sa.Column('end_time', sa.Integer(), nullable=True), sa.Column('reason', sa.Text(), nullable=True), sa.ForeignKeyConstraint(['item_type_id'], ['ItemType.id'], name=op.f('fk_BlacklistToItemType_item_type_id')), sa.ForeignKeyConstraint(['user_id'], ['Blacklist.id'], name=op.f('fk_BlacklistToItemType_user_id')), @@ -79,13 +87,17 @@ def upgrade(): op.create_table('Item', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name', sa.String(length=190), nullable=True), + sa.Column('update_name_from_schema', sa.Boolean(), nullable=False), sa.Column('type_id', sa.Integer(), nullable=True), + sa.Column('lending_id', sa.Integer(), nullable=True), sa.Column('lending_duration', sa.Integer(), nullable=True), - sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('due', sa.Integer(), nullable=True), + sa.Column('deleted_time', sa.Integer(), nullable=True), sa.Column('visible_for', sa.String(length=190), nullable=True), + sa.ForeignKeyConstraint(['lending_id'], ['Lending.id'], name=op.f('fk_Item_lending_id')), sa.ForeignKeyConstraint(['type_id'], ['ItemType.id'], name=op.f('fk_Item_type_id')), sa.PrimaryKeyConstraint('id', name=op.f('pk_Item')), - sa.UniqueConstraint('name', name=op.f('uq_Item_name')) + sa.UniqueConstraint('name', 'type_id', name='_name_type_id_uc') ) op.create_table('ItemTypeToAttributeDefinition', sa.Column('item_type_id', sa.Integer(), nullable=False), @@ -114,19 +126,17 @@ def upgrade(): sa.Column('name', sa.String(length=190), nullable=True), sa.Column('file_type', sa.String(length=190), nullable=True), sa.Column('file_hash', sa.String(length=190), nullable=True), - sa.Column('creation', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True), - sa.Column('invalidation', sa.DateTime(), nullable=True), + sa.Column('creation', sa.Integer(), nullable=True), + sa.Column('invalidation', sa.Integer(), nullable=True), + sa.Column('visible_for', sa.String(length=190), nullable=True), sa.ForeignKeyConstraint(['item_id'], ['Item.id'], name=op.f('fk_File_item_id')), sa.PrimaryKeyConstraint('id', name=op.f('pk_File')) ) - with op.batch_alter_table('File', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_File_file_hash'), ['file_hash'], unique=False) - op.create_table('ItemToAttributeDefinition', sa.Column('item_id', sa.Integer(), nullable=False), sa.Column('attribute_definition_id', sa.Integer(), nullable=False), sa.Column('value', sa.String(length=190), nullable=True), - sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('deleted_time', sa.Integer(), nullable=True), sa.ForeignKeyConstraint(['attribute_definition_id'], ['AttributeDefinition.id'], name=op.f('fk_ItemToAttributeDefinition_attribute_definition_id')), sa.ForeignKeyConstraint(['item_id'], ['Item.id'], name=op.f('fk_ItemToAttributeDefinition_item_id')), sa.PrimaryKeyConstraint('item_id', 'attribute_definition_id', name=op.f('pk_ItemToAttributeDefinition')) @@ -138,14 +148,6 @@ def upgrade(): sa.ForeignKeyConstraint(['parent_id'], ['Item.id'], name=op.f('fk_ItemToItem_parent_id')), sa.PrimaryKeyConstraint('parent_id', 'item_id', name=op.f('pk_ItemToItem')) ) - op.create_table('ItemToLending', - sa.Column('item_id', sa.Integer(), nullable=False), - sa.Column('lending_id', sa.Integer(), nullable=False), - sa.Column('due', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['item_id'], ['Item.id'], name=op.f('fk_ItemToLending_item_id')), - sa.ForeignKeyConstraint(['lending_id'], ['Lending.id'], name=op.f('fk_ItemToLending_lending_id')), - sa.PrimaryKeyConstraint('item_id', 'lending_id', name=op.f('pk_ItemToLending')) - ) op.create_table('ItemToTag', sa.Column('item_id', sa.Integer(), nullable=False), sa.Column('tag_id', sa.Integer(), nullable=False), @@ -159,12 +161,8 @@ def upgrade(): def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_table('ItemToTag') - op.drop_table('ItemToLending') op.drop_table('ItemToItem') op.drop_table('ItemToAttributeDefinition') - with op.batch_alter_table('File', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_File_file_hash')) - op.drop_table('File') op.drop_table('TagToAttributeDefinition') op.drop_table('ItemTypeToItemType') @@ -172,6 +170,10 @@ def downgrade(): op.drop_table('Item') op.drop_table('BlacklistToItemType') op.drop_table('Tag') + with op.batch_alter_table('Settings', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_Settings_user')) + + op.drop_table('Settings') op.drop_table('Lending') op.drop_table('ItemType') with op.batch_alter_table('Blacklist', schema=None) as batch_op: diff --git a/requirements.txt b/requirements.txt index b3e8617..d0e03e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Flask==1.0.2 -flask-restplus==0.11.0 +flask-restplus==0.12.1 Flask-SQLAlchemy==2.3.1 Flask-Migrate==2.2.1 Flask-Webpack==0.1.0 diff --git a/total_tolles_ferleihsystem/__init__.py b/total_tolles_ferleihsystem/__init__.py index b2d4724..e6e253a 100644 --- a/total_tolles_ferleihsystem/__init__.py +++ b/total_tolles_ferleihsystem/__init__.py @@ -44,6 +44,7 @@ APP.logger.info('Connecting to database %s.', APP.config['SQLALCHEMY_DATABASE_URI']) AUTH_LOGGER = getLogger('flask.app.auth') # type: Logger +LENDING_LOGGER = getLogger('ttf.lending') # type: Logger # Setup DB with Migrations and bcrypt DB: SQLAlchemy diff --git a/total_tolles_ferleihsystem/api/catalog/attribute_definition.py b/total_tolles_ferleihsystem/api/catalog/attribute_definition.py index 0745e09..bd30aa3 100644 --- a/total_tolles_ferleihsystem/api/catalog/attribute_definition.py +++ b/total_tolles_ferleihsystem/api/catalog/attribute_definition.py @@ -1,15 +1,17 @@ """ This module contains all API endpoints for the namespace 'attribute_definition' """ +import time from flask import request from flask_restplus import Resource, abort, marshal -from flask_jwt_extended import jwt_required +from flask_jwt_extended import jwt_required, get_jwt_claims +from sqlalchemy.orm import joinedload from sqlalchemy.exc import IntegrityError from .. import API, satisfies_role from ..models import ATTRIBUTE_DEFINITION_GET, ATTRIBUTE_DEFINITION_POST, ATTRIBUTE_DEFINITION_PUT, ATTRIBUTE_DEFINITION_VALUES -from ... import DB +from ... import DB, APP from ...login import UserRole from ...db_models.item import ItemToAttributeDefinition from ...db_models.attributeDefinition import AttributeDefinition @@ -35,8 +37,21 @@ def get(self): """ Get a list of all attribute definitions currently in the system """ + base_query = AttributeDefinition.query test_for = request.args.get('deleted', 'false') == 'true' - return AttributeDefinition.query.filter(AttributeDefinition.deleted == test_for).order_by(AttributeDefinition.name).all() + if test_for: + base_query = base_query.filter(AttributeDefinition.deleted_time != None) + else: + base_query = base_query.filter(AttributeDefinition.deleted_time == None) + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((AttributeDefinition.visible_for == 'all') | (AttributeDefinition.visible_for == 'moderator')) + else: + base_query = base_query.filter(AttributeDefinition.visible_for == 'all') + + return base_query.order_by(AttributeDefinition.name).all() @jwt_required @satisfies_role(UserRole.ADMIN) @@ -55,8 +70,10 @@ def post(self): return marshal(new, ATTRIBUTE_DEFINITION_GET), 201 except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Name is not unique. %s', err) abort(409, 'Name is not unique!') + APP.logger.error('SQL Error: %s', err) abort(500) @ANS.route('//') @@ -73,8 +90,18 @@ def get(self, definition_id): """ Get a single attribute definition """ - attribute = AttributeDefinition.query.filter(AttributeDefinition.id == definition_id).first() + base_query = AttributeDefinition.query.filter(AttributeDefinition.id == definition_id) + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((AttributeDefinition.visible_for == 'all') | (AttributeDefinition.visible_for == 'moderator')) + else: + base_query = base_query.filter(AttributeDefinition.visible_for == 'all') + + attribute = base_query.first() if attribute is None: + APP.logger.debug('Requested attribute not found for id: %s !', definition_id) abort(404, 'Requested attribute not found!') return attribute @@ -126,6 +153,7 @@ def post(self, definition_id): """ attribute = AttributeDefinition.query.filter(AttributeDefinition.id == definition_id).first() if attribute is None: + APP.logger.debug('Requested attribute not found for id: %s !', definition_id) abort(404, 'Requested attribute not found!') attribute.deleted = False DB.session.commit() @@ -143,6 +171,7 @@ def put(self, definition_id): """ attribute = AttributeDefinition.query.filter(AttributeDefinition.id == definition_id).first() if attribute is None: + APP.logger.debug('Requested attribute not found for id: %s !', definition_id) abort(404, 'Requested attribute not found!') attribute.update(**request.get_json()) try: @@ -150,8 +179,10 @@ def put(self, definition_id): return marshal(attribute, ATTRIBUTE_DEFINITION_GET), 200 except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Name is not unique. %s', err) abort(409, 'Name is not unique!') + APP.logger.error('SQL Error: %s', err) abort(500) @@ -168,7 +199,18 @@ def get(self, definition_id): """ Get all values of this attribute definition """ - if AttributeDefinition.query.filter(AttributeDefinition.id == definition_id).first() is None: + base_query = AttributeDefinition.query.options(joinedload('_item_to_attribute_definitions').joinedload('item')).filter(AttributeDefinition.id == definition_id) + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((AttributeDefinition.visible_for == 'all') | (AttributeDefinition.visible_for == 'moderator')) + else: + base_query = base_query.filter(AttributeDefinition.visible_for == 'all') + + attributeDefinition = base_query.first() + if attributeDefinition is None: + APP.logger.debug('Requested attribute not found for id: %s !', definition_id) abort(404, 'Requested attribute not found!') - return [item.value for item in DB.session.query(ItemToAttributeDefinition.value).filter(ItemToAttributeDefinition.attribute_definition_id == definition_id).distinct()] + return list(set([itad.value for itad in attributeDefinition._item_to_attribute_definitions])).sort() diff --git a/total_tolles_ferleihsystem/api/catalog/file.py b/total_tolles_ferleihsystem/api/catalog/file.py index 1fc03ea..4b2b811 100644 --- a/total_tolles_ferleihsystem/api/catalog/file.py +++ b/total_tolles_ferleihsystem/api/catalog/file.py @@ -9,6 +9,7 @@ from flask import request, make_response from flask_restplus import Resource, abort, marshal from flask_jwt_extended import jwt_required, get_jwt_claims +from sqlalchemy.orm import joinedload from sqlalchemy.exc import IntegrityError from total_tolles_ferleihsystem.tasks.file import create_archive @@ -36,14 +37,14 @@ def get(self): """ Get a list of files """ - base_query = File.query + base_query = File.query.options(joinedload('item')) # auth check if UserRole(get_jwt_claims()) != UserRole.ADMIN: if UserRole(get_jwt_claims()) == UserRole.MODERATOR: - base_query = base_query.filter((Item.visible_for == 'all') | (Item.visible_for == 'moderator')) + base_query = base_query.filter((File.visible_for == 'all') | (File.visible_for == 'moderator')) else: - base_query = base_query.filter(Item.visible_for == 'all') + base_query = base_query.filter(File.visible_for == 'all') return base_query.all() @@ -64,7 +65,7 @@ def post(self): file = request.files['file'] if not file: abort(400, 'File Empty!') - if file.filename == None or file.filename == '': + if file.filename is None or file.filename == '': abort(400, 'No file name!') item_id = request.form['item_id'] if item_id is None: @@ -85,12 +86,9 @@ def post(self): file_on_disk.write(file.stream.read()) # add the file to the sql database - try: - DB.session.add(new) - DB.session.commit() - return marshal(new, FILE_GET), 201 - except IntegrityError: - abort(500, 'SQL Error!') + DB.session.add(new) + DB.session.commit() + return marshal(new, FILE_GET), 201 @ANS.route('//') @@ -106,18 +104,20 @@ def get(self, file_id): """ Get a single file object """ - base_query = File.query + base_query = File.query.filter(File.id == file_id).options(joinedload('item')) # auth check if UserRole(get_jwt_claims()) != UserRole.ADMIN: if UserRole(get_jwt_claims()) == UserRole.MODERATOR: - base_query = base_query.filter((Item.visible_for == 'all') | (Item.visible_for == 'moderator')) + base_query = base_query.filter((File.visible_for == 'all') | (File.visible_for == 'moderator')) else: - base_query = base_query.filter(Item.visible_for == 'all') + base_query = base_query.filter(File.visible_for == 'all') + + file = base_query.first() - file = base_query.filter(File.id == file_id).first() if file is None: - abort(404, 'Requested item not found!') + APP.logger.debug('Requested file not found for id: %s !', file_id) + abort(404, 'Requested file not found!') return file @@ -130,8 +130,11 @@ def delete(self, file_id): Delete a file object """ file = File.query.filter(File.id == file_id).first() + if file is None: - abort(404, 'Requested item not found!') + APP.logger.debug('Requested file not found for id: %s !', file_id) + abort(404, 'Requested file not found!') + DB.session.delete(file) DB.session.commit() return "", 204 @@ -146,17 +149,16 @@ def put(self, file_id): """ Replace a file object """ - file = File.query.filter(File.id == file_id).first() + file = File.query.filter(File.id == file_id).options(joinedload('item')).first() + if file is None: + APP.logger.debug('Requested file not found for id: %s !', file_id) abort(404, 'Requested file not found!') file.update(**request.get_json()) - try: - DB.session.commit() - return file - except IntegrityError: - abort(500, 'SQL Error!') + DB.session.commit() + return file @ANS.route('/archive') @@ -204,23 +206,32 @@ def map_function(file_id: int) -> Tuple[str, str]: PATH2: str = '/file-store' ANS2 = API.namespace('file', description='The download Endpoint to download any file from the system.', path=PATH2) -@ANS2.route('//') +@ANS2.route('//') class FileData(Resource): """ The endpoints to get the actual stored file """ @jwt_required - @satisfies_role(UserRole.MODERATOR) @ANS.response(404, 'Requested file not found!') @ANS.response(500, 'Something crashed while reading file!') - def get(self, file_hash): + def get(self, file_id): """ Get the actual file """ - file = File.query.filter(File.file_hash == file_hash).first() + base_query = File.query.filter(File.file_id == file_id).options(joinedload('item')) + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((File.visible_for == 'all') | (File.visible_for == 'moderator')) + else: + base_query = base_query.filter(File.visible_for == 'all') + + file = base_query.first() if file is None: + APP.logger.debug('Requested file not found for id: %s !', file_id) abort(404, 'Requested file was not found!') headers = { @@ -229,4 +240,6 @@ def get(self, file_hash): with open(os.path.join(APP.config['DATA_DIRECTORY'], file.file_hash), mode='rb') as file_on_disk: return make_response(file_on_disk.read(), headers) + + APP.logger.error('Crash while downloading file: %s !', file_id) abort(500, 'Something crashed while reading file!') diff --git a/total_tolles_ferleihsystem/api/catalog/item.py b/total_tolles_ferleihsystem/api/catalog/item.py index 132141b..e5cb4a9 100644 --- a/total_tolles_ferleihsystem/api/catalog/item.py +++ b/total_tolles_ferleihsystem/api/catalog/item.py @@ -5,22 +5,20 @@ from flask import request from flask_restplus import Resource, abort, marshal from flask_jwt_extended import jwt_required, get_jwt_claims +from sqlalchemy.orm import joinedload from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import noload, joinedload from .. import API, satisfies_role -from ..models import ITEM_GET, ITEM_POST, ID, ITEM_PUT, ITEM_TAG_GET, ATTRIBUTE_PUT, ATTRIBUTE_GET, FILE_GET, ITEM_GET_WITH_PARENTS -from ... import DB +from ..models import ITEM_GET, ITEM_POST, ID, ITEM_PUT, ITEM_TAG_GET, ATTRIBUTE_GET, FILE_GET, LENDING_GET +from ... import DB, APP from ...login import UserRole from ...performance import record_view_performance -from ...db_models.item import Item, ItemToTag, ItemToAttributeDefinition, ItemToItem, File, ItemToLending +from ...db_models.item import Item, ItemToTag, ItemToAttributeDefinition, ItemToItem, File, Lending from ...db_models.itemType import ItemType, ItemTypeToItemType from ...db_models.tag import Tag from ...db_models.attributeDefinition import AttributeDefinition -import logging - PATH: str = '/catalog/items' ANS = API.namespace('item', description='Items', path=PATH) @@ -41,9 +39,12 @@ def get(self): """ Get a list of all items currently in the system """ + base_query = Item.query.options(joinedload('lending'), joinedload("_tags")) test_for = request.args.get('deleted', 'false') == 'true' - - base_query = Item.query.options(joinedload(Item.type)).filter(Item.deleted == test_for) + if test_for: + base_query = base_query.filter(Item.deleted_time != None) + else: + base_query = base_query.filter(Item.deleted_time == None) # auth check if UserRole(get_jwt_claims()) != UserRole.ADMIN: @@ -53,7 +54,7 @@ def get(self): base_query = base_query.filter(Item.visible_for == 'all') if request.args.get('lent', 'false') == 'true': - base_query = base_query.join(ItemToLending) + base_query = base_query.filter(Item.lending_id != None) return base_query.order_by(Item.name).all() @@ -84,7 +85,9 @@ def post(self): return marshal(new, ITEM_GET), 201 except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + print(message) + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Name is not unique. %s', err) abort(409, 'Name is not unique!') abort(500) @@ -96,7 +99,7 @@ class ItemDetail(Resource): @jwt_required @ANS.response(404, 'Requested item not found!') - @API.marshal_with(ITEM_GET_WITH_PARENTS) + @API.marshal_with(ITEM_GET) # pylint: disable=R0201 def get(self, item_id): """ @@ -192,8 +195,9 @@ def put(self, item_id): return marshal(item, ITEM_GET), 200 except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: - abort(409, 'Name is not unique!:' + message) + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Name is not unique. %s', err) + abort(409, 'Name is not unique!') abort(500) @ANS.route('//tags/') @@ -219,7 +223,8 @@ def get(self, item_id): else: base_query = base_query.filter(Item.visible_for == 'all') - if base_query.filter(Item.id == item_id).filter(Item.deleted == False).first() is None: + # pylint: disable=C0121 + if base_query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() is None: abort(404, 'Requested item not found!') associations = ItemToTag.query.filter(ItemToTag.item_id == item_id).all() @@ -238,10 +243,12 @@ def post(self, item_id): Associate a new tag with the item. """ tag_id = request.get_json()["id"] - item = Item.query.filter(Item.id == item_id).filter(Item.deleted == False).first() + # pylint: disable=C0121 + item = Item.query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() if item is None: abort(404, 'Requested item not found!') - tag = Tag.query.filter(Tag.id == tag_id).filter(Tag.deleted == False).first() + # pylint: disable=C0121 + tag = Tag.query.filter(Tag.id == tag_id).filter(Tag.deleted_time == None).first() if tag is None: abort(400, 'Requested item tag not found!') @@ -258,7 +265,8 @@ def post(self, item_id): return [e.tag for e in associations] except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Tag is already associated with this item. %s', err) abort(409, 'Tag is already associated with this item!') abort(500) @@ -275,10 +283,12 @@ def delete(self, item_id): """ tag_id = request.get_json()["id"] - item = Item.query.filter(Item.id == item_id).filter(Item.deleted == False).first() + # pylint: disable=C0121 + item = Item.query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() if item is None: abort(404, 'Requested item not found!') - if Tag.query.filter(Tag.id == tag_id).filter(Tag.deleted == False).first() is None: + # pylint: disable=C0121 + if Tag.query.filter(Tag.id == tag_id).filter(Tag.deleted_time == None).first() is None: abort(400, 'Requested item tag not found!') association = (ItemToTag @@ -324,10 +334,18 @@ def get(self, item_id): else: base_query = base_query.filter(Item.visible_for == 'all') - if base_query.filter(Item.id == item_id).filter(Item.deleted == False).first() is None: + # pylint: disable=C0121 + if base_query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() is None: abort(404, 'Requested item not found!') - attributes = ItemToAttributeDefinition.query.filter(ItemToAttributeDefinition.item_id == item_id).filter(ItemToAttributeDefinition.deleted == False).join(ItemToAttributeDefinition.attribute_definition).order_by(AttributeDefinition.name).all() + # pylint: disable=C0121 + attributes = (ItemToAttributeDefinition.query + .filter(ItemToAttributeDefinition.item_id == item_id) + .filter(ItemToAttributeDefinition.deleted_time == None) + .join(ItemToAttributeDefinition.attribute_definition) + .order_by(AttributeDefinition.name) + .options(joinedload('attribute_definition')) + .all()) return attributes; # DON'T CHANGE THIS!! # It is necessary because a Flask Bug prehibits log messages on return statements. @@ -355,14 +373,17 @@ def get(self, item_id, attribute_definition_id): else: base_query = base_query.filter(Item.visible_for == 'all') - if base_query.filter(Item.id == item_id).filter(Item.deleted == False).first() is None: + # pylint: disable=C0121 + if base_query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() is None: abort(404, 'Requested item not found!') + # pylint: disable=C0121 attribute = (ItemToAttributeDefinition .query .filter(ItemToAttributeDefinition.item_id == item_id) - .filter(ItemToAttributeDefinition.deleted == False) + .filter(ItemToAttributeDefinition.deleted_time == None) .filter(ItemToAttributeDefinition.attribute_definition_id == attribute_definition_id) + .options(joinedload('attribute_definition')) .first()) if attribute is None: @@ -382,15 +403,18 @@ def put(self, item_id, attribute_definition_id): Set a single attribute of this item. """ - if Item.query.filter(Item.id == item_id).filter(Item.deleted == False).first() is None: + # pylint: disable=C0121 + if Item.query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() is None: abort(404, 'Requested item not found!') value = request.get_json()["value"] + # pylint: disable=C0121 attribute = (ItemToAttributeDefinition .query .filter(ItemToAttributeDefinition.item_id == item_id) .filter(ItemToAttributeDefinition.attribute_definition_id == attribute_definition_id) - .filter(ItemToAttributeDefinition.deleted == False) + .filter(ItemToAttributeDefinition.deleted_time == None) + .options(joinedload('attribute_definition')) .first()) if attribute is None: @@ -404,6 +428,38 @@ def put(self, item_id, attribute_definition_id): except IntegrityError: abort(500) + +@ANS.route('//parent/') +class ItemParentItems(Resource): + """ + The parent items of this item object + """ + + @jwt_required + @ANS.response(404, 'Requested item not found!') + @API.marshal_list_with(ITEM_GET) + # pylint: disable=R0201 + def get(self, item_id): + """ + Get all contained items of this item. + """ + base_query = Item.query + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((Item.visible_for == 'all') | (Item.visible_for == 'moderator')) + else: + base_query = base_query.filter(Item.visible_for == 'all') + + # pylint: disable=C0121 + if base_query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() is None: + abort(404, 'Requested item not found!') + + associations = ItemToItem.query.filter(ItemToItem.item_id == item_id).options(joinedload('parent')).all() + return [e.parent for e in associations if not e.item.deleted] + + @ANS.route('//contained/') class ItemContainedItems(Resource): """ @@ -412,7 +468,7 @@ class ItemContainedItems(Resource): @jwt_required @ANS.response(404, 'Requested item not found!') - @API.marshal_with(ITEM_GET) + @API.marshal_list_with(ITEM_GET) # pylint: disable=R0201 def get(self, item_id): """ @@ -427,10 +483,11 @@ def get(self, item_id): else: base_query = base_query.filter(Item.visible_for == 'all') - if base_query.filter(Item.id == item_id).filter(Item.deleted == False).first() is None: + # pylint: disable=C0121 + if base_query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() is None: abort(404, 'Requested item not found!') - associations = ItemToItem.query.filter(ItemToItem.parent_id == item_id).all() + associations = ItemToItem.query.filter(ItemToItem.parent_id == item_id).options(joinedload('item')).all() return [e.item for e in associations if not e.item.deleted] @jwt_required @@ -447,8 +504,10 @@ def post(self, item_id): Add a new contained item to this item """ contained_item_id = request.get_json()["id"] - parent = Item.query.filter(Item.id == item_id).filter(Item.deleted == False).first() - child = Item.query.filter(Item.id == contained_item_id).filter(Item.deleted == False).first() + # pylint: disable=C0121 + parent = Item.query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() + # pylint: disable=C0121 + child = Item.query.filter(Item.id == contained_item_id).filter(Item.deleted_time == None).first() if parent is None: abort(404, 'Requested item (current) not found!') @@ -468,12 +527,13 @@ def post(self, item_id): try: DB.session.add(new) DB.session.commit() - associations = ItemToItem.query.filter(ItemToItem.parent_id == item_id).all() + associations = ItemToItem.query.filter(ItemToItem.parent_id == item_id).options(joinedload('item')).all() return [e.item for e in associations] except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: - abort(409, 'Attribute definition is already asociated with this tag!') + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('That item is already contained in this item. %s', err) + abort(409, 'That item is already contained in this item.') abort(500) @jwt_required @@ -489,9 +549,11 @@ def delete(self, item_id): """ contained_item_id = request.get_json()["id"] - if Item.query.filter(Item.id == item_id).filter(Item.deleted == False).first() is None: + # pylint: disable=C0121 + if Item.query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() is None: abort(404, 'Requested item (current) not found!') - if Item.query.filter(Item.id == contained_item_id).filter(Item.deleted == False).first() is None: + # pylint: disable=C0121 + if Item.query.filter(Item.id == contained_item_id).filter(Item.deleted_time == None).first() is None: abort(400, 'Requested item (to be contained) not found!') association = (ItemToItem @@ -532,7 +594,38 @@ def get(self, item_id): else: base_query = base_query.filter(Item.visible_for == 'all') - if base_query.filter(Item.id == item_id).filter(Item.deleted == False).first() is None: + # pylint: disable=C0121 + if base_query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() is None: + abort(404, 'Requested item not found!') + + return File.query.filter(File.item_id == item_id).options(joinedload('item')).all() + +@ANS.route('//lending/') +class ItemLendings(Resource): + """ + Current lending of a single item. + """ + + @jwt_required + @ANS.response(404, 'Requested item not found!') + @API.marshal_list_with(LENDING_GET) + # pylint: disable=R0201 + def get(self, item_id): + """ + Get the lending concerning the specific item. + """ + base_query = Item.query + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((Item.visible_for == 'all') | (Item.visible_for == 'moderator')) + else: + base_query = base_query.filter(Item.visible_for == 'all') + + # pylint: disable=C0121 + item = base_query.filter(Item.id == item_id).filter(Item.deleted_time == None).first() + if item is None: abort(404, 'Requested item not found!') - return File.query.filter(File.item_id == item_id).all() + return item.lending diff --git a/total_tolles_ferleihsystem/api/catalog/item_tag.py b/total_tolles_ferleihsystem/api/catalog/item_tag.py index 7b9d11a..196328e 100644 --- a/total_tolles_ferleihsystem/api/catalog/item_tag.py +++ b/total_tolles_ferleihsystem/api/catalog/item_tag.py @@ -4,7 +4,8 @@ from flask import request from flask_restplus import Resource, abort, marshal -from flask_jwt_extended import jwt_required +from flask_jwt_extended import jwt_required, get_jwt_claims +from sqlalchemy.orm import joinedload from sqlalchemy.exc import IntegrityError from .. import API, satisfies_role @@ -26,7 +27,6 @@ class ItemTagList(Resource): Item tags root item tag """ - @jwt_required @API.param('deleted', 'get all deleted item tags (and only these)', type=bool, required=False, default=False) @API.marshal_list_with(ITEM_TAG_GET) @@ -35,8 +35,21 @@ def get(self): """ Get a list of all item tags currently in the system """ + base_query = Tag.query test_for = request.args.get('deleted', 'false') == 'true' - return Tag.query.filter(Tag.deleted == test_for).order_by(Tag.name).all() + if test_for: + base_query = base_query.filter(Tag.deleted_time != None) + else: + base_query = base_query.filter(Tag.deleted_time == None) + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((Tag.visible_for == 'all') | (Tag.visible_for == 'moderator')) + else: + base_query = base_query.filter(Tag.visible_for == 'all') + + return base_query.order_by(Tag.name).all() @jwt_required @satisfies_role(UserRole.ADMIN) @@ -55,10 +68,13 @@ def post(self): return marshal(new, ITEM_TAG_GET), 201 except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Name is not unique. %s', err) abort(409, 'Name is not unique!') + APP.logger.error('SQL Error: %s', err) abort(500) + @ANS.route('//') class ItemTagDetail(Resource): """ @@ -73,9 +89,20 @@ def get(self, tag_id): """ Get a single item tag object """ - item_tag = Tag.query.filter(Tag.id == tag_id).first() + base_query = Tag.query.filter(Tag.id == tag_id) + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((Tag.visible_for == 'all') | (Tag.visible_for == 'moderator')) + else: + base_query = base_query.filter(Tag.visible_for == 'all') + + item_tag = base_query.first() + if item_tag is None: abort(404, 'Requested item tag not found!') + return item_tag @jwt_required @@ -89,15 +116,12 @@ def delete(self, tag_id): """ item_tag = Tag.query.filter(Tag.id == tag_id).first() if item_tag is None: + APP.logger.debug('Requested item tag not found for id: %s !', tag_id) abort(404, 'Requested item tag not found!') itts = ItemToTag.query.filter(ItemToTag.tag_id == tag_id).all() items = [itt.item for itt in itts] - #Not intended -neumantm - #for itt in itts: - # DB.session.delete(itt) - for item in items: _, attributes_to_delete, _ = item.get_attribute_changes_from_tag(tag_id, True) for attr in attributes_to_delete: @@ -119,6 +143,7 @@ def post(self, tag_id): """ item_tag = Tag.query.filter(Tag.id == tag_id).first() if item_tag is None: + APP.logger.debug('Requested item tag not found for id: %s !', tag_id) abort(404, 'Requested item tag not found!') itts = ItemToTag.query.filter(ItemToTag.tag_id == tag_id).all() @@ -145,20 +170,25 @@ def put(self, tag_id): Replace a item tag object """ item_tag = Tag.query.filter(Tag.id == tag_id).first() + if item_tag is None: + APP.logger.debug('Requested item tag not found for id: %s !', tag_id) abort(404, 'Requested item tag not found!') + item_tag.update(**request.get_json()) + try: DB.session.commit() return marshal(item_tag, ITEM_TAG_GET), 200 except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Name is not unique. %s', err) abort(409, 'Name is not unique!') + APP.logger.error('SQL Error: %s', err) abort(500) - @ANS.route('//attributes/') class ItemTagAttributes(Resource): """ @@ -173,16 +203,21 @@ def get(self, tag_id): """ Get all attribute definitions for this tag. """ - if Tag.query.filter(Tag.id == tag_id).filter(Tag.deleted == False).first() is None: + base_query = Tag.query.options(joinedload('_tag_to_attribute_definitions')).filter(Tag.id == tag_id).filter(Tag.deleted_time == None) + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((Tag.visible_for == 'all') | (Tag.visible_for == 'moderator')) + else: + base_query = base_query.filter(Tag.visible_for == 'all') + + tag = base_query.first() + if tag is None: + APP.logger.debug('Requested item tag not found for id: %s !', tag_id) abort(404, 'Requested item tag not found!') - # Two possibilitys: - # return [e.attribute_definition for e in TagToAttributeDefinition.query - # .filter(TagToAttributeDefinition.tag_id == tag_id).all()] - # return [e.attribute_definition for e in Tag.query.filter(Tag.id == tag_id) - # .first()._tag_to_attribute_definitions ] - associations = TagToAttributeDefinition.query.filter(TagToAttributeDefinition.tag_id == tag_id).all() - return [e.attribute_definition for e in associations if not e.tag.deleted] + return [ttad.attribute_definition for ttad in tag._tag_to_attribute_definitions] @jwt_required @satisfies_role(UserRole.ADMIN) @@ -197,14 +232,16 @@ def post(self, tag_id): Associate a new attribute definition with the tag. """ attribute_definition_id = request.get_json()["id"] - attribute_definition = AttributeDefinition.query.filter(AttributeDefinition.id == attribute_definition_id).filter(AttributeDefinition.deleted == False).first() + attribute_definition = AttributeDefinition.query.filter(AttributeDefinition.id == attribute_definition_id).filter(AttributeDefinition.deleted_time == None).first() - if Tag.query.filter(Tag.id == tag_id).filter(Tag.deleted == False).first() is None: + if Tag.query.filter(Tag.id == tag_id).filter(Tag.deleted_time == None).first() is None: + APP.logger.debug('Requested item tag not found for id: %s !', tag_id) abort(404, 'Requested item tag not found!') if attribute_definition is None: + APP.logger.debug('Requested attribute definition not found for id: %s !', attribute_definition_id) abort(400, 'Requested attribute definition not found!') - items = [itt.item for itt in ItemToTag.query.filter(ItemToTag.tag_id == tag_id).all()] + items = [itt.item for itt in ItemToTag.query.filter(ItemToTag.tag_id == tag_id).options(joinedload('item')).all()] new = TagToAttributeDefinition(tag_id, attribute_definition_id) try: @@ -219,8 +256,10 @@ def post(self, tag_id): return [e.attribute_definition for e in associations] except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Attribute definition is already asociated with this tag! %s', err) abort(409, 'Attribute definition is already asociated with this tag!') + APP.logger.error('SQL Error: %s', err) abort(500) @jwt_required @@ -235,8 +274,9 @@ def delete(self, tag_id): Remove association of a attribute definition with the tag. """ attribute_definition_id = request.get_json()["id"] - tag = Tag.query.filter(Tag.id == tag_id).filter(Tag.deleted == False).first() + tag = Tag.query.filter(Tag.id == tag_id).filter(Tag.deleted_time == None).first() if tag is None: + APP.logger.debug('Requested item tag not found for id: %s !', tag_id) abort(404, 'Requested item tag not found!') code, msg, commit = tag.unassociate_attr_def(attribute_definition_id) @@ -246,4 +286,5 @@ def delete(self, tag_id): if code == 204: return '', 204 + APP.logger.debug('Error: %s, %s', code, msg) abort(code, msg) diff --git a/total_tolles_ferleihsystem/api/catalog/item_type.py b/total_tolles_ferleihsystem/api/catalog/item_type.py index 0835bd2..c491970 100644 --- a/total_tolles_ferleihsystem/api/catalog/item_type.py +++ b/total_tolles_ferleihsystem/api/catalog/item_type.py @@ -5,21 +5,22 @@ from flask import request from flask_restplus import Resource, abort, marshal from flask_jwt_extended import jwt_required, get_jwt_claims +from sqlalchemy.orm import joinedload from sqlalchemy.exc import IntegrityError from .. import API, satisfies_role from ..models import ITEM_TYPE_GET, ITEM_TYPE_POST, ATTRIBUTE_DEFINITION_GET, ID, ITEM_TYPE_PUT -from ... import DB +from ... import DB, APP from ...login import UserRole from ...db_models.attributeDefinition import AttributeDefinition from ...db_models.itemType import ItemType, ItemTypeToAttributeDefinition, ItemTypeToItemType from ...db_models.item import Item + PATH: str = '/catalog/item_types' ANS = API.namespace('item_type', description='ItemTypes', path=PATH) - @ANS.route('/') class ItemTypeList(Resource): """ @@ -34,9 +35,13 @@ def get(self): """ Get a list of all item types currently in the system """ - test_for = request.args.get('deleted', 'false') == 'true' base_query = ItemType.query - + test_for = request.args.get('deleted', 'false') == 'true' + if test_for: + base_query = base_query.filter(ItemType.deleted_time != None) + else: + base_query = base_query.filter(ItemType.deleted_time == None) + # auth check if UserRole(get_jwt_claims()) != UserRole.ADMIN: if UserRole(get_jwt_claims()) == UserRole.MODERATOR: @@ -44,7 +49,7 @@ def get(self): else: base_query = base_query.filter(ItemType.visible_for == 'all') - return base_query.filter(ItemType.deleted == test_for).order_by(ItemType.name).all() + return base_query.order_by(ItemType.name).all() @jwt_required @satisfies_role(UserRole.ADMIN) @@ -57,14 +62,17 @@ def post(self): Add a new item type to the system """ new = ItemType(**request.get_json()) + try: DB.session.add(new) DB.session.commit() return marshal(new, ITEM_TYPE_GET), 201 except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Name is not unique. %s', err) abort(409, 'Name is not unique!') + APP.logger.error('SQL Error, %s', err) abort(500) @ANS.route('//') @@ -81,7 +89,7 @@ def get(self, type_id): """ Get a single item type object """ - base_query = ItemType.query + base_query = ItemType.query.filter(ItemType.id == type_id) # auth check if UserRole(get_jwt_claims()) != UserRole.ADMIN: @@ -90,9 +98,12 @@ def get(self, type_id): else: base_query = base_query.filter(ItemType.visible_for == 'all') - item_type = base_query.filter(ItemType.id == type_id).first() + item_type = base_query.first() + if item_type is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(404, 'Requested item type not found!') + return item_type @jwt_required @@ -105,17 +116,13 @@ def delete(self, type_id): Delete a item type object """ item_type = ItemType.query.filter(ItemType.id == type_id).first() + if item_type is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(404, 'Requested item type not found!') item_type.deleted = True - # Not intended -neumantm - # for element in item_type._contained_item_types: - # DB.session.delete(element) - # for element in item_type._item_type_to_attribute_definitions: - # DB.session.delete(element) - items = Item.query.filter(Item.type_id == type_id).all() for item in items: code, msg, commit = item.delete() @@ -135,9 +142,13 @@ def post(self, type_id): Undelete a item type object """ item_type = ItemType.query.filter(ItemType.id == type_id).first() + if item_type is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(404, 'Requested item type not found!') + item_type.deleted = False + DB.session.commit() return "", 204 @@ -152,16 +163,22 @@ def put(self, type_id): Replace a item type object """ item_type = ItemType.query.filter(ItemType.id == type_id).first() + if item_type is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(404, 'Requested item type not found!') + item_type.update(**request.get_json()) + try: DB.session.commit() return marshal(item_type, ITEM_TYPE_GET), 200 except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Name is not unique. %s', err) abort(409, 'Name is not unique!') + APP.logger.error('SQL Error %s', err) abort(500) @@ -179,14 +196,22 @@ def get(self, type_id): """ Get all attribute definitions for this item type. """ - if ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted == False).first() is None: + base_query = ItemType.query.options(joinedload('_item_type_to_attribute_definitions')).filter(ItemType.id == type_id).filter(ItemType.deleted_time == None) + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((ItemType.visible_for == 'all') | (ItemType.visible_for == 'moderator')) + else: + base_query = base_query.filter(ItemType.visible_for == 'all') + + item_type = base_query.first() + + if item_type is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(404, 'Requested item type not found!') - associations = (ItemTypeToAttributeDefinition - .query - .filter(ItemTypeToAttributeDefinition.item_type_id == type_id) - .all()) - return [element.attribute_definition for element in associations if not element.item_type.deleted] + return [ittad.attribute_definition for ittad in item_type._item_type_to_attribute_definitions] @jwt_required @satisfies_role(UserRole.ADMIN) @@ -201,16 +226,19 @@ def post(self, type_id): Associate a new attribute definition with the item type. """ attribute_definition_id = request.get_json()["id"] - attribute_definition = AttributeDefinition.query.filter(AttributeDefinition.id == attribute_definition_id).filter(AttributeDefinition.deleted == False).first() + # pylint: disable=C0121 + attribute_definition = AttributeDefinition.query.filter(AttributeDefinition.id == attribute_definition_id).filter(AttributeDefinition.deleted_time == None).first() - if ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted == False).first() is None: + if ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted_time == None).first() is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(404, 'Requested item type not found!') if attribute_definition is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(400, 'Requested attribute definition not found!') items = Item.query.filter(Item.type_id == type_id).all() - new = ItemTypeToAttributeDefinition(type_id, attribute_definition_id) + try: DB.session.add(new) for item in items: @@ -226,8 +254,10 @@ def post(self, type_id): return [e.attribute_definition for e in associations] except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Attribute definition is already asociated with item type! %s', err) abort(409, 'Attribute definition is already asociated with item type!') + APP.logger.error('SQL Error %s', err) abort(500) @jwt_required @@ -242,8 +272,10 @@ def delete(self, type_id): Remove association of a attribute definition with the item type. """ attribute_definition_id = request.get_json()["id"] - item_type = ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted == False).first() + item_type = ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted_time == None).first() + if item_type is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(404, 'Requested item type not found!') code, msg, commit = item_type.unassociate_attr_def(attribute_definition_id) @@ -254,11 +286,12 @@ def delete(self, type_id): if code == 204: return '', 204 + APP.logger.error("Error. %s, %s", code, msg) abort(code, msg) -@ANS.route('//can_contain/') -class ItemTypeCanContainTypes(Resource): +@ANS.route('//contained_types/') +class ItemTypeContainedTypes(Resource): """ The item types that a item of this type can contain. """ @@ -271,7 +304,7 @@ def get(self, type_id): """ Get all item types, this item_type may contain. """ - base_query = ItemType.query + base_query = ItemType.query.options(joinedload('_contained_item_types').joinedload('item_type')).filter(ItemType.id == type_id).filter(ItemType.deleted_time == None) # auth check if UserRole(get_jwt_claims()) != UserRole.ADMIN: @@ -280,11 +313,12 @@ def get(self, type_id): else: base_query = base_query.filter(ItemType.visible_for == 'all') - if base_query.filter(ItemType.id == type_id).filter(ItemType.deleted == False).first() is None: + item_type = base_query.first() + if item_type is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(404, 'Requested item type not found!') - associations = ItemTypeToItemType.query.filter(ItemTypeToItemType.parent_id == type_id).all() - return [e.item_type for e in associations if not e.parent.deleted] + return [cit.item_type for cit in item_type._contained_item_types] @jwt_required @satisfies_role(UserRole.ADMIN) @@ -300,22 +334,25 @@ def post(self, type_id): """ child_id = request.get_json()["id"] - - if ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted == False).first() is None: + if ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted_time == None).first() is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(404, 'Requested item type not found!') - if ItemType.query.filter(ItemType.id == child_id).filter(ItemType.deleted == False).first() is None: - abort(400, 'Requested attribute definition not found!') + if ItemType.query.filter(ItemType.id == child_id).filter(ItemType.deleted_time == None).first() is None: + APP.logger.debug('Requested contained type (id: %s) not found!', child_id) + abort(400, 'Requested contained type not found!') new = ItemTypeToItemType(type_id, child_id) try: DB.session.add(new) DB.session.commit() - associations = ItemTypeToItemType.query.filter(ItemTypeToItemType.parent_id == type_id).all() + associations = ItemTypeToItemType.query.filter(ItemTypeToItemType.parent_id == type_id).options(joinedload('item_type')).all() return [e.item_type for e in associations] except IntegrityError as err: message = str(err) - if 'UNIQUE constraint failed' in message: + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('Item type can already be contained in this item type. %s', err) abort(409, 'Item type can already be contained in this item type.') + APP.logger.error('SQL Error %s', err) abort(500) @jwt_required @@ -331,22 +368,121 @@ def delete(self, type_id): """ child_id = request.get_json()["id"] - - if ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted == False).first() is None: + if ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted_time == None).first() is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) abort(404, 'Requested item type not found!') - if ItemType.query.filter(ItemType.id == child_id).filter(ItemType.deleted == False).first() is None: - abort(400, 'Requested attribute definition not found!') + if ItemType.query.filter(ItemType.id == child_id).filter(ItemType.deleted_time == None).first() is None: + APP.logger.debug('Requested contained type (id: %s) not found!', child_id) + abort(400, 'Requested contained type not found!') association = (ItemTypeToItemType .query .filter(ItemTypeToItemType.parent_id == type_id) .filter(ItemTypeToItemType.item_type_id == child_id) .first()) + if association is None: return '', 204 + + DB.session.delete(association) + DB.session.commit() + return '', 204 + +@ANS.route('//parent_types/') +class ItemTypeParentTypes(Resource): + """ + The item types that a item of this type can be contained by. + """ + + @jwt_required + @ANS.response(404, 'Requested item type not found!') + @API.marshal_with(ITEM_TYPE_GET) + # pylint: disable=R0201 + def get(self, type_id): + """ + Get all item types, this item_type may be contained in. + """ + base_query = ItemType.query.options(joinedload('_possible_parent_item_types').joinedload('parent')).filter(ItemType.id == type_id).filter(ItemType.deleted_time == None) + + # auth check + if UserRole(get_jwt_claims()) != UserRole.ADMIN: + if UserRole(get_jwt_claims()) == UserRole.MODERATOR: + base_query = base_query.filter((ItemType.visible_for == 'all') | (ItemType.visible_for == 'moderator')) + else: + base_query = base_query.filter(ItemType.visible_for == 'all') + + item_type = base_query.first() + if item_type is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) + abort(404, 'Requested item type not found!') + + return [ppit.parent for ppit in item_type._possible_parent_item_types] + + @jwt_required + @satisfies_role(UserRole.ADMIN) + @ANS.doc(body=ID) + @ANS.response(404, 'Requested item type not found!') + @ANS.response(400, 'Requested parent item type not found!') + @ANS.response(409, 'Item type can already be contained in this item type.') + @API.marshal_with(ITEM_TYPE_GET) + # pylint: disable=R0201 + def post(self, type_id): + """ + Add new item type which can contain this item type. + """ + parent_id = request.get_json()["id"] + + if ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted_time == None).first() is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) + abort(404, 'Requested item type not found!') + if ItemType.query.filter(ItemType.id == parent_id).filter(ItemType.deleted_time == None).first() is None: + APP.logger.debug('Requested parent type (id: %s) not found!', parent_id) + abort(400, 'Requested parent type not found!') + + new = ItemTypeToItemType(parent_id, type_id) + try: - DB.session.delete(association) + DB.session.add(new) DB.session.commit() - return '', 204 - except IntegrityError: + associations = ItemTypeToItemType.query.filter(ItemTypeToItemType.parent_id == type_id).options(joinedload('item_type')).all() + return [e.item_type for e in associations] + except IntegrityError as err: + message = str(err) + if APP.config['DB_UNIQUE_CONSTRAIN_FAIL'] in message: + APP.logger.info('This item type can already contain the given item type. %s', err) + abort(409, 'This item type can already contain the given item type.') + APP.logger.error('SQL Error %s', err) abort(500) + + @jwt_required + @satisfies_role(UserRole.ADMIN) + @ANS.doc(body=ID) + @ANS.response(404, 'Requested item type not found!') + @ANS.response(400, 'Requested child item type not found!') + @ANS.response(204, 'Success.') + # pylint: disable=R0201 + def delete(self, type_id): + """ + Remove item type which can contain this item type + """ + parent_id = request.get_json()["id"] + + if ItemType.query.filter(ItemType.id == type_id).filter(ItemType.deleted_time == None).first() is None: + APP.logger.debug('Requested item type (id: %s) not found!', type_id) + abort(404, 'Requested item type not found!') + if ItemType.query.filter(ItemType.id == parent_id).filter(ItemType.deleted_time == None).first() is None: + APP.logger.debug('Requested parent type (id: %s) not found!', parent_id) + abort(400, 'Requested parent type not found!') + + association = (ItemTypeToItemType + .query + .filter(ItemTypeToItemType.parent_id == type_id) + .filter(ItemTypeToItemType.item_type_id == parent_id) + .first()) + + if association is None: + return '', 204 + + DB.session.delete(association) + DB.session.commit() + return '', 204 diff --git a/total_tolles_ferleihsystem/api/lending.py b/total_tolles_ferleihsystem/api/lending.py index 2c5ec51..6e318a3 100644 --- a/total_tolles_ferleihsystem/api/lending.py +++ b/total_tolles_ferleihsystem/api/lending.py @@ -7,13 +7,14 @@ from flask_restplus import Resource, abort, marshal from flask_jwt_extended import jwt_required from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import joinedload from . import API, satisfies_role from .. import DB from .models import LENDING_GET, LENDING_POST, LENDING_PUT, ID_LIST from ..login import UserRole -from ..db_models.item import Lending, ItemToLending, Item +from ..db_models.item import Lending, Item PATH: str = '/lending' ANS = API.namespace('lending', description='Lendings', path=PATH) @@ -21,23 +22,18 @@ @ANS.route('/') class LendingList(Resource): """ - Lendings root item tag + List of all active lendings """ @jwt_required - @API.param('active', 'get only active lendings', type=bool, required=False, default=True) @API.marshal_list_with(LENDING_GET) # pylint: disable=R0201 def get(self): """ Get a list of all lendings currently in the system """ - active = request.args.get('active', 'true') == 'true' - base_query = Lending.query - if active: - base_query = base_query.join(ItemToLending).distinct() - return base_query.all() + return Lending.query.options(joinedload('_items')).all() @jwt_required @satisfies_role(UserRole.MODERATOR) @@ -51,35 +47,14 @@ def post(self): """ Add a new lending to the system """ - json = request.get_json() - item_ids = json.pop('item_ids') - items = [] - item_to_lendings = [] - - for element in item_ids: - item = Item.query.filter(Item.id == element).first() - if item is None: - abort(400, "Item not found:" + str(element)) - if not item.type.lendable: - abort(400, "Item not lendable:" + str(element)) - if item.is_currently_lent: - abort(400, "Item already lent:" + str(element)) - items.append(item) - - new = Lending(**json) try: - DB.session.add(new) - DB.session.commit() - for element in items: - item_to_lendings.append(ItemToLending(element, new)) - DB.session.add_all(item_to_lendings) - DB.session.commit() - return marshal(new, LENDING_GET), 201 - except IntegrityError as err: - message = str(err) - if 'UNIQUE constraint failed' in message: - abort(409, 'Name is not unique!') - abort(500) + new = Lending(** request.get_json()) + except ValueError as err: + abort(400, str(err)) + + DB.session.add(new) + DB.session.commit() + return marshal(new, LENDING_GET), 201 @ANS.route('//') class LendingDetail(Resource): @@ -96,7 +71,7 @@ def get(self, lending_id): """ Get a single lending object """ - lending = Lending.query.filter(Lending.id == lending_id).first() + lending = Lending.query.filter(Lending.id == lending_id).options(joinedload('_items')).first() if lending is None: abort(404, 'Requested lending not found!') return lending @@ -113,6 +88,7 @@ def delete(self, lending_id): lending = Lending.query.filter(Lending.id == lending_id).first() if lending is None: abort(404, 'Requested lending not found!') + lending.pre_delete() DB.session.delete(lending) DB.session.commit() return "", 204 @@ -120,71 +96,48 @@ def delete(self, lending_id): @jwt_required @satisfies_role(UserRole.MODERATOR) @ANS.doc(model=LENDING_GET, body=LENDING_PUT) - @ANS.response(409, 'Name is not Unique.') @ANS.response(404, 'Requested lending not found!') + @ANS.response(400, "Item not found") + @ANS.response(400, "Item not lendable") + @ANS.response(400, "Item already lent") # pylint: disable=R0201 def put(self, lending_id): """ Replace a lending object """ - lending = Lending.query.filter(Lending.id == lending_id).first() + lending = Lending.query.filter(Lending.id == lending_id).options(joinedload('_items')).first() if lending is None: abort(404, 'Requested lending not found!') - - json = request.get_json() - item_ids = json.pop('item_ids') - items = [] - item_to_lendings = [] - - for element in item_ids: - item = Item.query.filter(Item.id == element).first() - if item is None: - abort(400, "Item not found:" + str(element)) - if not item.type.lendable: - abort(400, "Item not lendable:" + str(element)) - if item.is_currently_lent: - abort(400, "Item already lent:" + str(element)) - items.append(item) - - lending.update(**request.get_json()) try: - DB.session.commit() - for element in ItemToLending.query.filter(ItemToLending.lending_id == lending_id).all(): - DB.session.delete(element) - for element in items: - item_to_lendings.append(ItemToLending(element, lending)) - DB.session.add_all(item_to_lendings) - DB.session.commit() - return marshal(lending, LENDING_GET), 200 - except IntegrityError as err: - message = str(err) - if 'UNIQUE constraint failed' in message: - abort(409, 'Name is not unique!') - abort(500) + lending.update(**request.get_json()) + except ValueError as err: + abort(400, str(err)) + + DB.session.commit() + return marshal(lending, LENDING_GET), 200 @jwt_required @satisfies_role(UserRole.MODERATOR) @ANS.doc(body=ID_LIST) @ANS.response(404, 'Requested lending not found!') - @ANS.response(400, 'Requested item is not part of this lending.') - @API.marshal_with(LENDING_GET) + @ANS.response(400, "Item not found") + @ANS.response(201, "Lending would be empty. Was deleted.") # pylint: disable=R0201 def post(self, lending_id): """ Give back a list of items. """ - lending = Lending.query.filter(Lending.id == lending_id).first() + lending = Lending.query.filter(Lending.id == lending_id).options(joinedload('_items')).first() if lending is None: abort(404, 'Requested lending not found!') - - ids = request.get_json()["ids"] try: - for element in ids: - to_delete = ItemToLending.query.filter(ItemToLending.item_id == element).first() - if to_delete is None: - abort(400, "Requested item is not part of this lending:" + str(element)) - DB.session.delete(to_delete) + lending.remove_items(request.get_json()["ids"]) + except ValueError as err: + abort(400, str(err)) + DB.session.commit() + if len(lending._items) <= 0: + lending.pre_delete() + DB.session.delete(lending) DB.session.commit() - return lending - except IntegrityError: - abort(500) + return None, 201 + return marshal(lending, LENDING_GET) diff --git a/total_tolles_ferleihsystem/api/models.py b/total_tolles_ferleihsystem/api/models.py index 6e26fee..9ba96f9 100644 --- a/total_tolles_ferleihsystem/api/models.py +++ b/total_tolles_ferleihsystem/api/models.py @@ -7,15 +7,33 @@ from ..hal_field import HaLUrl, UrlData, NestedFields from ..db_models import STD_STRING_SIZE + +# +# --- Helper Models --- +# These modules are here to provide some consistency across all models. +# + WITH_CURIES = API.model('WithCuries', { 'curies': HaLUrl(UrlData('api.doc', templated=True, hashtag='!{rel}', name='rel')), }) ID = API.model('Id', { - 'id': fields.Integer(min=1, example=1, readonly=True), + 'id': fields.Integer(min=1, example=1, readonly=True, title='Internal Identifier'), +}) +ID_LIST = API.model('IdList', { + 'ids': fields.List(fields.Integer(min=1, example=1)), +}) + +VISIBLE_FOR = API.model('VisibleFor', { + 'visible_for': fields.String(enum=('all', 'moderator', 'administrator'), max_length=STD_STRING_SIZE, title='Access Rights'), }) + +# +# --- Root --- +# + ROOT_LINKS = API.inherit('RootLinks', WITH_CURIES, { 'self': HaLUrl(UrlData('api.default_root_resource')), 'auth': HaLUrl(UrlData('api.auth_authentication_routes')), @@ -25,10 +43,16 @@ 'spec': HaLUrl(UrlData('api.specs')), 'lending': HaLUrl(UrlData('api.lending_lending_list')), }) + ROOT_MODEL = API.model('RootModel', { '_links': NestedFields(ROOT_LINKS), }) + +# +# --- Authentication Routes --- +# + AUTHENTICATION_ROUTES_LINKS = API.inherit('AuthenticationRoutesLinks', WITH_CURIES, { 'self': HaLUrl(UrlData('api.auth_authentication_routes')), 'login': HaLUrl(UrlData('api.auth_login')), @@ -38,10 +62,16 @@ 'check': HaLUrl(UrlData('api.auth_check')), 'settings': HaLUrl(UrlData('api.auth_settings_resource')), }) + AUTHENTICATION_ROUTES_MODEL = API.model('AuthenticationRoutesModel', { '_links': NestedFields(AUTHENTICATION_ROUTES_LINKS), }) + +# +# --- Catalog --- +# + CATALOG_LINKS = API.inherit('CatalogLinks', WITH_CURIES, { 'self': HaLUrl(UrlData('api.default_catalog_resource')), 'items': HaLUrl(UrlData('api.item_item_list')), @@ -50,76 +80,67 @@ 'files': HaLUrl(UrlData('api.file_file_list')), 'attribute_definitions': HaLUrl(UrlData('api.attribute_definition_attribute_definition_list')), }) + CATALOG_MODEL = API.model('CatalogModel', { '_links': NestedFields(CATALOG_LINKS), }) -ITEM_TYPE_LINKS = API.inherit('ItemTypeLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.item_type_item_type_detail', url_data={'type_id': 'id'}), - required=False), - 'attributes': HaLUrl(UrlData('api.item_type_item_type_attributes', url_data={'type_id' : 'id'})), - 'can_contain': HaLUrl(UrlData('api.item_type_item_type_can_contain_types', - url_data={'type_id' : 'id'})), -}) -ITEM_TYPE_LIST_LINKS = API.inherit('ItemTypeLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.item_type_item_type_list')), -}) +# +# --- Item Type --- +# -ITEM_TYPE_POST = API.model('ItemTypePOST', { +ITEM_TYPE_BASIC = API.inherit('ItemTypeBasic', VISIBLE_FOR, { 'name': fields.String(max_length=STD_STRING_SIZE, title='Name'), 'name_schema': fields.String(max_length=STD_STRING_SIZE, title='Name Schema'), + 'lendable': fields.Boolean(default=True), 'lending_duration': fields.Integer(title='Lending Duration'), - 'visible_for': fields.String(enum=('all', 'moderator', 'administrator'), title='Access Rights'), 'how_to': fields.String(nullable=True, title='How to'), }) - -ITEM_TYPE_PUT = API.inherit('ItemTypePUT', ITEM_TYPE_POST, { - 'lendable': fields.Boolean(default=True), +ITEM_TYPE_LINKS = API.inherit('ItemTypeLinks', WITH_CURIES, { + 'self': HaLUrl(UrlData('api.item_type_item_type_detail', url_data={'type_id': 'id'})), + 'attributes': HaLUrl(UrlData('api.item_type_item_type_attributes', url_data={'type_id' : 'id'})), + 'parent_types': HaLUrl(UrlData('api.item_type_item_type_parent_types', + url_data={'type_id' : 'id'})), + 'contained_types': HaLUrl(UrlData('api.item_type_item_type_contained_types', + url_data={'type_id' : 'id'})), }) -ITEM_TYPE_GET = API.inherit('ItemType', ITEM_TYPE_PUT, ID, { +ITEM_TYPE_POST = API.inherit('ItemTypePOST', ITEM_TYPE_BASIC, {}) +ITEM_TYPE_PUT = API.inherit('ItemTypePUT', ITEM_TYPE_BASIC, {}) +ITEM_TYPE_GET = API.inherit('ItemTypeGET', ITEM_TYPE_BASIC, ID, { 'deleted': fields.Boolean(readonly=True), '_links': NestedFields(ITEM_TYPE_LINKS), }) -ITEM_TAG_LINKS = API.inherit('ItemTagLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.item_tag_item_tag_detail', url_data={'tag_id' : 'id'}), - required=False), - 'attributes': HaLUrl(UrlData('api.item_tag_item_tag_attributes', url_data={'tag_id' : 'id'})), -}) -ITEM_TAG_LIST_LINKS = API.inherit('ItemTagLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.item_tag_item_tag_list')), -}) +# +# --- Item Tag --- +# -ITEM_TAG_POST = API.model('ItemTagPOST', { +ITEM_TAG_BASIC = API.inherit('ItemTagBasic', VISIBLE_FOR, { 'name': fields.String(max_length=STD_STRING_SIZE, title='Name'), 'lending_duration': fields.Integer(nullable=True, title='Lending Duration'), - 'visible_for': fields.String(enum=('all', 'moderator', 'administrator'), max_length=STD_STRING_SIZE, - title='Access Rights'), }) - -ITEM_TAG_PUT = API.inherit('ItemTagPUT', ITEM_TAG_POST, { +ITEM_TAG_LINKS = API.inherit('ItemTagLinks', WITH_CURIES, { + 'self': HaLUrl(UrlData('api.item_tag_item_tag_detail', url_data={'tag_id' : 'id'}), + required=False), + 'attributes': HaLUrl(UrlData('api.item_tag_item_tag_attributes', url_data={'tag_id' : 'id'})), }) -ITEM_TAG_GET = API.inherit('ItemTagGET', ITEM_TAG_PUT, ID, { +ITEM_TAG_POST = API.inherit('ItemTagPOST', ITEM_TAG_BASIC, {}) +ITEM_TAG_PUT = API.inherit('ItemTagPUT', ITEM_TAG_BASIC, {}) +ITEM_TAG_GET = API.inherit('ItemTagGET', ITEM_TAG_BASIC, ID, { 'deleted': fields.Boolean(readonly=True), '_links': NestedFields(ITEM_TAG_LINKS), }) -ATTRIBUTE_DEFINITION_LINKS = API.inherit('AttributeDefinitionLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.attribute_definition_attribute_definition_detail', - url_data={'definition_id' : 'id'}), required=False), - 'autocomplete': HaLUrl(UrlData('api.attribute_definition_attribute_definition_values', - url_data={'definition_id' : 'id'}), required=False), -}) -ATTRIBUTE_DEFINITION_LIST_LINKS = API.inherit('AttributeDefinitionLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.attribute_definition_attribute_definition_list')), -}) +# +# --- Attribute Definition --- +# -ATTRIBUTE_DEFINITION_POST = API.model('AttributeDefinitionPOST', { +ATTRIBUTE_DEFINITION_BASIC = API.inherit('AttributeDefinitionBasic', VISIBLE_FOR, { 'name': fields.String(max_length=STD_STRING_SIZE), 'type': fields.String(enum=('string', 'integer', 'number', 'boolean'), max_length=STD_STRING_SIZE), 'jsonschema': fields.String(nullable=True, default='{\n \n}', valueType='json', description="""Subset of jsonschema v4 @@ -129,13 +150,17 @@ * string: "minLength", "maxLength", "pattern", "enum", "format" ["date"|"date-time"] * integer: "minimum", "maximum" * float: "minimum", "maxinum" """), - 'visible_for': fields.String(enum=('all', 'moderator', 'administrator'), max_length=STD_STRING_SIZE), }) - -ATTRIBUTE_DEFINITION_PUT = API.inherit('AttributeDefinitionPUT', ATTRIBUTE_DEFINITION_POST, { +ATTRIBUTE_DEFINITION_LINKS = API.inherit('AttributeDefinitionLinks', WITH_CURIES, { + 'self': HaLUrl(UrlData('api.attribute_definition_attribute_definition_detail', + url_data={'definition_id' : 'id'}), required=False), + 'autocomplete': HaLUrl(UrlData('api.attribute_definition_attribute_definition_values', + url_data={'definition_id' : 'id'}), required=False), }) -ATTRIBUTE_DEFINITION_GET = API.inherit('AttributeDefinitionGET', ATTRIBUTE_DEFINITION_PUT, ID, { +ATTRIBUTE_DEFINITION_POST = API.inherit('AttributeDefinitionPOST', ATTRIBUTE_DEFINITION_BASIC, {}) +ATTRIBUTE_DEFINITION_PUT = API.inherit('AttributeDefinitionPUT', ATTRIBUTE_DEFINITION_BASIC, {}) +ATTRIBUTE_DEFINITION_GET = API.inherit('AttributeDefinitionGET', ATTRIBUTE_DEFINITION_BASIC, ID, { 'deleted': fields.Boolean(readonly=True), '_links': NestedFields(ATTRIBUTE_DEFINITION_LINKS), }) @@ -147,118 +172,132 @@ } }) -ID_LIST = API.model('IdList', { - 'ids': fields.List(fields.Integer(min=1, example=1)), -}) - -ITEM_LINKS = API.inherit('ItemLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.item_item_detail', url_data={'item_id' : 'id'}), required=False), - 'tags': HaLUrl(UrlData('api.item_item_item_tags', url_data={'item_id' : 'id'})), - 'attributes': HaLUrl(UrlData('api.item_item_attribute_list', url_data={'item_id' : 'id'})), - 'contained_items': HaLUrl(UrlData('api.item_item_contained_items', url_data={'item_id' : 'id'})), - 'files': HaLUrl(UrlData('api.item_item_file', url_data={'item_id' : 'id'})) -}) -ITEM_LIST_LINKS = API.inherit('ItemLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.item_item_list')), -}) +# +# --- Item --- +# -ITEM_POST = API.model('ItemPOST', { +ITEM_BASIC = API.inherit('ItemBasic', VISIBLE_FOR, { 'name': fields.String(max_length=STD_STRING_SIZE, title='Name'), 'update_name_from_schema': fields.Boolean(default=True, title='Schema Name'), 'type_id': fields.Integer(min=1, title='Type'), 'lending_duration': fields.Integer(nullable=True, title='Lending Duration'), - 'visible_for': fields.String(enum=('all', 'moderator', 'administrator'), max_length=STD_STRING_SIZE, - title='Access Rights'), }) - -ITEM_PUT = API.inherit('ItemPUT', ITEM_POST, { +ITEM_LINKS = API.inherit('ItemLinks', WITH_CURIES, { + 'self': HaLUrl(UrlData('api.item_item_detail', url_data={'item_id' : 'id'}), required=False), + 'item_type': HaLUrl(UrlData('api.item_type_item_type_detail', url_data={'type_id': 'type_id'})), + 'tags': HaLUrl(UrlData('api.item_item_item_tags', url_data={'item_id' : 'id'})), + 'attributes': HaLUrl(UrlData('api.item_item_attribute_list', url_data={'item_id' : 'id'})), + 'parent_items': HaLUrl(UrlData('api.item_item_parent_items', url_data={'item_id' : 'id'})), + 'contained_items': HaLUrl(UrlData('api.item_item_contained_items', url_data={'item_id' : 'id'})), + 'files': HaLUrl(UrlData('api.item_item_file', url_data={'item_id' : 'id'})), + 'lendings': HaLUrl(UrlData('api.item_item_lendings', url_data={'item_id' : 'id'})), }) -ITEM_GET = API.inherit('ItemGET', ITEM_PUT, ID, { +ITEM_POST = API.inherit('ItemPOST', ITEM_BASIC, {}) +ITEM_PUT = API.inherit('ItemPUT', ITEM_BASIC, {}) +ITEM_GET = API.inherit('ItemGET', ITEM_BASIC, ID, { 'deleted': fields.Boolean(readonly=True), - 'type': fields.Nested(ITEM_TYPE_GET), 'is_currently_lent': fields.Boolean(readonly=True), + 'lending_id': fields.Integer(min=1, title='Lending'), 'effective_lending_duration': fields.Integer(readonly=True), - 'lending_id': fields.Integer(readonly=True), - 'due': fields.DateTime(attribute='item_lending.due', readonly=True), - '_links': NestedFields(ITEM_LINKS) -}) - -ITEM_GET_WITH_PARENTS = API.inherit('ItemGET_PARENTS', ITEM_GET, { - 'parent': fields.Nested(ITEM_GET, attribute='parent') + 'due': fields.Integer(readonly=True, title="Due date", description="[Unix time]"), + '_links': NestedFields(ITEM_LINKS), }) +# +# --- Attribute --- +# +ATTRIBUTE_BASIC = API.model('AttributeBasic', { + 'value': fields.String(max_length=STD_STRING_SIZE), +}) ATTRIBUTE_LINKS = API.inherit('AttributeLinks', WITH_CURIES, { 'self': HaLUrl(UrlData('api.item_item_attribute_detail', url_data={'item_id': 'item_id', 'attribute_definition_id': 'attribute_definition_id'}), required=False), }) -ATTRIBUTE_LIST_LINKS = API.inherit('AttributeLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.item_item_attribute_list')), -}) - -ATTRIBUTE_PUT = API.model('AttributePUT', { - 'value': fields.String(max_length=STD_STRING_SIZE), -}) - -ATTRIBUTE_GET = API.inherit('AttributeGET', ATTRIBUTE_PUT, { +ATTRIBUTE_PUT = API.inherit('AttributePUT', ATTRIBUTE_BASIC, {}) +ATTRIBUTE_GET = API.inherit('AttributeGET', ATTRIBUTE_BASIC, { 'attribute_definition_id': fields.Integer(), 'attribute_definition': fields.Nested(ATTRIBUTE_DEFINITION_GET), '_links': NestedFields(ATTRIBUTE_LINKS) }) -FILE_LINKS = API.inherit('FileLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.file_file_detail', - url_data={'file_id': 'id'}), required=False), - 'download': HaLUrl(UrlData('api.file_file_data', - url_data={'file_hash': 'file_hash'}), required=False), -}) -FILE_BASIC = API.model('FileBASIC', { +# +# --- File --- +# + +FILE_BASIC = API.inherit('FileBASIC', VISIBLE_FOR, { 'name': fields.String(max_length=STD_STRING_SIZE, nullable=True), 'file_type': fields.String(max_length=20), - 'invalidation': fields.DateTime(nullable=True), - 'visible_for': fields.String(enum=('all', 'moderator', 'administrator'), title='Access Rights'), + 'invalidation': fields.Integer(nullable=True, title="Invalidation date", description="[Unix time]"), +}) +FILE_LINKS = API.inherit('FileLinks', WITH_CURIES, { + 'self': HaLUrl(UrlData('api.file_file_detail', + url_data={'file_id': 'id'})), + 'item': HaLUrl(UrlData('api.item_item_detail', url_data={'item_id': 'id'}), required=False), + 'download': HaLUrl(UrlData('api.file_file_data', + url_data={'file_id': 'file_id'}), required=False), }) +FILE_PUT = API.inherit('FilePUT', FILE_BASIC, {}) FILE_GET = API.inherit('FileGET', FILE_BASIC, ID, { 'item': fields.Nested(ITEM_GET), 'file_hash': fields.String(max_length=STD_STRING_SIZE), - 'creation': fields.DateTime(), + 'creation': fields.Integer(title="Creation date", description="[Unix time]"), '_links': NestedFields(FILE_LINKS) }) -FILE_PUT = API.inherit('FilePUT', FILE_BASIC, { -}) - -LENDING_LINKS = API.inherit('LendingLinks', WITH_CURIES, { - 'self': HaLUrl(UrlData('api.lending_lending_detail', - url_data={'lending_id' : 'id'}), required=False), -}) -ITEM_LENDING = API.model('ItemLending', { - 'due': fields.DateTime(), - 'item': fields.Nested(ITEM_GET), -}) +# +# --- Lending --- +# LENDING_BASIC = API.model('LendingBASIC', { 'moderator': fields.String(max_length=STD_STRING_SIZE), 'user': fields.String(max_length=STD_STRING_SIZE), 'deposit': fields.String(example="Studentenausweis", max_length=STD_STRING_SIZE), }) +LENDING_LINKS = API.inherit('LendingLinks', WITH_CURIES, { + 'self': HaLUrl(UrlData('api.lending_lending_detail', + url_data={'lending_id' : 'id'}), required=False), +}) LENDING_POST = API.inherit('LendingPOST', LENDING_BASIC, { 'item_ids': fields.List(fields.Integer(min=1)) }) +LENDING_PUT = API.inherit('LendingPUT', LENDING_POST, {}) +LENDING_GET = API.inherit('LendingGET', LENDING_BASIC, ID, { + '_links': NestedFields(LENDING_LINKS), + 'date': fields.Integer(title="Lending date", description="[Unix time]"), + 'items': fields.Nested(ITEM_GET, attribute='_items'), +}) + +# +# --- Blacklist --- +# -LENDING_PUT = API.inherit('LendingPUT', LENDING_POST, { +BLACKLIST_BASIC = API.model('BlacklistBasic', { + 'name': fields.String(max_length=STD_STRING_SIZE), + 'system_wide': fields.Boolean(default=False), + 'reason': fields.String(), +}) +BLACKLIST_ITEM_TYPE = API.model('BlacklistItemType', { + 'end_time': fields.Integer(title="Date of end of blacklisting", description="[Unix time]"), + 'reason': fields.String(), +}) +BLACKLIST_LINKS = API.inherit('BlacklistLinks', WITH_CURIES, { + 'self': HaLUrl(UrlData('', url_data={'id' : 'id'})), }) -LENDING_GET = API.inherit('LendingGET', LENDING_BASIC, ID, { - '_links': NestedFields(LENDING_LINKS), - 'date': fields.DateTime(), - 'itemLendings': fields.Nested(ITEM_LENDING) +BLACKLIST_POST = API.inherit('BlacklistPOST', BLACKLIST_ITEM_TYPE, { + 'item_type_id': fields.Integer(min=1, example=1, readonly=True, title='Internal Identifier'), +}) +BLACKLIST_PUT = API.inherit('BlacklistPUT', BLACKLIST_BASIC, {}) +BLACKLIST_GET = API.inherit('BlacklistGET', BLACKLIST_BASIC, ID, { + '_links': NestedFields(BLACKLIST_LINKS), + 'blocked_types': fields.Nested(BLACKLIST_ITEM_TYPE) }) diff --git a/total_tolles_ferleihsystem/api/search.py b/total_tolles_ferleihsystem/api/search.py index 6da86e5..9a7ebee 100644 --- a/total_tolles_ferleihsystem/api/search.py +++ b/total_tolles_ferleihsystem/api/search.py @@ -5,8 +5,10 @@ from flask import request from flask_restplus import Resource from flask_jwt_extended import jwt_optional, get_jwt_claims +from sqlalchemy.orm import joinedload from . import API -from ..db_models.item import Item, ItemToTag, ItemToAttributeDefinition, ItemToLending +from ..db_models.item import Item, ItemToTag, ItemToAttributeDefinition +from ..db_models.itemType import ItemType from ..db_models.tag import Tag from .models import ITEM_GET from ..login import UserRole @@ -29,6 +31,7 @@ class Search(Resource): @API.param('type', 'Only show items with the given type id', type=int, required=False, default='') @API.param('deleted', 'If true also search deleted items', type=bool, required=False, default=False) @API.param('lent', 'If true also search lent items', type=bool, required=False, default=False) + @API.param('lendable', 'If true only return lendable items', type=bool, required=False, default=False) @API.marshal_list_with(ITEM_GET) # pylint: disable=R0201 def get(self): @@ -42,9 +45,24 @@ def get(self): item_type = request.args.get('type', default=-1, type=int) deleted = request.args.get('deleted', default=False, type=lambda x: x == 'true') lent = request.args.get('lent', default=False, type=lambda x: x == 'true') + lendable = request.args.get('lendable', default=False, type=lambda x: x == 'true') - search_string = '%' + search + '%' - search_result = Item.query + def generate_keyword_search_condition(search_string_param, search_condition_param = None): + search_string_param = search_string_param.strip() + + if search_condition_param is None: + search_condition_param = Item.name.like('%' + search_string_param + '%'); + else: + search_condition_param = search_condition_param | Item.name.like('%' + search_string_param + '%'); + + if not tags: + search_condition_param = search_condition_param | Tag.name.like('%' + search_string_param + '%') + + search_condition_param = search_condition_param | ItemToAttributeDefinition.value.like('%' + search_string_param.strip() + '%') + + return search_condition_param + + search_result = Item.query.options(joinedload('lending'), joinedload("_tags")) # auth check if UserRole(get_jwt_claims()) != UserRole.ADMIN: @@ -54,22 +72,27 @@ def get(self): search_result = search_result.filter(Item.visible_for == 'all') if search: - search_condition = Item.name.like(search_string) + search_array = search.split('|') #TODO make character configurable if not tags: search_result = search_result.join(ItemToTag, isouter=True).join(Tag, isouter=True) - search_condition = search_condition | Tag.name.like(search_string) - if not attributes: - search_result = search_result.join(ItemToAttributeDefinition, isouter=True) - search_condition = search_condition | ItemToAttributeDefinition.value.like(search_string) + search_result = search_result.join(ItemToAttributeDefinition, isouter=True).filter(~ItemToAttributeDefinition.attribute_definition_id.in_([attribute.split('-', 1)[0] for attribute in attributes])) + + search_condition = generate_keyword_search_condition(search_array[0]) + for search_string in search_array[1:]: + search_condition = generate_keyword_search_condition(search_string, search_condition) search_result = search_result.filter(search_condition) if not deleted: - search_result = search_result.filter(~Item.deleted) + search_result = search_result.filter(Item.deleted_time == None) if not lent: - search_result = search_result.join(ItemToLending, isouter=True).filter(ItemToLending.lending_id.is_(None)) + search_result = search_result.filter(Item.lending == None) + + if lendable: + search_result = search_result.join(ItemType) + search_result = search_result.filter(ItemType.lendable == True) if item_type != -1: search_result = search_result.filter(Item.type_id == item_type) @@ -83,6 +106,17 @@ def get(self): search_result = search_result.join(ItemToAttributeDefinition, aliased=True) search_result = search_result.filter(ItemToAttributeDefinition.attribute_definition_id == attribute.split('-', 1)[0]) - search_result = search_result.filter(ItemToAttributeDefinition.value == attribute.split('-', 1)[1]) + + search_value = attribute.split('-', 1)[1].strip() + if search_value[:2] == '>=': + search_result = search_result.filter(ItemToAttributeDefinition.value >= search_value[2:].strip()) + elif search_value[:2] == '<=': + search_result = search_result.filter(ItemToAttributeDefinition.value <= search_value[2:].strip()) + elif search_value[:1] == '>': + search_result = search_result.filter(ItemToAttributeDefinition.value > search_value[1:].strip()) + elif search_value[:1] == '<': + search_result = search_result.filter(ItemToAttributeDefinition.value < search_value[1:].strip()) + else: + search_result = search_result.filter(ItemToAttributeDefinition.value == search_value) return search_result.order_by(Item.name).limit(limit).all() diff --git a/total_tolles_ferleihsystem/config.py b/total_tolles_ferleihsystem/config.py index 74456ce..2eae852 100644 --- a/total_tolles_ferleihsystem/config.py +++ b/total_tolles_ferleihsystem/config.py @@ -13,6 +13,7 @@ class Config(object): JWT_SECRET_KEY = ''.join(hex(randint(0, 255))[2:] for i in range(16)) SQLALCHEMY_DATABASE_URI = 'sqlite://:memory:' SQLALCHEMY_TRACK_MODIFICATIONS = False + DB_UNIQUE_CONSTRAIN_FAIL = 'UNIQUE constraint failed' WEBPACK_MANIFEST_PATH = './build/manifest.json' LOGGING = { 'version': 1, @@ -143,7 +144,9 @@ class ProductionConfig(Config): class DebugConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/test.db' + LONG_REQUEST_THRESHHOLD = 0 JWT_SECRET_KEY = 'debug' + JWT_ACCESS_TOKEN_EXPIRES = False LOGIN_PROVIDERS = ['Debug'] Config.LOGGING['loggers']['flask.app.auth']['level'] = logging.DEBUG Config.LOGGING['loggers']['flask.app.db']['level'] = logging.DEBUG diff --git a/total_tolles_ferleihsystem/db_models/attributeDefinition.py b/total_tolles_ferleihsystem/db_models/attributeDefinition.py index 98b1cbb..15bc285 100644 --- a/total_tolles_ferleihsystem/db_models/attributeDefinition.py +++ b/total_tolles_ferleihsystem/db_models/attributeDefinition.py @@ -12,7 +12,7 @@ class AttributeDefinition (DB.Model): type = DB.Column(DB.String(STD_STRING_SIZE)) jsonschema = DB.Column(DB.Text) visible_for = DB.Column(DB.String(STD_STRING_SIZE)) - deleted = DB.Column(DB.Boolean, default=False) + deleted_time = DB.Column(DB.Integer, default=None) def __init__(self, name: str, type: str, jsonschema: str, visible_for: str): self.name = name @@ -24,4 +24,15 @@ def update(self, name: str, type: str, jsonschema: str, visible_for: str): self.name = name self.type = type self.jsonschema = jsonschema - self.visible_for = visible_for \ No newline at end of file + self.visible_for = visible_for + + @property + def deleted(self): + return self.deleted_time is not None + + @deleted.setter + def deleted(self, value: bool): + if value: + self.deleted_time = int(time.time()) + else: + self.deleted_time = None \ No newline at end of file diff --git a/total_tolles_ferleihsystem/db_models/blacklist.py b/total_tolles_ferleihsystem/db_models/blacklist.py index 028b9e2..fe29ca1 100644 --- a/total_tolles_ferleihsystem/db_models/blacklist.py +++ b/total_tolles_ferleihsystem/db_models/blacklist.py @@ -28,18 +28,15 @@ class BlacklistToItemType (DB.Model): user_id = DB.Column(DB.Integer, DB.ForeignKey('Blacklist.id'), primary_key=True) item_type_id = DB.Column(DB.Integer, DB.ForeignKey('ItemType.id'), primary_key=True) - end_time = DB.Column(DB.DateTime, nullable=True) + end_time = DB.Column(DB.Integer, nullable=True) reason = DB.Column(DB.Text, nullable=True) - user = DB.relationship('Blacklist', backref=DB.backref('_item_types', lazy='joined', + user = DB.relationship('Blacklist', lazy='select', backref=DB.backref('_item_types', lazy='select', single_parent=True, cascade="all, delete-orphan")) item_type = DB.relationship('ItemType', lazy='joined') - def __init__(self, user: Blacklist, item_type: ItemType, end_time: any=None, reason: str=None): - #TODO Fabi pls FIX duration time + def __init__(self, user: Blacklist, item_type: ItemType, end_time: int=None, reason: str=None): self.user = user self.item_type = item_type - - if self.end_time != None: - self.end_time = end_time - self.reason = reason + self.reason = reason + self.end_time = end_time diff --git a/total_tolles_ferleihsystem/db_models/item.py b/total_tolles_ferleihsystem/db_models/item.py index e85f10e..5113c78 100644 --- a/total_tolles_ferleihsystem/db_models/item.py +++ b/total_tolles_ferleihsystem/db_models/item.py @@ -1,15 +1,14 @@ """ The database models of the item and all connected tables """ - -import datetime from sqlalchemy.sql import func from sqlalchemy.schema import UniqueConstraint import string from json import loads +import time from datetime import date -from .. import DB +from .. import DB, LENDING_LOGGER from . import STD_STRING_SIZE from . import itemType @@ -20,7 +19,6 @@ 'File', 'Lending', 'ItemToItem', - 'ItemToLending', 'ItemToTag', 'ItemToAttributeDefinition' ] @@ -36,18 +34,22 @@ class Item(DB.Model): name = DB.Column(DB.String(STD_STRING_SIZE)) update_name_from_schema = DB.Column(DB.Boolean, default=True, nullable=False) type_id = DB.Column(DB.Integer, DB.ForeignKey('ItemType.id')) - lending_duration = DB.Column(DB.Integer, nullable=True) # in seconds - deleted = DB.Column(DB.Boolean, default=False) + lending_id = DB.Column(DB.Integer, DB.ForeignKey('Lending.id'), default=None, nullable=True) + lending_duration = DB.Column(DB.Integer, nullable=True) # in seconds + due = DB.Column(DB.Integer, default=-1) # unix time + deleted_time = DB.Column(DB.Integer, default=None) visible_for = DB.Column(DB.String(STD_STRING_SIZE), nullable=True) type = DB.relationship('ItemType', lazy='joined') + lending = DB.relationship('Lending', lazy='select', + backref=DB.backref('_items', lazy='select')) __table_args__ = ( UniqueConstraint('name', 'type_id', name='_name_type_id_uc'), ) def __init__(self, update_name_from_schema: bool, name: str, type_id: int, lending_duration: int = -1, - visible_for: str = ''): + visible_for: str = ''): self.update_name_from_schema = update_name_from_schema self.name = name @@ -61,7 +63,7 @@ def __init__(self, update_name_from_schema: bool, name: str, type_id: int, lendi self.visible_for = visible_for def update(self, update_name_from_schema: bool, name: str, type_id: int, lending_duration: int = -1, - visible_for: str = ''): + visible_for: str = ''): """ Function to update the objects data """ @@ -77,33 +79,22 @@ def update(self, update_name_from_schema: bool, name: str, type_id: int, lending self.visible_for = visible_for @property - def lending_id(self): - """ - The lending_id this item is currently associated with. -1 if not lent. - """ - if self._lending: - return self._lending[0].lending_to_item.lending_id - return -1 + def deleted(self): + return self.deleted_time is not None - @property - def item_lending(self): - """ - The lending this item is currently associated with. - """ - if self._lending: - return self._lending[0] - return None + @deleted.setter + def deleted(self, value: bool): + if value: + self.deleted_time = int(time.time()) + else: + self.deleted_time = None @property def is_currently_lent(self): """ If the item is currently lent. """ - return self.lending_id != -1 - - @property - def due(self): - return -1 if self.lending_id is not None else self.item_lending.due + return self.lending is not None @property def parent(self): @@ -119,7 +110,7 @@ def effective_lending_duration(self): if self.lending_duration and (self.lending_duration >= 0): return self.lending_duration - tag_lending_duration = min((t.lending_duration for t in self._tags if t.lending_duration > 0), default=-1) + tag_lending_duration = min((t.tag.lending_duration for t in self._tags if t.tag.lending_duration > 0), default=-1) if tag_lending_duration >= 0: return tag_lending_duration @@ -184,20 +175,20 @@ def get_attribute_changes(self, definition_ids, remove: bool = False): if(remove): # Check if multiple sources bring it, if yes don't delete it. sources = 0 - if(def_id in [ittad.attribute_definition_id for ittad in self.type._item_type_to_attribute_definitions if not ittad.attribute_definition.deleted ]): + if(def_id in [ittad.attribute_definition_id for ittad in self.type._item_type_to_attribute_definitions if not ittad.attribute_definition.deleted]): sources += 1 for tag in [itt.tag for itt in self._tags]: if(def_id in [ttad.attribute_definition_id for ttad in tag._tag_to_attribute_definitions if not ttad.attribute_definition.deleted]): sources += 1 - if sources == 1 : + if sources == 1: attributes_to_delete.append(itad) elif(itad.deleted): attributes_to_undelete.append(itad) if not exists and not remove: attributes_to_add.append(ItemToAttributeDefinition(self.id, - def_id, - "")) #TODO: Get default if possible. + def_id, + "")) # TODO: Get default if possible. return attributes_to_add, attributes_to_delete, attributes_to_undelete def get_new_attributes_from_type(self, type_id: int): @@ -209,7 +200,8 @@ def get_new_attributes_from_type(self, type_id: int): .query .filter(itemType.ItemTypeToAttributeDefinition.item_type_id == type_id) .all()) - attributes_to_add, _, _ = self.get_attribute_changes([ittad.attribute_definition_id for ittad in item_type_attribute_definitions if not ittad.item_type.deleted], False) + attributes_to_add, _, _ = self.get_attribute_changes( + [ittad.attribute_definition_id for ittad in item_type_attribute_definitions if not ittad.item_type.deleted], False) return attributes_to_add @@ -263,26 +255,27 @@ class File(DB.Model): item_id = DB.Column(DB.Integer, DB.ForeignKey('Item.id'), nullable=True) name = DB.Column(DB.String(STD_STRING_SIZE), nullable=True) file_type = DB.Column(DB.String(STD_STRING_SIZE)) - file_hash = DB.Column(DB.String(STD_STRING_SIZE), nullable=True, index=True) - creation = DB.Column(DB.DateTime, server_default=func.now()) - invalidation = DB.Column(DB.DateTime, nullable=True) + file_hash = DB.Column(DB.String(STD_STRING_SIZE), nullable=True) + creation = DB.Column(DB.Integer) + invalidation = DB.Column(DB.Integer, nullable=True) visible_for = DB.Column(DB.String(STD_STRING_SIZE), nullable=True) - item = DB.relationship('Item', lazy='joined', backref=DB.backref('_files', lazy='select', + item = DB.relationship('Item', lazy='select', backref=DB.backref('_files', lazy='select', single_parent=True, cascade="all, delete-orphan")) - def __init__(self, name: str, file_type: str, file_hash: str, item_id: int=None, visible_for: str = ''): + def __init__(self, name: str, file_type: str, file_hash: str, item_id: int = None, visible_for: str = ''): if item_id is not None: self.item_id = item_id self.name = name self.file_type = file_type self.file_hash = file_hash + self.creation = int(time.time()) if visible_for != '' and visible_for != None: self.visible_for = visible_for - def update(self, name: str, file_type: str, invalidation, item_id: int, visible_for: str = '') -> None: + def update(self, name: str, file_type: str, invalidation: int, item_id: int, visible_for: str = '') -> None: """ Function to update the objects data """ @@ -302,24 +295,91 @@ class Lending(DB.Model): id = DB.Column(DB.Integer, primary_key=True) moderator = DB.Column(DB.String(STD_STRING_SIZE)) user = DB.Column(DB.String(STD_STRING_SIZE)) - date = DB.Column(DB.DateTime) + date = DB.Column(DB.Integer) #unix time deposit = DB.Column(DB.String(STD_STRING_SIZE)) - def __init__(self, moderator: str, user: str, deposit: str): + def __repr__(self): + ret = "Lending by " + ret += self.moderator + ret += " to " + ret += self.user + ret += " at " + ret += str(self.date) + ret += " with " + ret += self.deposit + ret += ". Items:" + ret += str([str(item.id) for item in self._items]) + return ret + + def __init__(self, moderator: str, user: str, deposit: str, item_ids: list): self.moderator = moderator self.user = user - self.date = datetime.datetime.now() + self.date = int(time.time()) self.deposit = deposit - - def update(self, moderator: str, user: str, deposit: str): + for element in item_ids: + item = Item.query.filter(Item.id == element).filter(Item.deleted_time == None).first() + if item is None: + raise ValueError("Item not found:" + str(element)) + if not item.type.lendable: + raise ValueError("Item not lendable:" + str(element)) + if item.is_currently_lent: + raise ValueError("Item already lent:" + str(element)) + item.lending = self + item.due = self.date + item.effective_lending_duration + LENDING_LOGGER.info("New lending: %s", repr(self)) + + + def update(self, moderator: str, user: str, deposit: str, item_ids: list): """ Function to update the objects data """ self.moderator = moderator self.user = user self.deposit = deposit + old_items = self._items + new_items = [] + + for element in item_ids: + item = Item.query.filter(Item.id == element).filter(Item.deleted_time == None).first() + if item is None: + raise ValueError("Item not found:" + str(element)) + new_items.append(item) + + items_to_remove = [item for item in old_items if item not in new_items] + items_to_add = [item for item in new_items if item not in old_items] + + for item in items_to_remove: + item.lending = None + item.due = -1 + + for item in items_to_add: + if not item.type.lendable: + raise ValueError("Item not lendable:" + str(item)) + if item.is_currently_lent: + raise ValueError("Item already lent:" + str(item)) + item.lending = self + item.due = self.date + item.effective_lending_duration + LENDING_LOGGER.info("Updated lending: %s", repr(self)) + def remove_items(self, item_ids: list): + """ + Function to remove a list of items from this lending + """ + for element in item_ids: + item = Item.query.filter(Item.id == element).first() + if item is None: + raise ValueError("Item not found:" + str(element)) + item.lending = None + item.due = -1 + LENDING_LOGGER.info("Updated lending(remove items): %s", repr(self)) + + def pre_delete(self): + LENDING_LOGGER.info("Deleting lending: %s", repr(self)) + for item in list(self._items): + item.lending = None + item.due = -1 + class ItemToItem(DB.Model): __tablename__ = 'ItemToItem' @@ -327,39 +387,18 @@ class ItemToItem(DB.Model): parent_id = DB.Column(DB.Integer, DB.ForeignKey('Item.id'), primary_key=True) item_id = DB.Column(DB.Integer, DB.ForeignKey('Item.id'), primary_key=True) - parent = DB.relationship('Item', foreign_keys=[parent_id], + parent = DB.relationship('Item', foreign_keys=[parent_id], lazy='select', backref=DB.backref('_contained_items', lazy='select', single_parent=True, cascade="all, delete-orphan")) - item = DB.relationship('Item', foreign_keys=[item_id], backref=DB.backref('_parents', lazy='select', - single_parent=True, - cascade="all, delete-orphan"), - lazy='joined') + item = DB.relationship('Item', foreign_keys=[item_id], lazy='select', + backref=DB.backref('_parents', lazy='select', + single_parent=True, cascade="all, delete-orphan")) def __init__(self, parent_id: int, item_id: int): self.parent_id = parent_id self.item_id = item_id -class ItemToLending (DB.Model): - - __tablename__ = 'ItemToLending' - - item_id = DB.Column(DB.Integer, DB.ForeignKey('Item.id'), primary_key=True) - lending_id = DB.Column(DB.Integer, DB.ForeignKey('Lending.id'), primary_key=True) - due = DB.Column(DB.DateTime) - - item = DB.relationship('Item', backref=DB.backref('_lending', lazy='joined', - single_parent=True, cascade="all, delete-orphan")) - lending = DB.relationship('Lending', backref=DB.backref('itemLendings', lazy='joined', - single_parent=True, - cascade="all, delete-orphan"), lazy='select') - - def __init__(self, item: Item, lending: Lending): - self.item = item - self.lending = lending - self.due = lending.date + datetime.timedelta(0, item.effective_lending_duration) - - class ItemToTag (DB.Model): __tablename__ = 'ItemToTag' @@ -367,8 +406,8 @@ class ItemToTag (DB.Model): item_id = DB.Column(DB.Integer, DB.ForeignKey('Item.id'), primary_key=True) tag_id = DB.Column(DB.Integer, DB.ForeignKey('Tag.id'), primary_key=True) - item = DB.relationship('Item', backref=DB.backref('_tags', lazy='joined', - single_parent=True, cascade="all, delete-orphan")) + item = DB.relationship('Item', lazy='select', backref=DB.backref('_tags', lazy='select', + single_parent=True, cascade="all, delete-orphan")) tag = DB.relationship('Tag', lazy='joined') def __init__(self, item_id: int, tag_id: int): @@ -383,11 +422,12 @@ class ItemToAttributeDefinition (DB.Model): item_id = DB.Column(DB.Integer, DB.ForeignKey('Item.id'), primary_key=True) attribute_definition_id = DB.Column(DB.Integer, DB.ForeignKey('AttributeDefinition.id'), primary_key=True) value = DB.Column(DB.String(STD_STRING_SIZE)) - deleted = DB.Column(DB.Boolean, default=False) + deleted_time = DB.Column(DB.Integer, default=None) - item = DB.relationship('Item', backref=DB.backref('_attributes', lazy='select', - single_parent=True, cascade="all, delete-orphan")) - attribute_definition = DB.relationship('AttributeDefinition', backref=DB.backref('_item_to_attribute_definitions', lazy='joined')) + item = DB.relationship('Item', lazy='select', backref=DB.backref('_attributes', lazy='select', + single_parent=True, cascade="all, delete-orphan")) + attribute_definition = DB.relationship('AttributeDefinition', lazy='select', + backref=DB.backref('_item_to_attribute_definitions', lazy='select')) def __init__(self, item_id: int, attribute_definition_id: int, value: str): self.item_id = item_id @@ -416,8 +456,12 @@ def undelete(self) -> None: self.deleted = False @property - def is_deleted(self) -> bool: - """ - Checks whether this association is currently soft deleted - """ - return self.deleted + def deleted(self): + return self.deleted_time is not None + + @deleted.setter + def deleted(self, value: bool): + if value: + self.deleted_time = int(time.time()) + else: + self.deleted_time = None diff --git a/total_tolles_ferleihsystem/db_models/itemType.py b/total_tolles_ferleihsystem/db_models/itemType.py index f124889..1faec6b 100644 --- a/total_tolles_ferleihsystem/db_models/itemType.py +++ b/total_tolles_ferleihsystem/db_models/itemType.py @@ -1,9 +1,12 @@ +import time + from .. import DB from . import STD_STRING_SIZE from .attributeDefinition import AttributeDefinition from . import item -__all__= [ 'ItemType', 'ItemTypeToItemType', 'ItemTypeToAttributeDefinition' ] +__all__ = ['ItemType', 'ItemTypeToItemType', 'ItemTypeToAttributeDefinition'] + class ItemType (DB.Model): @@ -14,11 +17,11 @@ class ItemType (DB.Model): name_schema = DB.Column(DB.String(STD_STRING_SIZE)) lendable = DB.Column(DB.Boolean, default=True) lending_duration = DB.Column(DB.Integer, nullable=True) - deleted = DB.Column(DB.Boolean, default=False) + deleted_time = DB.Column(DB.Integer, default=None) visible_for = DB.Column(DB.String(STD_STRING_SIZE), nullable=True) how_to = DB.Column(DB.Text, nullable=True) - def __init__(self, name: str, name_schema: str, lending_duration: int, visible_for: str = '', how_to: str = ''): + def __init__(self, name: str, name_schema: str, lendable: bool, lending_duration: int, visible_for: str = '', how_to: str = ''): self.name = name self.name_schema = name_schema self.lending_duration = lending_duration @@ -29,7 +32,7 @@ def __init__(self, name: str, name_schema: str, lending_duration: int, visible_f if how_to != '' and how_to != None: self.how_to = how_to - def update(self, name: str, name_schema: str, lendable: bool, lending_duration: int, visible_for: str, how_to:str): + def update(self, name: str, name_schema: str, lendable: bool, lending_duration: int, visible_for: str, how_to: str): self.name = name self.name_schema = name_schema self.lendable = lendable @@ -37,12 +40,23 @@ def update(self, name: str, name_schema: str, lendable: bool, lending_duration: self.visible_for = visible_for self.how_to = how_to + @property + def deleted(self): + return self.deleted_time is not None + + @deleted.setter + def deleted(self, value: bool): + if value: + self.deleted_time = int(time.time()) + else: + self.deleted_time = None + def unassociate_attr_def(self, attribute_definition_id): """ Does all necessary changes to the database for unassociating a attribute definition from this type. Does not commit the changes. """ - if AttributeDefinition.query.filter(AttributeDefinition.id == attribute_definition_id).filter(AttributeDefinition.deleted == False).first() is None: + if AttributeDefinition.query.filter(AttributeDefinition.id == attribute_definition_id).filter(AttributeDefinition.deleted_time == None).first() is None: return(400, 'Requested attribute definition not found!', False) association = (ItemTypeToAttributeDefinition .query @@ -52,7 +66,8 @@ def unassociate_attr_def(self, attribute_definition_id): if association is None: return(204, '', False) - itads = item.ItemToAttributeDefinition.query.filter(item.ItemToAttributeDefinition.attribute_definition_id == attribute_definition_id).all() + itads = item.ItemToAttributeDefinition.query.filter( + item.ItemToAttributeDefinition.attribute_definition_id == attribute_definition_id).all() items = [itad.item for itad in itads] @@ -72,10 +87,12 @@ class ItemTypeToItemType (DB.Model): parent_id = DB.Column(DB.Integer, DB.ForeignKey('ItemType.id', ondelete='CASCADE'), primary_key=True) item_type_id = DB.Column(DB.Integer, DB.ForeignKey('ItemType.id'), primary_key=True) - parent = DB.relationship('ItemType', foreign_keys=[parent_id], + parent = DB.relationship('ItemType', foreign_keys=[parent_id], lazy='select', backref=DB.backref('_contained_item_types', lazy='select', single_parent=True, cascade="all, delete-orphan")) - item_type = DB.relationship('ItemType', foreign_keys=[item_type_id], lazy='joined') + item_type = DB.relationship('ItemType', foreign_keys=[item_type_id], lazy='select', + backref=DB.backref('_possible_parent_item_types', lazy='select', + single_parent=True, cascade="all, delete-orphan")) def __init__(self, parent_id: int, item_type_id: int): self.parent_id = parent_id @@ -89,7 +106,8 @@ class ItemTypeToAttributeDefinition (DB.Model): item_type_id = DB.Column(DB.Integer, DB.ForeignKey('ItemType.id'), primary_key=True) attribute_definition_id = DB.Column(DB.Integer, DB.ForeignKey('AttributeDefinition.id'), primary_key=True) - item_type = DB.relationship('ItemType', backref=DB.backref('_item_type_to_attribute_definitions', lazy='select')) + item_type = DB.relationship('ItemType', lazy='select', + backref=DB.backref('_item_type_to_attribute_definitions', lazy='select')) attribute_definition = DB.relationship('AttributeDefinition', lazy='joined') def __init__(self, item_type_id: int, attribute_definition_id: int): diff --git a/total_tolles_ferleihsystem/db_models/tag.py b/total_tolles_ferleihsystem/db_models/tag.py index 55c5423..af0024b 100644 --- a/total_tolles_ferleihsystem/db_models/tag.py +++ b/total_tolles_ferleihsystem/db_models/tag.py @@ -1,6 +1,7 @@ """ Module containing database models for everything concerning Item-Tags. """ +import time from .. import DB from . import STD_STRING_SIZE @@ -19,7 +20,7 @@ class Tag(DB.Model): id = DB.Column(DB.Integer, primary_key=True) name = DB.Column(DB.String(STD_STRING_SIZE), unique=True) lending_duration = DB.Column(DB.Integer) - deleted = DB.Column(DB.Boolean, default=False) + deleted_time = DB.Column(DB.Integer, default=None) visible_for = DB.Column(DB.String(STD_STRING_SIZE)) def __init__(self, name: str, lending_duration: int, visible_for: str): @@ -32,12 +33,23 @@ def update(self, name: str, lending_duration: int, visible_for: str): self.lending_duration = lending_duration self.visible_for = visible_for + @property + def deleted(self): + return self.deleted_time is not None + + @deleted.setter + def deleted(self, value: bool): + if value: + self.deleted_time = int(time.time()) + else: + self.deleted_time = None + def unassociate_attr_def(self, attribute_definition_id): """ Does all necessary changes to the database for unassociating a attribute definition from this tag. Does not commit the changes. """ - if attributeDefinition.AttributeDefinition.query.filter(attributeDefinition.AttributeDefinition.id == attribute_definition_id).filter(attributeDefinition.AttributeDefinition.deleted == False).first() is None: + if attributeDefinition.AttributeDefinition.query.filter(attributeDefinition.AttributeDefinition.id == attribute_definition_id).filter(attributeDefinition.AttributeDefinition.deleted_time == None).first() is None: return(400, 'Requested attribute definition not found!', False) association = (TagToAttributeDefinition .query @@ -67,7 +79,7 @@ class TagToAttributeDefinition (DB.Model): tag_id = DB.Column(DB.Integer, DB.ForeignKey('Tag.id'), primary_key=True) attribute_definition_id = DB.Column(DB.Integer, DB.ForeignKey('AttributeDefinition.id'), primary_key=True) - tag = DB.relationship(Tag, backref=DB.backref('_tag_to_attribute_definitions', lazy='select')) + tag = DB.relationship(Tag, lazy='select', backref=DB.backref('_tag_to_attribute_definitions', lazy='select')) attribute_definition = DB.relationship('AttributeDefinition', lazy='joined') def __init__(self, tag_id: int, attribute_definition_id: int): diff --git a/total_tolles_ferleihsystem/package-lock.json b/total_tolles_ferleihsystem/package-lock.json index 5c24ce8..b569295 100644 --- a/total_tolles_ferleihsystem/package-lock.json +++ b/total_tolles_ferleihsystem/package-lock.json @@ -211,185 +211,6 @@ "tsickle": "^0.21.0" } }, - "@babel/code-frame": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.49.tgz", - "integrity": "sha1-vs2AVIJzREDJ0TfkbXc0DmTX9Rs=", - "dev": true, - "requires": { - "@babel/highlight": "7.0.0-beta.49" - } - }, - "@babel/generator": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.49.tgz", - "integrity": "sha1-6c/9qROZaszseTu8JauRvBnQv3o=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49", - "jsesc": "^2.5.1", - "lodash": "^4.17.5", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=", - "dev": true - } - } - }, - "@babel/helper-function-name": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.49.tgz", - "integrity": "sha1-olwRGbnwNSeGcBJuAiXAMEHI3jI=", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "7.0.0-beta.49", - "@babel/template": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.49.tgz", - "integrity": "sha1-z1Aj8y0q2S0Ic3STnOwJUby1FEE=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.49.tgz", - "integrity": "sha1-QNeO2glo0BGxxShm5XRs+yPldUg=", - "dev": true, - "requires": { - "@babel/types": "7.0.0-beta.49" - } - }, - "@babel/highlight": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.49.tgz", - "integrity": "sha1-lr3GtD4TSCASumaRsQGEktOWIsw=", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-beta.49.tgz", - "integrity": "sha1-lE0MW6KBK7FZ7b0iZ0Ov0mUXm9w=", - "dev": true - }, - "@babel/template": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.49.tgz", - "integrity": "sha1-44q+ghfLl5P0YaUwbXrXRdg+HSc=", - "dev": true, - "requires": { - "@babel/code-frame": "7.0.0-beta.49", - "@babel/parser": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49", - "lodash": "^4.17.5" - } - }, - "@babel/traverse": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.49.tgz", - "integrity": "sha1-TypzaCoYM07WYl0QCo0nMZ98LWg=", - "dev": true, - "requires": { - "@babel/code-frame": "7.0.0-beta.49", - "@babel/generator": "7.0.0-beta.49", - "@babel/helper-function-name": "7.0.0-beta.49", - "@babel/helper-split-export-declaration": "7.0.0-beta.49", - "@babel/parser": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49", - "debug": "^3.1.0", - "globals": "^11.1.0", - "invariant": "^2.2.0", - "lodash": "^4.17.5" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "globals": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.5.0.tgz", - "integrity": "sha512-hYyf+kI8dm3nORsiiXUQigOU62hDLfJ9G01uyGMxhc6BKsircrUhC4uJPQPUSuq2GrTmiiEt7ewxlMdBewfmKQ==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.0.0-beta.49", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.49.tgz", - "integrity": "sha1-t+Oxw/TUz+Eb34yJ8e/V4WF7h6Y=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.5", - "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - } - } - }, "@ngtools/json-schema": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@ngtools/json-schema/-/json-schema-1.1.0.tgz", @@ -437,6 +258,15 @@ "integrity": "sha512-ikB0JHv6vCR1KYUQAzTO4gi/lXLElT4Tx+6De2pc/OZwizE9LRNiTa+U8TBFKBD/nntPnr/MPSHSnOTybjhqNA==", "dev": true }, + "@zxing/library": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.12.3.tgz", + "integrity": "sha512-/PUAFaMe6eV55DLUJzcF1d9OEFOR265CFxKeLDEdJMnAWhNOX9SajLVg2+PkFh+PQTtypoNUuzEY+nhPiPjSLQ==", + "requires": { + "text-encoding": "^0.7.0", + "ts-custom-error": "^2.2.2" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -578,6 +408,15 @@ "integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=", "dev": true }, + "append-transform": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", + "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", + "dev": true, + "requires": { + "default-require-extensions": "^1.0.0" + } + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -811,27 +650,11 @@ "babel-runtime": "^6.22.0" } }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=" - } - } - }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -1537,12 +1360,6 @@ "babel-runtime": "^6.26.0" } }, - "compare-versions": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.3.0.tgz", - "integrity": "sha512-MAAAIOdi2s4Gl6rZ76PNcUa9IOYB+5ICdT41o5uMRf09aEu/F9RK+qhe8RjXNPwcTjGV7KU7h2P/fljThFVqyQ==", - "dev": true - }, "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", @@ -1953,6 +1770,15 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "default-require-extensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", + "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", + "dev": true, + "requires": { + "strip-bom": "^2.0.0" + } + }, "defined": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", @@ -2369,7 +2195,8 @@ "es6-promise": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", + "dev": true }, "es6-promisify": { "version": "5.0.0", @@ -3366,16 +3193,6 @@ } } }, - "fsm-as-promised": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/fsm-as-promised/-/fsm-as-promised-0.13.2.tgz", - "integrity": "sha1-X04RCGgotwoZItx7T4HAgX1ugjg=", - "requires": { - "es6-promise": "^4.0.2", - "lodash": "^4.16.2", - "stampit": "^3.0.1" - } - }, "fstream": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", @@ -4232,17 +4049,6 @@ } } }, - "instascan": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/instascan/-/instascan-1.0.0.tgz", - "integrity": "sha1-3llJ9z9pj43/aN8Ke89pQqeZ7kQ=", - "requires": { - "babel-polyfill": "^6.9.1", - "fsm-as-promised": "^0.13.0", - "visibilityjs": "^1.2.3", - "webrtc-adapter": "^1.4.0" - } - }, "interpret": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", @@ -4509,222 +4315,22 @@ "dev": true }, "istanbul-api": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.6.tgz", - "integrity": "sha512-luJDnB1uJ5Qsg/WwusGfNXayQ4598yDgW5S0nUS85T576m1LVJzSqLrCDULkT6sTQXVKHa54093gNuCKumMCjQ==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", + "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", "dev": true, "requires": { "async": "^2.1.4", - "compare-versions": "^3.1.0", "fileset": "^2.0.2", - "istanbul-lib-coverage": "^1.2.0", - "istanbul-lib-hook": "^1.2.0", - "istanbul-lib-instrument": "^2.1.0", - "istanbul-lib-report": "^1.1.4", - "istanbul-lib-source-maps": "^1.2.5", - "istanbul-reports": "^1.4.1", + "istanbul-lib-coverage": "^1.2.1", + "istanbul-lib-hook": "^1.2.2", + "istanbul-lib-instrument": "^1.10.2", + "istanbul-lib-report": "^1.1.5", + "istanbul-lib-source-maps": "^1.2.6", + "istanbul-reports": "^1.5.1", "js-yaml": "^3.7.0", "mkdirp": "^0.5.1", "once": "^1.4.0" - }, - "dependencies": { - "append-transform": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", - "dev": true, - "requires": { - "default-require-extensions": "^2.0.0" - } - }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "optional": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "default-require-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", - "dev": true, - "requires": { - "strip-bom": "^3.0.0" - } - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "dev": true, - "requires": { - "async": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "istanbul-lib-coverage": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", - "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.1.tgz", - "integrity": "sha512-eLAMkPG9FU0v5L02lIkcj/2/Zlz9OuluaXikdr5iStk8FDbSwAixTK9TkYxbF0eNnzAJTwM2fkV2A1tpsIp4Jg==", - "dev": true, - "requires": { - "append-transform": "^1.0.0" - } - }, - "istanbul-lib-instrument": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-2.2.0.tgz", - "integrity": "sha512-ozQGtlIw+/a/F3n6QwWiuuyRAPp64+g2GVsKYsIez0sgIEzkU5ZpL2uZ5pmAzbEJ82anlRaPlOQZzkRXspgJyg==", - "dev": true, - "requires": { - "@babel/generator": "7.0.0-beta.49", - "@babel/parser": "7.0.0-beta.49", - "@babel/template": "7.0.0-beta.49", - "@babel/traverse": "7.0.0-beta.49", - "@babel/types": "7.0.0-beta.49", - "istanbul-lib-coverage": "^2.0.0", - "semver": "^5.5.0" - }, - "dependencies": { - "istanbul-lib-coverage": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.0.tgz", - "integrity": "sha512-yMSw5xLIbdaxiVXHk3amfNM2WeBxLrwH/BCyZ9HvA/fylwziAIJOG2rKqWyLqEJqwKT725vxxqidv+SyynnGAA==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz", - "integrity": "sha512-Azqvq5tT0U09nrncK3q82e/Zjkxa4tkFZv7E6VcqP0QCPn6oNljDPfrZEC/umNXds2t7b8sRJfs6Kmpzt8m2kA==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^1.2.0", - "mkdirp": "^0.5.1", - "path-parse": "^1.0.5", - "supports-color": "^3.1.2" - } - }, - "istanbul-lib-source-maps": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.5.tgz", - "integrity": "sha512-8O2T/3VhrQHn0XcJbP1/GN7kXMiRAlPi+fj3uEHrjBD8Oz7Py0prSC25C09NuAZS6bgW1NNKAvCSHZXB0irSGA==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.2.0", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.1", - "source-map": "^0.5.3" - } - }, - "istanbul-reports": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.0.tgz", - "integrity": "sha512-HeZG0WHretI9FXBni5wZ9DOgNziqDCEwetxnme5k1Vv5e81uTqcsy3fMH99gXGDGKr1ea87TyGseDMa2h4HEUA==", - "dev": true, - "requires": { - "handlebars": "^4.0.11" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dev": true, - "optional": true, - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - } - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } } }, "istanbul-instrumenter-loader": { @@ -4753,10 +4359,25 @@ } } }, + "istanbul-lib-coverage": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", + "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", + "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", + "dev": true, + "requires": { + "append-transform": "^0.4.0" + } + }, "istanbul-lib-instrument": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz", - "integrity": "sha512-1dYuzkOCbuR5GRJqySuZdsmsNKPL3PTuyPevQfoCXJePT9C8y1ga75neU+Tuy9+yS3G/dgx8wgOmp2KLpgdoeQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", + "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", "dev": true, "requires": { "babel-generator": "^6.18.0", @@ -4764,14 +4385,77 @@ "babel-traverse": "^6.18.0", "babel-types": "^6.18.0", "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-coverage": "^1.2.1", "semver": "^5.3.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", + "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^1.2.1", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", + "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.2.1", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" }, "dependencies": { - "istanbul-lib-coverage": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz", - "integrity": "sha512-GvgM/uXRwm+gLlvkWHTjDAvwynZkL9ns15calTrmhGgowlwJBbWMYzWbKqE2DT6JDP1AFXKa+Zi0EkqNCUqY0A==", + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", + "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", + "dev": true, + "requires": { + "handlebars": "^4.0.3" + }, + "dependencies": { + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true } } @@ -4817,9 +4501,9 @@ "dev": true }, "jquery": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", - "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.0.tgz", + "integrity": "sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ==" }, "js-base64": { "version": "2.4.3", @@ -4834,13 +4518,21 @@ "dev": true }, "js-yaml": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", - "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "dev": true, "requires": { "argparse": "^1.0.7", - "esprima": "^2.6.0" + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } } }, "jsbn": { @@ -5224,9 +4916,10 @@ } }, "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true }, "lodash.assign": { "version": "4.2.0", @@ -5637,6 +5330,12 @@ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", "dev": true }, + "neo-async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", + "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", + "dev": true + }, "ng2-file-upload": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ng2-file-upload/-/ng2-file-upload-1.3.0.tgz", @@ -7235,7 +6934,8 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true }, "regex-cache": { "version": "0.4.4", @@ -7629,11 +7329,6 @@ } } }, - "sdp": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/sdp/-/sdp-1.5.4.tgz", - "integrity": "sha1-jgOPbdsUvXZa4fS1IW4SCUUR4NA=" - }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -8143,11 +7838,6 @@ } } }, - "stampit": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/stampit/-/stampit-3.2.1.tgz", - "integrity": "sha1-lTpBpJRYoLKG/7HjydbOcDblids=" - }, "statuses": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", @@ -8357,6 +8047,18 @@ "mkdirp": "~0.5.1", "sax": "~1.2.1", "whet.extend": "~0.9.9" + }, + "dependencies": { + "js-yaml": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", + "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^2.6.0" + } + } } }, "symbol-observable": { @@ -8411,6 +8113,12 @@ } } }, + "text-encoding": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", + "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", + "optional": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -8513,6 +8221,11 @@ } } }, + "ts-custom-error": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-2.2.2.tgz", + "integrity": "sha512-I0FEdfdatDjeigRqh1JFj67bcIKyRNm12UVGheBjs2pXgyELg2xeiQLVaWu1pVmNGXZVnz/fvycSU41moBIpOg==" + }, "ts-node": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.0.6.tgz", @@ -8866,11 +8579,6 @@ } } }, - "visibilityjs": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/visibilityjs/-/visibilityjs-1.2.8.tgz", - "integrity": "sha512-Y+aL3OUX88b+/VSmkmC2ApuLbf0grzbNLpCfIDSw3BzTU6PqcPsdgIOaw8b+eZoy+DdQqnVN3y/Evow9vQq9Ig==" - }, "vlq": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", @@ -9258,14 +8966,6 @@ } } }, - "webrtc-adapter": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-1.4.0.tgz", - "integrity": "sha1-WCiaY9BUxls2+w7zieovbQx/XZg=", - "requires": { - "sdp": "^1.0.0" - } - }, "websocket-driver": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", diff --git a/total_tolles_ferleihsystem/package.json b/total_tolles_ferleihsystem/package.json index a2ed794..a2c5e1b 100644 --- a/total_tolles_ferleihsystem/package.json +++ b/total_tolles_ferleihsystem/package.json @@ -24,12 +24,12 @@ "@angular/platform-browser-dynamic": "^4.0.0", "@angular/router": "^4.0.0", "@types/file-saver": "^1.3.0", + "@zxing/library": "^0.12.3", "bootstrap": "^4.0.0-alpha.6", "core-js": "^2.4.1", "file-saver": "^1.3.8", "font-awesome": "^4.7.0", - "instascan": "^1.0.0", - "jquery": "^3.3.1", + "jquery": "^3.4.0", "named-regexp-groups": "^1.0.3", "ng2-file-upload": "^1.2.1", "ngx-bootstrap": "^1.7.1", diff --git a/total_tolles_ferleihsystem/performance.py b/total_tolles_ferleihsystem/performance.py index 56774a0..bbf73a5 100644 --- a/total_tolles_ferleihsystem/performance.py +++ b/total_tolles_ferleihsystem/performance.py @@ -11,7 +11,7 @@ from typing import Any, List -QueryRecord = namedtuple('QueryRecord', ['duration', 'statement', 'write']) +QueryRecord = namedtuple('QueryRecord', ['duration', 'statement', 'write', 'params']) class RequestPerformance: @@ -47,10 +47,10 @@ def end_request(self): def start_query(self): self.query_start = time() - def end_query(self, statement): + def end_query(self, statement, parameters): query_end = time() write = not statement.upper().startswith('SELECT') - self.queries.append(QueryRecord(query_end - self.query_start, statement, write)) + self.queries.append(QueryRecord(query_end - self.query_start, statement, write, parameters)) self.query_start = query_end def start_view_function(self): @@ -82,8 +82,8 @@ def log_performance_record(self, methodToLogWith): longest_query_duration = self.queries[0].duration methodToLogWith(f'performance report: duration {self.duration: 2.2f}s, {time_in_view}duration without queries {duration_wo_queries: 2.2f}s, query-duration {tot_query_duration: 2.2f}s, {q_number: 2d} queries ({q_write_number: 2d} write), longest query {longest_query_duration: 2.2f}s, url {method:6} {url}') for q in self.queries: - if q.duration > 1: - methodToLogWith(f'performance report: long query detected: duration {q.duration: 2.2f}s, statement "{q.statement}"') + if q.duration > APP.config.get('LONG_REQUEST_THRESHHOLD', 1): + methodToLogWith(f'performance report: long query detected: duration {q.duration: 2.2f}s, statement "{q.statement}", params: {q.params}') else: methodToLogWith(f'performance report: duration {self.duration: 2.2f}s, {time_in_view}url {method} {url}') @@ -116,7 +116,7 @@ def before_query(conn, cursor, statement, parameters, context, executemany): def after_query(conn, cursor, statement, parameters, context, executemany): r_perf: RequestPerformance = g.get('ttf_request_performance') if r_perf is not None: - r_perf.end_query(statement) + r_perf.end_query(statement, parameters) def record_view_performance(): diff --git a/total_tolles_ferleihsystem/src/app/app-routing.module.ts b/total_tolles_ferleihsystem/src/app/app-routing.module.ts index 312f6b1..1768af0 100644 --- a/total_tolles_ferleihsystem/src/app/app-routing.module.ts +++ b/total_tolles_ferleihsystem/src/app/app-routing.module.ts @@ -13,6 +13,7 @@ import { LendingOverviewComponent } from './lending/lending-overview.component'; import { LendingComponent } from './lending/lending.component'; import { ItemsOverviewComponent } from './items/items-overview.component'; +import { ItemsOfTypeOverviewComponent } from './items/items-of-type-overview.component'; import { ItemDetailComponent } from './items/item-detail.component'; import { ItemTypesOverviewComponent } from './item-types/item-types-overview.component'; @@ -35,6 +36,7 @@ const routes: Routes = [ { path: 'lendings/:id', component: LendingComponent, canActivate: [ModGuard] }, { path: 'items', component: ItemsOverviewComponent, canActivate: [LoginGuard] }, { path: 'items/:id', component: ItemDetailComponent, canActivate: [LoginGuard] }, + { path: 'items-of-type/:id', component: ItemsOfTypeOverviewComponent, canActivate: [LoginGuard] }, { path: 'item-types', component: ItemTypesOverviewComponent, canActivate: [ModGuard] }, { path: 'item-types/:id', component: ItemTypeDetailComponent, canActivate: [ModGuard] }, { path: 'tags', component: TagsOverviewComponent, canActivate: [ModGuard] }, diff --git a/total_tolles_ferleihsystem/src/app/app.module.ts b/total_tolles_ferleihsystem/src/app/app.module.ts index 4a6355b..a0056fa 100644 --- a/total_tolles_ferleihsystem/src/app/app.module.ts +++ b/total_tolles_ferleihsystem/src/app/app.module.ts @@ -28,6 +28,7 @@ import { LendingComponent } from './lending/lending.component'; import { ItemLendingComponent } from './lending/item-lending.component'; import { ItemsOverviewComponent } from './items/items-overview.component'; +import { ItemsOfTypeOverviewComponent } from './items/items-of-type-overview.component'; import { ItemListComponent } from './items/item-list.component'; import { ItemDetailComponent } from './items/item-detail.component'; import { FileDetailComponent } from './items/file-detail.component'; @@ -53,6 +54,8 @@ import { AttributeDefinitionDetailComponent } from './attribute-definitions/attr import { AttributeDefinitionEditComponent } from './attribute-definitions/attribute-definition-edit.component'; import { LinkedAttributeDefinitionComponent } from './linked-attribute-definitions/linked-attribute-definitions.component'; +import { AttributeDefinitionTitlePipe } from './attribute-definitions/attribute-title.pipe'; + import { AppComponent } from './app.component'; @@ -81,6 +84,7 @@ import { AppComponent } from './app.component'; ItemLendingComponent, ItemsOverviewComponent, + ItemsOfTypeOverviewComponent, ItemListComponent, ItemDetailComponent, FileDetailComponent, @@ -105,6 +109,8 @@ import { AppComponent } from './app.component'; AttributeDefinitionDetailComponent, AttributeDefinitionEditComponent, LinkedAttributeDefinitionComponent, + + AttributeDefinitionTitlePipe ], imports: [ HttpModule, diff --git a/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-create.component.ts b/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-create.component.ts index 193fb76..5c4ee7a 100644 --- a/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-create.component.ts +++ b/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-create.component.ts @@ -45,13 +45,13 @@ export class AttributeDefinitionCreateComponent { } save = () => { - this.api.postAttributeDefinition(this.newAttributeDefinitionData).subscribe(data => { + const sub = this.api.postAttributeDefinition(this.newAttributeDefinitionData).subscribe(data => { if (this.allowAutoNavigate) { this.settings.getSetting('navigateAfterCreation').take(1).subscribe(navigate => { - console.log(navigate) if (navigate) { this.router.navigate(['attribute-definitions', data.id]); } + sub.unsubscribe(); }); } }); diff --git a/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-edit.component.html b/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-edit.component.html index 7f645be..f8335e1 100644 --- a/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-edit.component.html +++ b/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-edit.component.html @@ -4,7 +4,20 @@

Edit: {{attrDef?.name}}

- + +
+ + +

Check definition:

+ +
+
+
+ +
+
diff --git a/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-edit.component.ts b/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-edit.component.ts index 49fa95f..aac0ba8 100644 --- a/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-edit.component.ts +++ b/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-definition-edit.component.ts @@ -5,6 +5,10 @@ import { ApiService } from '../shared/rest/api.service'; import { Subscription } from 'rxjs/Rx'; import { DynamicFormComponent } from '../shared/forms/dynamic-form/dynamic-form.component'; import { JWTService } from '../shared/rest/jwt.service'; +import { QuestionBase } from 'app/shared/forms/question-base'; +import { FormGroup } from '@angular/forms'; +import { QuestionService } from 'app/shared/forms/question.service'; +import { QuestionControlService } from 'app/shared/forms/question-control.service'; @Component({ selector: 'ttf-attribute-definition-edit', @@ -18,12 +22,15 @@ export class AttributeDefinitionEditComponent implements OnChanges, OnDestroy { @Input() attributeDefinitionID: number; - attrDef: ApiObject = { - _links: {'self': {'href': ''}}, - name: 'UNBEKANNT' - }; + questions: QuestionBase[] = []; + testForm: FormGroup; + + attrDef: any = null; + + currentAttrDef: any; - constructor(private api: ApiService, private router: Router, private jwt: JWTService) { } + constructor(private api: ApiService, private router: Router, private jwt: JWTService, + private qs: QuestionService, private qcs: QuestionControlService) { } ngOnChanges(): void { if (this.subscription != null) { @@ -31,6 +38,7 @@ export class AttributeDefinitionEditComponent implements OnChanges, OnDestroy { } this.subscription = this.api.getAttributeDefinition(this.attributeDefinitionID).subscribe(data => { this.attrDef = data; + this.updateTestForm(); }); } @@ -40,9 +48,52 @@ export class AttributeDefinitionEditComponent implements OnChanges, OnDestroy { } } + updateTestForm() { + let attribute_definition = this.attrDef; + if (this.currentAttrDef != null && this.currentAttrDef.name != null && this.currentAttrDef.name !== '') { + attribute_definition = this.currentAttrDef; + } + let schema: any = {}; + if (attribute_definition.jsonschema != null && attribute_definition.jsonschema !== '') { + schema = JSON.parse(attribute_definition.jsonschema); + } + schema.type = attribute_definition.type; + if (attribute_definition.type === 'string') { + const maxLength = (window as any).maxDBStringLength - 2; + if (schema.maxLength == null || schema.maxLength > maxLength) { + schema.maxLength = maxLength; + } + } + this.qs.getQuestionsFromScheme({ + type: 'object', + properties: { + [attribute_definition.name]: schema, + } + }).take(1).subscribe(questions => { + this.questions = questions; + this.questions.forEach(qstn => { + if (qstn.key === attribute_definition.name) { + qstn.autocompleteData = this.api.getAttributeAutocomplete(this.attrDef); + } + }); + this.testForm = this.qcs.toFormGroup(this.questions); + }); + } + + questionTrackFn(index: any, question: any) { + return question.key; + } + save = (event) => { + if (event.jsonschema !== '') { + // Try formatting the jsonscheme before saving to help editing later + try { + event.jsonschema = JSON.stringify(JSON.parse(event.jsonschema), undefined, '\t'); + } catch (error) {} + } this.api.putAttributeDefinition(this.attrDef.id, event).take(1).subscribe(() => { this.form.saveFinished(true); + this.updateTestForm(); }, () => { this.form.saveFinished(false); }); diff --git a/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-title.pipe.ts b/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-title.pipe.ts new file mode 100644 index 0000000..e910197 --- /dev/null +++ b/total_tolles_ferleihsystem/src/app/attribute-definitions/attribute-title.pipe.ts @@ -0,0 +1,9 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { attrDefTitle } from './helper-functions'; + +@Pipe({name: 'attributeDefinitionTitle'}) +export class AttributeDefinitionTitlePipe implements PipeTransform { + transform(value): string { + return attrDefTitle(value); + } +} \ No newline at end of file diff --git a/total_tolles_ferleihsystem/src/app/attribute-definitions/helper-functions.ts b/total_tolles_ferleihsystem/src/app/attribute-definitions/helper-functions.ts new file mode 100644 index 0000000..34f8bd1 --- /dev/null +++ b/total_tolles_ferleihsystem/src/app/attribute-definitions/helper-functions.ts @@ -0,0 +1,10 @@ +export function attrDefTitle(attributeDefinition) { + let title = attributeDefinition.name; + try { + const jsonscheme = JSON.parse(attributeDefinition.jsonschema); + if (jsonscheme.title != null && jsonscheme.title !== '') { + title = jsonscheme.title; + } + } catch (error) {} + return title; +} diff --git a/total_tolles_ferleihsystem/src/app/home/home.component.html b/total_tolles_ferleihsystem/src/app/home/home.component.html index ee6a120..b25aa1d 100644 --- a/total_tolles_ferleihsystem/src/app/home/home.component.html +++ b/total_tolles_ferleihsystem/src/app/home/home.component.html @@ -11,10 +11,10 @@ - {{item.name}} ({{item.type?.name}}) + {{item.name}} ({{itemTypes.get(item.type_id)?.name}}) - {{item.due | date:'short'}} + {{item.due * 1000 | date:'short'}} @@ -46,4 +46,8 @@ Lendings + diff --git a/total_tolles_ferleihsystem/src/app/home/home.component.ts b/total_tolles_ferleihsystem/src/app/home/home.component.ts index 6076deb..1d50601 100644 --- a/total_tolles_ferleihsystem/src/app/home/home.component.ts +++ b/total_tolles_ferleihsystem/src/app/home/home.component.ts @@ -4,6 +4,7 @@ import { JWTService } from '../shared/rest/jwt.service'; import { ApiService } from '../shared/rest/api.service'; import { ApiObject } from '../shared/rest/api-base.service'; import { Observable } from 'rxjs'; +import { SettingsService } from 'app/shared/settings/settings.service'; @Component({ selector: 'ttf-home', @@ -16,7 +17,7 @@ import { Observable } from 'rxjs'; grid-column-gap: 20px; grid-row-gap: 20px; grid-template-columns: repeat(auto-fill, 17rem); - justify-content: space-between; + justify-content: space-between; margin-left: .5rem; margin-right: .5rem; }` @@ -25,29 +26,30 @@ import { Observable } from 'rxjs'; export class HomeComponent implements OnInit { lentItems: ApiObject[]; + itemTypes: Map = new Map(); + pinnedTypes: number[] = []; - justifyBetween: boolean = false; - - @ViewChild('#menuContainer') menuContainer; - - constructor(private data: NavigationService, private jwt: JWTService, private api: ApiService) { } + constructor(private data: NavigationService, private jwt: JWTService, private api: ApiService, private settings: SettingsService) { } ngOnInit(): void { this.data.changeTitle('Total Tolles Ferleihsystem – Home'); this.data.changeBreadcrumbs([]); - this.api.getLentItems('errors').subscribe(items => { + this.api.getLentItems('errors').take(2).subscribe(items => { this.lentItems = items; }); + this.api.getItemTypes().take(2).subscribe(types => { + types.forEach(itemType => this.itemTypes.set(itemType.id, itemType)); + }); + this.settings.getSetting('pinnedItemTypes').take(2).subscribe(pinnedTypes => { + if (pinnedTypes != null) { + this.pinnedTypes = pinnedTypes; + } + }); Observable.timer(5 * 60 * 1000, 5 * 60 * 1000).subscribe(() => this.api.getLentItems()); - Observable.timer(1).subscribe(() => this.updateJustify()); - } - - updateJustify() { - console.log(this.menuContainer); } itemOverdue(item: ApiObject): boolean { - const due = new Date(item.due); + const due = new Date(item.due * 1000); return due < new Date(); } diff --git a/total_tolles_ferleihsystem/src/app/item-types/item-type-edit.component.ts b/total_tolles_ferleihsystem/src/app/item-types/item-type-edit.component.ts index 569a573..6cf60ba 100644 --- a/total_tolles_ferleihsystem/src/app/item-types/item-type-edit.component.ts +++ b/total_tolles_ferleihsystem/src/app/item-types/item-type-edit.component.ts @@ -41,7 +41,7 @@ export class ItemTypeEditComponent implements OnChanges, OnDestroy { if (this.canContainSubscription != null) { this.canContainSubscription.unsubscribe(); } - this.canContainSubscription = this.api.getCanContain(this.itemType).subscribe(canContain => { + this.canContainSubscription = this.api.getContainedTypes(this.itemType).subscribe(canContain => { this.canContain = canContain; }); }); @@ -58,13 +58,13 @@ export class ItemTypeEditComponent implements OnChanges, OnDestroy { addCanContain() { if (this.canContainTypeID != null && this.canContainTypeID >= 0) { - this.api.postCanContain(this.itemType, this.canContainTypeID); + this.api.postContainedType(this.itemType, this.canContainTypeID); } } removeCanContain(id) { if (this.canContainTypeID != null && this.canContainTypeID >= 0) { - this.api.deleteCanContain(this.itemType, id); + this.api.deleteContainedType(this.itemType, id); } } diff --git a/total_tolles_ferleihsystem/src/app/item-types/item-types-overview.component.ts b/total_tolles_ferleihsystem/src/app/item-types/item-types-overview.component.ts index ec2d58d..72e1eef 100644 --- a/total_tolles_ferleihsystem/src/app/item-types/item-types-overview.component.ts +++ b/total_tolles_ferleihsystem/src/app/item-types/item-types-overview.component.ts @@ -27,11 +27,12 @@ export class ItemTypesOverviewComponent implements OnInit { } save = () => { - this.api.postItemType(this.newItemTypeData).subscribe(data => { + const sub = this.api.postItemType(this.newItemTypeData).subscribe(data => { this.settings.getSetting('navigateAfterCreation').take(1).subscribe(navigate => { if (navigate) { this.router.navigate(['item-types', data.id]); } + sub.unsubscribe(); }); }); }; diff --git a/total_tolles_ferleihsystem/src/app/items/attribute-edit.component.ts b/total_tolles_ferleihsystem/src/app/items/attribute-edit.component.ts index 9bad010..038bd4d 100644 --- a/total_tolles_ferleihsystem/src/app/items/attribute-edit.component.ts +++ b/total_tolles_ferleihsystem/src/app/items/attribute-edit.component.ts @@ -17,6 +17,7 @@ export class AttributeEditComponent implements OnChanges, OnDestroy { private itemSubscription: Subscription; private attributeSubscription: Subscription; + private formStatusChangeSubscription: Subscription; @Input() itemID: number; @Input() attributeID: number; @@ -46,7 +47,9 @@ export class AttributeEditComponent implements OnChanges, OnDestroy { if (this.attributeID != null) { this.attributeSubscription = this.api.getAttribute(this.item, this.attributeID).subscribe(data => { this.attribute = data; - this.getQuestion(this.attribute); + if (this.form == null) { + this.getQuestion(this.attribute); + } this.saved = true; }); } @@ -87,7 +90,10 @@ export class AttributeEditComponent implements OnChanges, OnDestroy { this.form.patchValue({ [attribute.attribute_definition.name]: value, }); - this.form.statusChanges.filter(status => status === 'VALID').map(status => { + if (this.formStatusChangeSubscription != null) { + this.formStatusChangeSubscription.unsubscribe(); + } + this.formStatusChangeSubscription = this.form.statusChanges.filter(status => status === 'VALID').map(status => { this.saved = false; return JSON.stringify(this.form.value[attribute.attribute_definition.name]); }).debounceTime(700).subscribe(value => { @@ -107,6 +113,9 @@ export class AttributeEditComponent implements OnChanges, OnDestroy { if (this.attributeSubscription != null) { this.attributeSubscription.unsubscribe(); } + if (this.formStatusChangeSubscription != null) { + this.formStatusChangeSubscription.unsubscribe(); + } } } diff --git a/total_tolles_ferleihsystem/src/app/items/item-detail.component.html b/total_tolles_ferleihsystem/src/app/items/item-detail.component.html index d47ff04..77a0fc9 100644 --- a/total_tolles_ferleihsystem/src/app/items/item-detail.component.html +++ b/total_tolles_ferleihsystem/src/app/items/item-detail.component.html @@ -5,7 +5,7 @@

Item "{{item?.name}}" - diff --git a/total_tolles_ferleihsystem/src/app/lending/lending.component.ts b/total_tolles_ferleihsystem/src/app/lending/lending.component.ts index ab03ec8..4cd03d0 100644 --- a/total_tolles_ferleihsystem/src/app/lending/lending.component.ts +++ b/total_tolles_ferleihsystem/src/app/lending/lending.component.ts @@ -5,6 +5,7 @@ import { StagingService } from '../navigation/staging-service'; import { ApiService } from '../shared/rest/api.service'; import { JWTService } from '../shared/rest/jwt.service'; import { Subscription } from 'rxjs/Rx'; +import { Router } from '@angular/router'; @Component({ selector: 'ttf-lending', @@ -19,7 +20,8 @@ export class LendingComponent implements OnInit, OnDestroy { lending; constructor(private data: NavigationService, private api: ApiService, - private jwt: JWTService, private route: ActivatedRoute) { } + private jwt: JWTService, private route: ActivatedRoute, + private router: Router) { } ngOnInit(): void { this.data.changeTitle('Total Tolles Ferleihsystem – Lending'); @@ -54,7 +56,11 @@ export class LendingComponent implements OnInit, OnDestroy { return(id?: number) { if (this.lending != null) { - this.api.returnLending(this.lending, id); + this.api.returnLending(this.lending, id).subscribe(lendingWasDeleted => { + if (lendingWasDeleted) { + this.router.navigate(['lendings']); + } + }); } } diff --git a/total_tolles_ferleihsystem/src/app/navigation/title-bar.component.html b/total_tolles_ferleihsystem/src/app/navigation/title-bar.component.html index 87a96a7..640375d 100644 --- a/total_tolles_ferleihsystem/src/app/navigation/title-bar.component.html +++ b/total_tolles_ferleihsystem/src/app/navigation/title-bar.component.html @@ -7,7 +7,7 @@

{{title}}

-
+
diff --git a/total_tolles_ferleihsystem/src/app/navigation/title-bar.component.ts b/total_tolles_ferleihsystem/src/app/navigation/title-bar.component.ts index 83af946..c183d0f 100644 --- a/total_tolles_ferleihsystem/src/app/navigation/title-bar.component.ts +++ b/total_tolles_ferleihsystem/src/app/navigation/title-bar.component.ts @@ -13,7 +13,7 @@ export class TitleBarComponent implements OnInit { title: string; - constructor(private data: NavigationService, private jwt: JWTService) { } + constructor(private data: NavigationService, private jwt: JWTService, private staging: StagingService) { } ngOnInit(): void { this.data.currentTitle.subscribe(title => this.title = title); diff --git a/total_tolles_ferleihsystem/src/app/qr/qr.component.ts b/total_tolles_ferleihsystem/src/app/qr/qr.component.ts index 475261f..201c886 100644 --- a/total_tolles_ferleihsystem/src/app/qr/qr.component.ts +++ b/total_tolles_ferleihsystem/src/app/qr/qr.component.ts @@ -1,5 +1,63 @@ import {Component, ViewChild, ViewEncapsulation, OnInit, Output, EventEmitter, OnDestroy, NgZone} from '@angular/core'; -import {Scanner, Camera} from 'instascan'; +import { BrowserQRCodeReader, Result } from '@zxing/library'; +import { Subject, Subscription } from 'rxjs/Rx'; +import { map, filter } from 'rxjs/operators'; + +class QrReader extends BrowserQRCodeReader { + + private scannerResults: Subject = new Subject(); + scannerResultsSubscription: Subscription; + latestScanResult: Result; + + startScanning(deviceId, videoElement, ignoreRepeating: number = 5000) { + this.reset(); + this.prepareVideoElement(videoElement); + let constraints; + if (undefined === deviceId) { + constraints = { + video: { facingMode: 'environment' } + }; + } else { + constraints = { + video: { deviceId: { exact: deviceId } } + }; + } + const decodeOnce = () => { + this.decodeOnceWithDelay( + (result) => this.scannerResults.next(result), + (err) => this.scannerResults.error(err) + ); + }; + this.scannerResultsSubscription = this.scannerResults.asObservable().subscribe(() => { + decodeOnce(); + }); + navigator.mediaDevices.getUserMedia(constraints) + .then((stream) => this.startDecodeFromStream(stream, decodeOnce)) + .catch((error) => this.scannerResults.error(error)); + return this.scannerResults.asObservable().pipe( + filter((result: Result) => { + if (this.latestScanResult != null) { + if (this.latestScanResult.getText() === result.getText()) { + if (this.latestScanResult.getTimestamp() + ignoreRepeating > result.getTimestamp()) { + // skip scan result if last scan was same result and is only + // ignoreRepeating milliseconds from this scan result + return false; + } + } + } + this.latestScanResult = result; + return true; + }) + ); + } + + reset() { + if (this.scannerResultsSubscription != null) { + this.scannerResultsSubscription.unsubscribe(); + } + super.reset() + } +} @Component({ @@ -11,49 +69,64 @@ export class QrComponent implements OnInit, OnDestroy { @Output() scanResult: EventEmitter = new EventEmitter(); - scanner: Scanner; + codeReader: QrReader; cameras: any[]; camera: number = -1; + scannerResults: Subject = new Subject(); + scannerResultsSubscription: Subscription; + lastScan; + isActive = true; + @ViewChild('video') preview; constructor(private zone: NgZone) {} ngOnInit() { - this.scanner = new Scanner({ - video: this.preview.nativeElement, - backgroundScan: true, - scanPeriod: 3, - }); - Camera.getCameras().then((cameras) => { - this.cameras = cameras; + this.codeReader = new QrReader(); + + this.codeReader.getVideoInputDevices().then(videoInputDevices => { + this.cameras = videoInputDevices; this.switchCamera(); - }).catch((e) => { - console.error(e); - }); - this.scanner.addListener('scan', (content) => { - this.zone.run(() => { - const result = content.match(/\/items\/([0-9]+)\/?$/); - if (result != null && result[1] != null) { - this.lastScan = result[1]; - this.scanResult.next(parseInt(result[1], 10)); - } - }); - }); + }).catch(err => console.error(err)); } ngOnDestroy() { - this.scanner.stop(); + this.codeReader.reset(); + if (this.scannerResultsSubscription != null) { + this.scannerResultsSubscription.unsubscribe(); + } } switchCamera() { if (this.cameras.length > 0) { this.camera = (this.camera + 1) % this.cameras.length; - this.scanner.start(this.cameras[this.camera]); + this.startScanning(); } else { console.error('No cameras found.'); } } + + startScanning() { + this.codeReader.reset(); + const cameraId = this.cameras[this.camera].deviceId; + const videoElement = this.preview.nativeElement; + this.scannerResultsSubscription = this.codeReader.startScanning(cameraId, videoElement).pipe( + map((result: Result) => { + return result.getText(); + }), + ).subscribe(this.processScanResult); + } + + processScanResult = (content) => { + this.zone.run(() => { + const result = content.match(/\/items\/([0-9]+)\/?$/); + if (result != null && result[1] != null) { + this.lastScan = result[1]; + this.scanResult.next(parseInt(result[1], 10)); + } + }); + } } diff --git a/total_tolles_ferleihsystem/src/app/search/search.component.html b/total_tolles_ferleihsystem/src/app/search/search.component.html index 82015cb..c10deca 100644 --- a/total_tolles_ferleihsystem/src/app/search/search.component.html +++ b/total_tolles_ferleihsystem/src/app/search/search.component.html @@ -25,6 +25,11 @@ + + Only Lendable Items: + + +
Filter Type: @@ -41,7 +46,7 @@
-
+
Search
@@ -56,12 +61,12 @@
{{letter}}
    -
  • +
  • {{item.name}} {{item.name}} lent - + @@ -71,7 +76,7 @@ Type: - {{item.type?.name}} + {{itemTypes.get(item.type_id)?.name}} Tags: @@ -85,7 +90,7 @@

    - {{attr.attribute_definition?.name}}: + {{attr.attribute_definition | attributeDefinitionTitle}}: {{attr.value}}{{last ? '' : ', '}} diff --git a/total_tolles_ferleihsystem/src/app/search/search.component.ts b/total_tolles_ferleihsystem/src/app/search/search.component.ts index 4b51588..b0932c3 100644 --- a/total_tolles_ferleihsystem/src/app/search/search.component.ts +++ b/total_tolles_ferleihsystem/src/app/search/search.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; +import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, OnChanges } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { QuestionService } from '../shared/forms/question.service'; @@ -13,9 +13,10 @@ import { Subject, Observable, AsyncSubject } from 'rxjs/Rx'; @Component({ selector: 'ttf-search', - templateUrl: './search.component.html' + templateUrl: './search.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) -export class SearchComponent { +export class SearchComponent implements OnChanges { typeQuestion: NumberQuestion = new NumberQuestion(); @@ -24,6 +25,7 @@ export class SearchComponent { searchstring: string = ''; includeDeleted: boolean = false; includeLent: boolean = true; + onlyLendable: boolean = false; type: number; tags: Set; attributes: ApiObject[]; @@ -36,6 +38,7 @@ export class SearchComponent { 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] data: Map; + itemTypes: Map = new Map(); availableLetters: Set; itemTags: Map = new Map(); itemAttributes: Map = new Map(); @@ -45,21 +48,42 @@ export class SearchComponent { @Input() asSelector: boolean = false; @Input() restrictToType: number = -1; + @Input() autoSearch: boolean = false; @Output() selectedChanged: EventEmitter = new EventEmitter(); + private changeDetectionBatchSubject: Subject = new Subject(); + constructor(private api: ApiService, private staging: StagingService, - private qs: QuestionService, private qcs: QuestionControlService) { } + private qs: QuestionService, private qcs: QuestionControlService, + private changeDetector: ChangeDetectorRef) { + this.changeDetectionBatchSubject.asObservable().debounceTime(100).subscribe(() => this.runChangeDetection()); + } + + private runChangeDetection() { + this.changeDetector.markForCheck(); + //this.changeDetector.checkNoChanges(); + } + + ngOnChanges(changes) { + if (changes.restrictToType != null || changes.autoSearch != null) { + if (this.autoSearch) { + this.search(); + } + } + } resetSearchData() { - console.log('RESET'); this.searchDone = false; this.data = new Map(); + this.changeDetectionBatchSubject.next(); } search = () => { this.searchDone = false; + this.changeDetectionBatchSubject.next(); if (this.restrictToType != null && this.restrictToType >= 0) { this.type = this.restrictToType; + this.changeDetectionBatchSubject.next(); } const attributes = new Map(); if (this.attributes != null) { @@ -67,22 +91,24 @@ export class SearchComponent { if (this.attributeForms.get(attr.id).valid && this.attributeForms.get(attr.id).value[attr.name] != null && !(attr.type === 'string' && this.attributeForms.get(attr.id).value[attr.name].length === 0)) { - attributes.set(attr.id, JSON.stringify(this.attributeForms.get(attr.id).value[attr.name])) + let value = JSON.stringify(this.attributeForms.get(attr.id).value[attr.name]); + if (attr.type === 'number' || attr.type === 'integer') { + // since search field was a string get rid of serialized '"' here for now... + // TODO remove if api supports queries like "> 5" for number attributes + value = value.replace(/(^\")|(\"$)/g, ''); + } + attributes.set(attr.id, value); } }); } - this.api.search(this.searchstring, this.type, this.tags, attributes, this.includeDeleted, this.includeLent).subscribe(data => { + this.api.getItemTypes(); // refresh item types cache + this.api.search(this.searchstring, this.type, this.tags, attributes, this.includeDeleted, this.includeLent, this.onlyLendable) + .subscribe(data => { const map = new Map(); const availableLetters = new Set(); this.alphabet.forEach(letter => map.set(letter, [])); this.nrOfItemsFound = data.length; data.forEach(item => { - this.api.getTagsForItem(item, 'errors', this.nrOfItemsFound > 9).take(1).subscribe(tags => { - this.itemTags.set(item.id, tags); - }); - this.api.getAttributes(item, 'errors', this.nrOfItemsFound > 9).take(1).subscribe(attributes => { - this.itemAttributes.set(item.id, attributes); - }); let letter: string = item.name.toUpperCase().substr(0, 1); if (letter === 'Ä') { letter = 'A'; @@ -108,12 +134,14 @@ export class SearchComponent { this.data = map; this.availableLetters = availableLetters; this.searchDone = true; + this.changeDetectionBatchSubject.next(); }); } setFilter(value) { if (value == null || (this.data != null && this.data.get(value) != null && this.data.get(value).length > 0)) { this.filter = value; + this.changeDetectionBatchSubject.next(); } } @@ -130,6 +158,7 @@ export class SearchComponent { .subscribe(attributes => { this.attributes = attributes; attributes.forEach(this.getQuestion); + this.changeDetectionBatchSubject.next(); }); const finished = []; @@ -158,6 +187,12 @@ export class SearchComponent { schema = JSON.parse(attribute_definition.jsonschema); } schema.type = attribute_definition.type; + // if type is number type make it string for supporting '>' queries in the future + if (schema.type === 'number' || schema.type === 'integer') { + schema.type = 'string'; + // add maxLength to get single line text field + schema.maxLength = (window as any).maxDBStringLength - 2; + } schema['x-nullable'] = true; if (attribute_definition.type === 'string') { const maxLength = (window as any).maxDBStringLength - 2; @@ -186,19 +221,22 @@ export class SearchComponent { form.patchValue({ [attribute_definition.name]: value, }); + this.changeDetectionBatchSubject.next(); }); } select(item: ApiObject) { this.selectedChanged.emit(item); + this.changeDetectionBatchSubject.next(); } stageAll(letter?: string) { if (letter == null) { this.data.forEach(items => { items.forEach(item => { - if (item.type.lendable && item.is_currently_lent) { + if (this.itemTypes.has(item._type_id) && this.itemTypes.get(item._type_id).lendable && item.is_currently_lent) { this.staging.stage(item.id); + this.changeDetectionBatchSubject.next(); } }); }); @@ -206,11 +244,32 @@ export class SearchComponent { const items = this.data.get(letter); if (items != null) { items.forEach(item => { - if (item.type.lendable && item.is_currently_lent) { + if (this.itemTypes.has(item._type_id) && this.itemTypes.get(item._type_id).lendable && item.is_currently_lent) { this.staging.stage(item.id); + this.changeDetectionBatchSubject.next(); } }); } } } + + /** + * Load further data for items in view. + * + * @param item the item that was scrolled into view + */ + loadData(item) { + this.api.getItemType(item.type_id, 'all', true).take(1).subscribe(itemType => { + this.itemTypes.set(itemType.id, itemType); + this.changeDetectionBatchSubject.next(); + }); + this.api.getTagsForItem(item, 'errors', true).take(1).subscribe(tags => { + this.itemTags.set(item.id, tags); + this.changeDetectionBatchSubject.next(); + }); + this.api.getAttributes(item, 'errors', true).take(1).subscribe(attributes => { + this.itemAttributes.set(item.id, attributes); + this.changeDetectionBatchSubject.next(); + }); + } } diff --git a/total_tolles_ferleihsystem/src/app/settings/settings.component.html b/total_tolles_ferleihsystem/src/app/settings/settings.component.html index 33eeaaf..89ad8ce 100644 --- a/total_tolles_ferleihsystem/src/app/settings/settings.component.html +++ b/total_tolles_ferleihsystem/src/app/settings/settings.component.html @@ -39,6 +39,12 @@

    User Settings:

    15 seconds 1 minute

    +

    + Pinned Item Types: {{pinnedTypes|json}} +

    +

    + {{isTypePinned(type) ? 'Unpin' : 'Pin'}} item type "{{type.name}}" to overview. +

    diff --git a/total_tolles_ferleihsystem/src/app/settings/settings.component.ts b/total_tolles_ferleihsystem/src/app/settings/settings.component.ts index c3395d1..4ae0377 100644 --- a/total_tolles_ferleihsystem/src/app/settings/settings.component.ts +++ b/total_tolles_ferleihsystem/src/app/settings/settings.component.ts @@ -4,6 +4,7 @@ import { NavigationService, Breadcrumb } from '../navigation/navigation-service' import { SettingsService } from '../shared/settings/settings.service'; import { Themes } from './themes'; +import { ApiService } from 'app/shared/rest/api.service'; @Component({ selector: 'ttf-settings', @@ -15,11 +16,14 @@ export class SettingsComponent implements OnInit { navigateAfterCreation: boolean; theme: any = {}; themeId: number = 0; - infoTimeout: -1; - alertTimeout: -1; - errorTimeout: -1; + infoTimeout: number = -1; + alertTimeout: number = -1; + errorTimeout: number = -1; + pinnedTypes: number[] = []; - constructor(private data: NavigationService, private settings: SettingsService) { } + itemTypes = []; + + constructor(private data: NavigationService, private settings: SettingsService, private api: ApiService) { } ngOnInit(): void { this.data.changeTitle('Total Tolles Ferleihsystem – Settings'); @@ -55,6 +59,26 @@ export class SettingsComponent implements OnInit { } this.errorTimeout = timeout; }); + this.settings.getSetting('pinnedItemTypes').subscribe(pinnedTypes => { + if (pinnedTypes == null) { + pinnedTypes = []; + } + this.pinnedTypes = pinnedTypes; + }); + this.api.getItemTypes().subscribe(itemTypes => this.itemTypes = itemTypes); + } + + isTypePinned(type) { + return this.pinnedTypes.some(typeId => type.id === typeId); + } + + toggleType(type) { + if (this.isTypePinned(type)) { + this.settings.setSetting('pinnedItemTypes', this.pinnedTypes.filter(typeId => typeId !== type.id)); + } else { + this.pinnedTypes.push(type.id); + this.settings.setSetting('pinnedItemTypes', this.pinnedTypes); + } } changeColor() { diff --git a/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dropdown-input/dropdown-input.component.html b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dropdown-input/dropdown-input.component.html new file mode 100644 index 0000000..6c9b27c --- /dev/null +++ b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dropdown-input/dropdown-input.component.html @@ -0,0 +1,8 @@ +
    + + + + +
    diff --git a/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dropdown-input/dropdown-input.component.scss b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dropdown-input/dropdown-input.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dropdown-input/dropdown-input.component.ts b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dropdown-input/dropdown-input.component.ts new file mode 100644 index 0000000..685146c --- /dev/null +++ b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dropdown-input/dropdown-input.component.ts @@ -0,0 +1,80 @@ +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +import { QuestionBase } from '../../question-base'; + + + +@Component({ + selector: 'ttf-dropdown-input', + templateUrl: 'dropdown-input.component.html', + styleUrls: ['dropdown-input.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DropdownInputComponent), + multi: true + }] +}) +export class DropdownInputComponent implements ControlValueAccessor { + + @Input() question: QuestionBase; + + searchString: string = ''; + + chosenOption: string = ''; + + onChange: any = () => {}; + + onTouched: any = () => {}; + + get value(): string { + if (this.chosenOption == null || this.chosenOption === '') { + return this.question.nullValue; + } else { + return this.chosenOption; + } + } + + @Input() + set value(val: string) { + if (val === this.question.nullValue) { + this.chosenOption = ''; + } else { + this.chosenOption = val; + this.searchString = val; + } + this.onChange(val); + this.onTouched(); + } + + updateValue(searchString) { + if (this.question != null && this.question.options != null) { + this.question.options.forEach(option => { + if (option === searchString) { + this.chosenOption = option; + this.update(); + return; + } + }); + } + } + + update() { + this.onChange(this.value); + this.onTouched(); + } + + registerOnChange(fn) { + this.onChange = fn; + } + + registerOnTouched(fn) { + this.onTouched = fn; + } + + writeValue(value) { + if (value) { + this.value = value; + } + } +} diff --git a/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dynamic-form-question.component.html b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dynamic-form-question.component.html index 27e0c05..1778a35 100644 --- a/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dynamic-form-question.component.html +++ b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dynamic-form-question.component.html @@ -38,12 +38,7 @@ -
    - - -
    + diff --git a/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dynamic-form.component.ts b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dynamic-form.component.ts index 10964b0..2c2db04 100644 --- a/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dynamic-form.component.ts +++ b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/dynamic-form.component.ts @@ -44,6 +44,16 @@ export class DynamicFormComponent implements OnInit, OnChanges { this.form.statusChanges.subscribe(status => { this.valid.emit(this.form.valid); this.data.emit(this.form.value); + if (this.objectModel === 'ItemPOST') { + // When posting an item that should have automatic name generation + // insert current time as temporary name + const currentVal = this.form.value; + if (currentVal.update_name_from_schema && + (currentVal.name == null || currentVal.name === '')) { + const currentDate = new Date(); + this.form.patchValue({'name': currentDate.toISOString()}); + } + } }); this.patchFormValues(); }); diff --git a/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/number-input/number-input.component.ts b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/number-input/number-input.component.ts index 2d62f17..29a97a0 100644 --- a/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/number-input/number-input.component.ts +++ b/total_tolles_ferleihsystem/src/app/shared/forms/dynamic-form/number-input/number-input.component.ts @@ -31,7 +31,6 @@ export class NumberInputComponent implements ControlValueAccessor { if (this.question.valueType === 'integer') { return parseInt(this._value, 10); } else { - console.log(this._value); return parseFloat(this._value); } } diff --git a/total_tolles_ferleihsystem/src/app/shared/is-visible.directive.ts b/total_tolles_ferleihsystem/src/app/shared/is-visible.directive.ts new file mode 100644 index 0000000..7783fd7 --- /dev/null +++ b/total_tolles_ferleihsystem/src/app/shared/is-visible.directive.ts @@ -0,0 +1,27 @@ +import { Directive, ElementRef, Output, EventEmitter, HostListener } from '@angular/core'; + +@Directive({ + selector: '[isVisible]' +}) +export class IsVisibleDirective { + @Output() isVisible: EventEmitter = new EventEmitter(); + + intersectionObserverOptions = { + root: null, + rootMargin: '150px 0px 300px 0px', + threshold: 0.25 + } + + constructor(private _elementRef: ElementRef) { + const observer = new IntersectionObserver((x) => { + const isVisible = x.some((entry) => entry.intersectionRatio > 0); + if (isVisible) { + observer.disconnect(); + this.isVisible.emit(); + } + }, this.intersectionObserverOptions); + + // provice the observer with a target + observer.observe(_elementRef.nativeElement); + } +} diff --git a/total_tolles_ferleihsystem/src/app/shared/rest/api.service.ts b/total_tolles_ferleihsystem/src/app/shared/rest/api.service.ts index bd547c0..b17f09c 100644 --- a/total_tolles_ferleihsystem/src/app/shared/rest/api.service.ts +++ b/total_tolles_ferleihsystem/src/app/shared/rest/api.service.ts @@ -1,5 +1,5 @@ import { Injectable, OnInit, Injector } from '@angular/core'; -import { Observable, } from 'rxjs/Rx'; +import { Observable, Subject, } from 'rxjs/Rx'; import { BaseApiService, ApiObject, LinkObject, ApiLinksObject } from './api-base.service'; import { JWTService } from './jwt.service'; import { InfoService } from '../info/info.service'; @@ -81,6 +81,7 @@ export class ApiService implements OnInit { this.jwtSource.complete(); this.getRoot(); this.getCatalog(); + this.getItemTypes(); this.getAuthRoot(); } @@ -215,9 +216,9 @@ export class ApiService implements OnInit { getSettings(): Observable { const stream = new AsyncSubject(); - this.currentJWT.subscribe(jwt => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getAuthRoot().subscribe(auth => { - this.rest.get(auth._links.settings, jwt.token()).subscribe(data => { + this.rest.get(auth._links.settings, token).subscribe(data => { stream.next((data as any).settings); stream.complete(); }); @@ -228,9 +229,9 @@ export class ApiService implements OnInit { updateSettings(settings: string): Observable { const stream = new AsyncSubject(); - this.currentJWT.subscribe(jwt => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getAuthRoot().subscribe(auth => { - this.rest.put(auth._links.settings, {'settings': settings}, jwt.token()).subscribe(data => { + this.rest.put(auth._links.settings, {'settings': settings}, token).subscribe(data => { stream.next((data as any).settings); stream.complete(); }); @@ -240,7 +241,8 @@ export class ApiService implements OnInit { } search(search: string, type?: number, tags?: Set, - attributes?: Map, deleted?: boolean, lent?: boolean): Observable> { + attributes?: Map, deleted?: boolean, + lent?: boolean, lendableOnly?: boolean): Observable> { const stream = new AsyncSubject>(); const params: any = {search: search}; @@ -269,7 +271,11 @@ export class ApiService implements OnInit { params.lent = lent; } - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + if (lendableOnly != null) { + params.lendable = lendableOnly; + } + + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getRoot().subscribe((root) => { this.rest.get(root._links.search, token, params).subscribe(data => { stream.next(data as ApiObject[]); @@ -351,10 +357,16 @@ export class ApiService implements OnInit { } const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.get(catalog._links.item_types, token, params).subscribe(data => { stream.next(data); + + // insert item types into detail cache streams + (data as ApiObject[]).forEach(itemType => { + const detailStream = this.getStreamSource(resource + '/' + itemType.id); + detailStream.next(itemType); + }); }, error => this.errorHandler(error, resource, 'GET', showErrors)); }, error => this.errorHandler(error, resource, 'GET', showErrors)); }); @@ -362,18 +374,20 @@ export class ApiService implements OnInit { return (stream.asObservable() as Observable).filter(data => data != null); } - getItemType(id: number, showErrors: string= 'all'): Observable { + getItemType(id: number, showErrors: string= 'all', preferCache: boolean= false): Observable { const baseResource = 'item_types'; const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { - this.getCatalog().subscribe((catalog) => { - this.rest.get(catalog._links.item_types.href + id, token).subscribe(data => { - this.updateResource(baseResource, data as ApiObject); + if (!(preferCache && stream.value != null)) { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { + this.getCatalog().subscribe((catalog) => { + this.rest.get(catalog._links.item_types.href + id, token).subscribe(data => { + this.updateResource(baseResource, data as ApiObject); + }, error => this.errorHandler(error, resource, 'GET', showErrors)); }, error => this.errorHandler(error, resource, 'GET', showErrors)); - }, error => this.errorHandler(error, resource, 'GET', showErrors)); - }); + }); + } return (stream.asObservable() as Observable).filter(data => data != null); } @@ -381,7 +395,7 @@ export class ApiService implements OnInit { postItemType(newData, showErrors: string= 'all'): Observable { const resource = 'item_types'; - return this.currentJWT.map(jwt => jwt.token()).flatMap(token => { + return this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).flatMap(token => { return this.getCatalog().flatMap(catalog => { return this.rest.post(catalog._links.item_types, newData, token).flatMap(data => { const stream = this.getStreamSource(resource + '/' + data.id); @@ -401,7 +415,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.put(catalog._links.item_types.href + id + '/', newData, token).subscribe(data => { this.updateResource(baseResource, data as ApiObject); @@ -417,7 +431,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.delete(catalog._links.item_types.href + id + '/', token).subscribe(() => { this.removeResource(baseResource, id); @@ -434,7 +448,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.post(catalog._links.item_types.href + id + '/', undefined, token).subscribe(() => { this.getItemType(id); @@ -446,12 +460,12 @@ export class ApiService implements OnInit { return (stream.asObservable() as Observable).filter(data => data != null); } - getCanContain(item_type: ApiObject, showErrors: string= 'all'): Observable { - const resource = 'item-types/' + item_type.id + '/can-contain'; + getContainedTypes(item_type: ApiObject, showErrors: string= 'all'): Observable { + const resource = 'item-types/' + item_type.id + '/contained-types'; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { - this.rest.get(item_type._links.can_contain, token).subscribe(data => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { + this.rest.get(item_type._links.contained_types, token).subscribe(data => { stream.next(data); }, error => this.errorHandler(error, resource, 'GET', showErrors)); }); @@ -459,12 +473,12 @@ export class ApiService implements OnInit { return (stream.asObservable() as Observable); } - postCanContain(item_type: ApiObject, itemTypeID, showErrors: string= 'all'): Observable { - const resource = 'item-types/' + item_type.id + '/can-contain'; + postContainedType(item_type: ApiObject, itemTypeID, showErrors: string= 'all'): Observable { + const resource = 'item-types/' + item_type.id + '/contained-types'; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { - this.rest.post(item_type._links.can_contain, {id: itemTypeID}, token).subscribe(data => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { + this.rest.post(item_type._links.contained_types, {id: itemTypeID}, token).subscribe(data => { stream.next(data); }, error => this.errorHandler(error, resource, 'POST', showErrors)); }); @@ -472,13 +486,13 @@ export class ApiService implements OnInit { return (stream.asObservable() as Observable); } - deleteCanContain(item_type: ApiObject, itemTypeID, showErrors: string= 'all'): Observable { - const resource = 'item-types/' + item_type.id + '/can-contain'; + deleteContainedType(item_type: ApiObject, itemTypeID, showErrors: string= 'all'): Observable { + const resource = 'item-types/' + item_type.id + '/contained-types'; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { - this.rest.delete(item_type._links.can_contain, token, {id: itemTypeID}).subscribe(data => { - this.getCanContain(item_type); + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { + this.rest.delete(item_type._links.contained_types, token, {id: itemTypeID}).subscribe(data => { + this.getContainedTypes(item_type); }, error => this.errorHandler(error, resource, 'DELETE', showErrors)); }); @@ -497,7 +511,7 @@ export class ApiService implements OnInit { } const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.get(catalog._links.item_tags, token, params).subscribe(data => { stream.next(data); @@ -513,7 +527,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.get(catalog._links.item_tags.href + id, token).subscribe(data => { this.updateResource(baseResource, data as ApiObject); @@ -527,7 +541,7 @@ export class ApiService implements OnInit { postTag(newData, showErrors: string= 'all'): Observable { const resource = 'tags'; - return this.currentJWT.map(jwt => jwt.token()).flatMap(token => { + return this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).flatMap(token => { return this.getCatalog().flatMap(catalog => { return this.rest.post(catalog._links.item_tags, newData, token).flatMap(data => { const stream = this.getStreamSource(resource + '/' + data.id); @@ -547,7 +561,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.put(catalog._links.item_tags.href + id + '/', newData, token).subscribe(data => { this.updateResource(baseResource, data as ApiObject); @@ -563,7 +577,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.delete(catalog._links.item_tags.href + id + '/', token).subscribe(() => { this.removeResource(baseResource, id); @@ -579,7 +593,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.post(catalog._links.item_tags.href + id + '/', undefined, token).subscribe(() => { this.getTag(id); @@ -603,7 +617,7 @@ export class ApiService implements OnInit { } const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.get(catalog._links.attribute_definitions, token, params).subscribe(data => { stream.next(data); @@ -619,7 +633,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.get(catalog._links.attribute_definitions.href + id, token).subscribe(data => { this.updateResource(baseResource, data as ApiObject); @@ -633,7 +647,7 @@ export class ApiService implements OnInit { postAttributeDefinition(newData, showErrors: string= 'all'): Observable { const resource = 'attribute_definitions'; - return this.currentJWT.map(jwt => jwt.token()).flatMap(token => { + return this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).flatMap(token => { return this.getCatalog().flatMap(catalog => { return this.rest.post(catalog._links.attribute_definitions, newData, token).flatMap(data => { const stream = this.getStreamSource(resource + '/' + data.id); @@ -653,7 +667,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.put(catalog._links.attribute_definitions.href + id + '/', newData, token).subscribe(data => { this.updateResource(baseResource, data as ApiObject); @@ -669,7 +683,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.delete(catalog._links.attribute_definitions.href + id + '/', token).subscribe(() => { this.removeResource(baseResource, id); @@ -685,7 +699,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.post(catalog._links.attribute_definitions.href + id + '/', undefined, token).subscribe(() => { this.getAttributeDefinition(id); @@ -700,14 +714,16 @@ export class ApiService implements OnInit { getAttributeAutocomplete(attrDef: ApiObject, showErrors: string= 'all'): Observable { const resource = 'attribute_definitions/' + attrDef.id + '/autocomplete'; const stream: BehaviorSubject = this.getStreamSource(resource) as any; - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.get(attrDef._links.autocomplete, token).subscribe(data => { const parsed = []; - (data as any[]).forEach(element => { - try { - parsed.push(JSON.parse(element as string)); - } catch (error) {} - }); + if (data != null && (data as any[]).length > 0) { + (data as any[]).forEach(element => { + try { + parsed.push(JSON.parse(element as string)); + } catch (error) {} + }); + } stream.next(parsed as any); }); }); @@ -722,7 +738,7 @@ export class ApiService implements OnInit { const resource = url.replace(/(^.*catalog\/)|(\/$)/, ''); const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.get(url, token).subscribe(data => { stream.next(data); }, error => this.errorHandler(error, resource, 'GET', showErrors)); @@ -739,7 +755,7 @@ export class ApiService implements OnInit { const resource = url.replace(/(^.*catalog\/)|(\/$)/, ''); const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.post(url, attributeDefinition, token).subscribe(data => { stream.next(data); }, error => this.errorHandler(error, resource, 'POST', showErrors)); @@ -757,7 +773,7 @@ export class ApiService implements OnInit { const resource = baseResource + attributeDefinition.id; const stream = this.getStreamSource(baseResource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.delete(url, token, attributeDefinition).subscribe(data => { this.getLinkedAttributeDefinitions(linkedObject); }, error => this.errorHandler(error, resource, 'DELETE', showErrors)); @@ -778,7 +794,7 @@ export class ApiService implements OnInit { } const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.get(catalog._links.items, token, params).subscribe(data => { stream.next(data); @@ -794,7 +810,7 @@ export class ApiService implements OnInit { const params = {lent: true}; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.get(catalog._links.items, token, params).subscribe(data => { (data as ApiObject[]).sort((a, b) => { @@ -804,8 +820,8 @@ export class ApiService implements OnInit { if (b.due == null || b.due == '') { return 1; } - const d_a = new Date(a.due); - const d_b = new Date(b.due); + const d_a = new Date(a.due * 1000); + const d_b = new Date(b.due * 1000); if (d_a < d_b) { return -1; } else if (d_a > d_b) { @@ -827,7 +843,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.get(catalog._links.items.href + id, token).subscribe(data => { this.updateResource(baseResource, data as ApiObject); @@ -841,7 +857,7 @@ export class ApiService implements OnInit { postItem(newData, showErrors: string= 'all'): Observable { const resource = 'items'; - return this.currentJWT.map(jwt => jwt.token()).flatMap(token => { + return this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).flatMap(token => { return this.getCatalog().flatMap(catalog => { return this.rest.post(catalog._links.items, newData, token).flatMap(data => { const stream = this.getStreamSource(resource + '/' + data.id); @@ -861,7 +877,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { // reset dependent caches! this.getStreamSource(resource + '/attributes').next([]); @@ -879,7 +895,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.delete(catalog._links.items.href + id + '/', token).subscribe(() => { this.removeResource(baseResource, id); @@ -896,7 +912,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe((catalog) => { this.rest.post(catalog._links.items.href + id + '/', undefined, token).subscribe(() => { this.getItem(id); @@ -913,7 +929,7 @@ export class ApiService implements OnInit { const stream = this.getStreamSource(resource); if (!(preferCache && stream.value != null)) { - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.get(item._links.tags.href, token).subscribe(data => { stream.next(data); }, error => this.errorHandler(error, resource, 'GET', showErrors)); @@ -927,7 +943,7 @@ export class ApiService implements OnInit { const resource = 'items/' + item.id + '/tags'; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.post(item._links.tags.href, tag, token).subscribe(data => { stream.next(data); this.getAttributes(item); @@ -941,7 +957,7 @@ export class ApiService implements OnInit { const resource = 'items/' + item.id + '/tags'; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.delete(item._links.tags.href, token, tag).subscribe(data => { stream.next(data); this.getTagsForItem(item); @@ -956,7 +972,7 @@ export class ApiService implements OnInit { const resource = 'items/' + item.id + '/contained-items'; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.get(item._links.contained_items, token).subscribe(data => { stream.next(data); }, error => this.errorHandler(error, resource, 'GET', showErrors)); @@ -969,7 +985,7 @@ export class ApiService implements OnInit { const resource = 'items/' + item.id + '/contained-items'; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.post(item._links.contained_items.href, {id: itemID}, token).subscribe(data => { stream.next(data); }, error => this.errorHandler(error, resource, 'POST', showErrors)); @@ -982,7 +998,7 @@ export class ApiService implements OnInit { const resource = 'items/' + item.id + '/contained-items'; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.delete(item._links.contained_items.href, token, {id: itemID}).subscribe(data => { this.getContainedItems(item); }, error => this.errorHandler(error, resource, 'DELETE', showErrors)); @@ -1005,7 +1021,7 @@ export class ApiService implements OnInit { const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { if (item != null) { this.rest.get(url, token).subscribe(data => { stream.next(data); @@ -1029,7 +1045,7 @@ export class ApiService implements OnInit { const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe(catalog => { this.rest.get(catalog._links.files.href + fileID, token).subscribe(data => { this.updateResource(baseResource, data as ApiObject); @@ -1048,7 +1064,7 @@ export class ApiService implements OnInit { formData.append('item_id', item.id); formData.append('file', file); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe(catalog => { this.rest.uploadFile(catalog._links.files, formData, token).subscribe(data => { stream.next(data); @@ -1063,7 +1079,7 @@ export class ApiService implements OnInit { downloadFile(file: ApiObject, showErrors: string= 'all') { const stream = new AsyncSubject(); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.downloadFile(file._links.download, token).subscribe(data => { stream.next(data); stream.complete(); @@ -1072,9 +1088,7 @@ export class ApiService implements OnInit { stream.subscribe(data => { const dispositonHeader = data.headers.get('content-disposition') - console.log(data); const blob = new Blob([data.blob()], {type: 'application/pdf'}); //octet-stream - console.log(blob); saveAs(blob, dispositonHeader.length > 25 ? dispositonHeader.substring(21) : file.name + file.file_type); }) } @@ -1084,7 +1098,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getCatalog().subscribe(catalog => { this.rest.put(catalog._links.files.href + id + '/', data, token).subscribe(data => { this.updateResource(baseResource, data as ApiObject); @@ -1100,7 +1114,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + file.id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.delete(file, token).subscribe(data => { stream.next(null); this.getFiles(item, showErrors); @@ -1118,7 +1132,7 @@ export class ApiService implements OnInit { const stream = this.getStreamSource(resource); if (!(preferCache && stream.value != null)) { - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.get(item._links.attributes, token).subscribe(data => { stream.next(data); }, error => this.errorHandler(error, resource, 'GET', showErrors)); @@ -1133,7 +1147,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.get(item._links.attributes.href + id + '/', token).subscribe(data => { this.updateResource(baseResource, data as ApiObject, 'attribute_definition_id'); }, error => this.errorHandler(error, resource, 'GET', showErrors)); @@ -1147,7 +1161,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.put(item._links.attributes.href + id + '/', {value: value}, token).subscribe(data => { this.updateResource(baseResource, data as ApiObject, 'attribute_definition_id'); this.getItem(item.id); @@ -1164,7 +1178,7 @@ export class ApiService implements OnInit { const resource = 'lendings'; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getRoot().subscribe((root) => { this.rest.get(root._links.lending, token).subscribe(data => { stream.next(data); @@ -1178,7 +1192,7 @@ export class ApiService implements OnInit { postLending(newData, showErrors: string= 'all'): Observable { const resource = 'lendings'; - return this.currentJWT.map(jwt => jwt.token()).flatMap(token => { + return this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).flatMap(token => { return this.getRoot().flatMap(root => { return this.rest.post(root._links.lending, newData, token).flatMap(data => { const stream = this.getStreamSource(resource + '/' + data.id); @@ -1199,7 +1213,7 @@ export class ApiService implements OnInit { const resource = baseResource + '/' + id; const stream = this.getStreamSource(resource); - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.getRoot().subscribe((root) => { this.rest.get(root._links.lending.href + id, token).subscribe(data => { this.updateResource(baseResource, data as ApiObject); @@ -1210,7 +1224,16 @@ export class ApiService implements OnInit { return (stream.asObservable() as Observable).filter(data => data != null); } - returnLending(lending: ApiObject, id?: number, showErrors: string= 'all'): Observable { + /** + * Returns items or the whole lending. + * + * The returned observable evaluates to true only if the lending was deleted by the api. + * + * @param lending the lending to return + * @param id a single item id to return from the lending (if unset return all items) + * @param showErrors whether to show errors to the user + */ + returnLending(lending: ApiObject, id?: number, showErrors: string= 'all'): Observable { const baseResource = 'lendings'; const resource = baseResource + '/' + lending.id; const stream = this.getStreamSource(resource); @@ -1220,19 +1243,34 @@ export class ApiService implements OnInit { if (id != null) { data.ids.push(id); } else { - lending.itemLendings.forEach(itemLending => { - data.ids.push(itemLending.item.id); + lending.items.forEach(item => { + data.ids.push(item.id); }); } - this.currentJWT.map(jwt => jwt.token()).subscribe(token => { + const result = new AsyncSubject(); + + this.currentJWT.flatMap(jwt => jwt.getValidApiToken()).subscribe(token => { this.rest.post(lending, data, token).subscribe(data => { - this.updateResource(baseResource, data as ApiObject); + if (data != null) { + this.updateResource(baseResource, data as ApiObject); + result.next(false); + } else { + // cleanup old stream + result.next(true); + this.streams[resource] = null; + stream.next(null); + } + result.complete(); this.getLentItems(); - }, error => this.errorHandler(error, resource, 'GET', showErrors)); + }, error => { + result.error(error); + result.complete(); + this.errorHandler(error, resource, 'GET', showErrors) + }); }); - return (stream.asObservable() as Observable).filter(data => data != null); + return result.asObservable(); } } diff --git a/total_tolles_ferleihsystem/src/app/shared/rest/jwt.service.ts b/total_tolles_ferleihsystem/src/app/shared/rest/jwt.service.ts index e22c4a0..aaa89f2 100644 --- a/total_tolles_ferleihsystem/src/app/shared/rest/jwt.service.ts +++ b/total_tolles_ferleihsystem/src/app/shared/rest/jwt.service.ts @@ -10,6 +10,8 @@ export class JWTService implements OnInit { private sessionExpirySource = new Subject(); readonly sessionExpiry = this.sessionExpirySource.asObservable(); + private apiTokenSource = new BehaviorSubject(null); + private userSource = new BehaviorSubject(undefined); readonly user = this.userSource.asObservable(); @@ -18,6 +20,7 @@ export class JWTService implements OnInit { private api: ApiService; + constructor (private injector: Injector, private router: Router) { Observable.timer(1).take(1).subscribe((() => { this.ngOnInit() @@ -33,20 +36,25 @@ export class JWTService implements OnInit { future = new Date(future.getTime() + (3 * 60 * 1000)) if (this.expiration(this.token()) < future) { this.api.refreshLogin(this.refreshToken()); + } else { + this.apiTokenSource.next(this.token()); } if (this.expiration(this.refreshToken()) < future) { this.sessionExpirySource.next(true); + this.apiTokenSource = new BehaviorSubject(null); } } }).bind(this)); if (!this.loggedIn()) { this.userSource.next(undefined); + this.apiTokenSource = new BehaviorSubject(null); this.api.guestLogin(); } } updateTokens(loginToken: string, refreshToken?: string) { localStorage.setItem(this.TOKEN, loginToken); + this.apiTokenSource.next(loginToken); if (refreshToken != null) { localStorage.setItem(this.REFRESH_TOKEN, refreshToken); } @@ -60,6 +68,7 @@ export class JWTService implements OnInit { logout() { this.userSource.next(undefined); + this.apiTokenSource = new BehaviorSubject(null); localStorage.removeItem(this.TOKEN); localStorage.removeItem(this.REFRESH_TOKEN); this.api.guestLogin(); @@ -78,6 +87,21 @@ export class JWTService implements OnInit { return localStorage.getItem(this.REFRESH_TOKEN); } + getValidApiToken() { + return this.apiTokenSource.asObservable().filter(token => { + if (token == null) { //filter out null + return false; + } + // filter out expired tokens + let future = new Date(); + future = new Date(future.getTime() + (60 * 1000)) + if (this.expiration(this.token()) < future) { + return false; + } + return true; + }).take(1); + } + private tokenToJson(token: string) { return JSON.parse(atob(token.split('.')[1])); } diff --git a/total_tolles_ferleihsystem/src/app/shared/shared.module.ts b/total_tolles_ferleihsystem/src/app/shared/shared.module.ts index 5f74795..bf45db6 100644 --- a/total_tolles_ferleihsystem/src/app/shared/shared.module.ts +++ b/total_tolles_ferleihsystem/src/app/shared/shared.module.ts @@ -11,6 +11,7 @@ import { NumberInputComponent } from './forms/dynamic-form/number-input/number-i import { DateInputComponent } from './forms/dynamic-form/date-input/date-input.component'; import { DateTimeInputComponent } from './forms/dynamic-form/date-input/datetime-input.component'; import { DurationInputComponent } from './forms/dynamic-form/duration-input/duration-input.component'; +import { DropdownInputComponent } from './forms/dynamic-form/dropdown-input/dropdown-input.component'; import { QuestionControlService } from './forms/question-control.service'; import { QuestionService } from './forms/question.service'; @@ -34,6 +35,7 @@ import { myTableComponent } from './table/table.component'; import { FileSelectorComponent } from './file-selector/file-selector.component'; import { ClickOutsideDirective } from './click-outside.directive'; +import { IsVisibleDirective } from './is-visible.directive'; @NgModule({ imports: [ CommonModule, FormsModule, ReactiveFormsModule ], @@ -53,7 +55,9 @@ import { ClickOutsideDirective } from './click-outside.directive'; DateInputComponent, DateTimeInputComponent, DurationInputComponent, + DropdownInputComponent, ClickOutsideDirective, + IsVisibleDirective, ], providers: [ InfoService, @@ -83,7 +87,9 @@ import { ClickOutsideDirective } from './click-outside.directive'; DateInputComponent, DateTimeInputComponent, DurationInputComponent, + DropdownInputComponent, ClickOutsideDirective, + IsVisibleDirective, CommonModule, FormsModule, diff --git a/total_tolles_ferleihsystem/src/app/staging/staged-item.component.html b/total_tolles_ferleihsystem/src/app/staging/staged-item.component.html index 456ba01..abdb885 100644 --- a/total_tolles_ferleihsystem/src/app/staging/staged-item.component.html +++ b/total_tolles_ferleihsystem/src/app/staging/staged-item.component.html @@ -23,7 +23,7 @@

    Tags: {{tag.name}}{{isLast ? '' : ', '}}

    Lending Duration:

    Attributes:

    -

    {{attr.attribute_definition?.name}}: {{attr?.value}}

    +

    {{attr.attribute_definition | attributeDefinitionTitle}}: {{attr?.value}}

    diff --git a/total_tolles_ferleihsystem/src/app/tags/tags-overview.component.ts b/total_tolles_ferleihsystem/src/app/tags/tags-overview.component.ts index 18c853a..4df8470 100644 --- a/total_tolles_ferleihsystem/src/app/tags/tags-overview.component.ts +++ b/total_tolles_ferleihsystem/src/app/tags/tags-overview.component.ts @@ -27,11 +27,12 @@ export class TagsOverviewComponent implements OnInit { } save = () => { - this.api.postTag(this.newTagData).subscribe(data => { + const sub = this.api.postTag(this.newTagData).subscribe(data => { this.settings.getSetting('navigateAfterCreation').take(1).subscribe(navigate => { if (navigate) { this.router.navigate(['tags', data.id]); } + sub.unsubscribe(); }); }); }; diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/LICENSE.txt b/total_tolles_ferleihsystem/src/assets/Roboto/LICENSE.txt new file mode 100644 index 0000000..75b5248 --- /dev/null +++ b/total_tolles_ferleihsystem/src/assets/Roboto/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Black.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Black.ttf new file mode 100644 index 0000000..51c71bb Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Black.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-BlackItalic.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-BlackItalic.ttf new file mode 100644 index 0000000..ca20ca3 Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-BlackItalic.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Bold.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Bold.ttf new file mode 100644 index 0000000..e612852 Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Bold.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-BoldItalic.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..677bc04 Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-BoldItalic.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Italic.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Italic.ttf new file mode 100644 index 0000000..5fd05c3 Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Italic.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Light.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Light.ttf new file mode 100644 index 0000000..4f1fb58 Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Light.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-LightItalic.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-LightItalic.ttf new file mode 100644 index 0000000..eec0ae9 Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-LightItalic.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Medium.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Medium.ttf new file mode 100644 index 0000000..86d1c52 Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Medium.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-MediumItalic.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-MediumItalic.ttf new file mode 100644 index 0000000..66aa174 Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-MediumItalic.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Regular.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Regular.ttf new file mode 100644 index 0000000..cb8ffcf Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Regular.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Thin.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Thin.ttf new file mode 100644 index 0000000..a85eb7c Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-Thin.ttf differ diff --git a/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-ThinItalic.ttf b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-ThinItalic.ttf new file mode 100644 index 0000000..ac77951 Binary files /dev/null and b/total_tolles_ferleihsystem/src/assets/Roboto/Roboto-ThinItalic.ttf differ diff --git a/total_tolles_ferleihsystem/src/styles.scss b/total_tolles_ferleihsystem/src/styles.scss index fd92c75..a320a55 100644 --- a/total_tolles_ferleihsystem/src/styles.scss +++ b/total_tolles_ferleihsystem/src/styles.scss @@ -4,7 +4,79 @@ @import "./variables"; @import '../node_modules/font-awesome/scss/font-awesome'; @import "../node_modules/tachyons/css/tachyons"; -@import url('https://fonts.googleapis.com/css?family=Roboto'); + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 100; + src: local('Roboto'), local('Roboto-Thin'), url(./assets/Roboto/Roboto-Thin.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 100; + src: local('Roboto'), local('Roboto-ThinItalic'), url(./assets/Roboto/Roboto-ThinItalic.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: local('Roboto-Light'), url(./assets/Roboto/Roboto-Light.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 300; + src: local('Roboto-LightItalic'), url(./assets/Roboto/Roboto-LightItalic.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto-Regular'), local('Roboto'), url(./assets/Roboto/Roboto-Regular.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 400; + src: local('Roboto-Italic'), url(./assets/Roboto/Roboto-Italic.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto-Medium'), url(./assets/Roboto/Roboto-Medium.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 500; + src: local('Roboto-MediumItalic'), url(./assets/Roboto/Roboto-MediumItalic.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + src: local('Roboto-Bold'), url(./assets/Roboto/Roboto-Bold.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 700; + src: local('Roboto-BoldItalic'), url(./assets/Roboto/Roboto-BoldItalic.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + src: local('Roboto-Black'), url(./assets/Roboto/Roboto-Black.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: italic; + font-weight: 900; + src: local('Roboto-BlackItalic'), url(./assets/Roboto/Roboto-BlackItalic.ttf) format('truetype'); +} :root { --background-color: #ffffff; @@ -222,4 +294,3 @@ body { .tooltip-bottom:not(.tooltip)::after, .tooltip-bottom:not(.tooltip)::before { display: none; } -