Add inventories with dispatcher
This commit is contained in:
parent
79feb33aa3
commit
f570e9d3d0
|
@ -0,0 +1,28 @@
|
|||
import os
|
||||
|
||||
import click.testing
|
||||
import flask.cli
|
||||
|
||||
from ereuse_devicehub.config import DevicehubConfig
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
|
||||
|
||||
class DevicehubGroup(flask.cli.FlaskGroup):
|
||||
CONFIG = DevicehubConfig
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
# todo this should be taken as an argument for the cli
|
||||
inventory = os.environ.get('dhi')
|
||||
if not inventory:
|
||||
raise ValueError('Please do "export dhi={inventory}"')
|
||||
self.create_app = self.create_app_factory(inventory)
|
||||
return super().main(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def create_app_factory(inventory):
|
||||
return lambda: Devicehub(inventory)
|
||||
|
||||
|
||||
@click.group(cls=DevicehubGroup)
|
||||
def cli():
|
||||
pass
|
|
@ -2,13 +2,12 @@ from distutils.version import StrictVersion
|
|||
from itertools import chain
|
||||
from typing import Set
|
||||
|
||||
import boltons.urlutils
|
||||
from teal.auth import TokenAuth
|
||||
from teal.config import Config
|
||||
from teal.enums import Currency
|
||||
from teal.utils import import_resource
|
||||
|
||||
from ereuse_devicehub.resources import agent, event, lot, tag, user
|
||||
from ereuse_devicehub.resources import agent, event, inventory, lot, tag, user
|
||||
from ereuse_devicehub.resources.device import definitions
|
||||
from ereuse_devicehub.resources.documents import documents
|
||||
from ereuse_devicehub.resources.enums import PriceSoftware, RatingSoftware
|
||||
|
@ -21,23 +20,16 @@ class DevicehubConfig(Config):
|
|||
import_resource(tag),
|
||||
import_resource(agent),
|
||||
import_resource(lot),
|
||||
import_resource(documents))
|
||||
import_resource(documents),
|
||||
import_resource(inventory)),
|
||||
)
|
||||
PASSWORD_SCHEMES = {'pbkdf2_sha256'} # type: Set[str]
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/devicehub' # type: str
|
||||
SCHEMA = 'dhub'
|
||||
MIN_WORKBENCH = StrictVersion('11.0a1') # type: StrictVersion
|
||||
"""
|
||||
the minimum version of ereuse.org workbench that this devicehub
|
||||
accepts. we recommend not changing this value.
|
||||
"""
|
||||
ORGANIZATION_NAME = None # type: str
|
||||
ORGANIZATION_TAX_ID = None # type: str
|
||||
"""
|
||||
The organization using this Devicehub.
|
||||
|
||||
It is used by default, for example, when creating tags.
|
||||
"""
|
||||
API_DOC_CONFIG_TITLE = 'Devicehub'
|
||||
API_DOC_CONFIG_VERSION = '0.2'
|
||||
API_DOC_CONFIG_COMPONENTS = {
|
||||
|
@ -60,19 +52,3 @@ class DevicehubConfig(Config):
|
|||
"""
|
||||
Official versions
|
||||
"""
|
||||
TAG_BASE_URL = None
|
||||
TAG_TOKEN = None
|
||||
"""Access to the tag provider."""
|
||||
|
||||
def __init__(self, schema: str = None, token=None) -> None:
|
||||
if not self.ORGANIZATION_NAME or not self.ORGANIZATION_TAX_ID:
|
||||
raise ValueError('You need to set the main organization parameters.')
|
||||
if not self.TAG_BASE_URL:
|
||||
raise ValueError('You need a tag service.')
|
||||
self.TAG_TOKEN = token or self.TAG_TOKEN
|
||||
if not self.TAG_TOKEN:
|
||||
raise ValueError('You need a tag token')
|
||||
self.TAG_BASE_URL = boltons.urlutils.URL(self.TAG_BASE_URL)
|
||||
if schema:
|
||||
self.SCHEMA = schema
|
||||
super().__init__()
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
import uuid
|
||||
from typing import Type
|
||||
|
||||
import boltons.urlutils
|
||||
import click
|
||||
import click_spinner
|
||||
import ereuse_utils.cli
|
||||
from ereuse_utils.session import DevicehubClient
|
||||
from flask.globals import _app_ctx_stack, g
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import event
|
||||
from teal.config import Config as ConfigClass
|
||||
from teal.teal import Teal
|
||||
|
||||
from ereuse_devicehub.auth import Auth
|
||||
from ereuse_devicehub.client import Client
|
||||
from ereuse_devicehub.config import DevicehubConfig
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.dummy.dummy import Dummy
|
||||
from ereuse_devicehub.resources.device.search import DeviceSearch
|
||||
from ereuse_devicehub.resources.inventory import Inventory, InventoryDef
|
||||
|
||||
|
||||
class Devicehub(Teal):
|
||||
|
@ -18,7 +25,8 @@ class Devicehub(Teal):
|
|||
Dummy = Dummy
|
||||
|
||||
def __init__(self,
|
||||
config: ConfigClass,
|
||||
inventory: str,
|
||||
config: DevicehubConfig = DevicehubConfig(),
|
||||
db: SQLAlchemy = db,
|
||||
import_name=__name__.split('.')[0],
|
||||
static_url_path=None,
|
||||
|
@ -31,27 +39,76 @@ class Devicehub(Teal):
|
|||
instance_relative_config=False,
|
||||
root_path=None,
|
||||
Auth: Type[Auth] = Auth):
|
||||
super().__init__(config, db, import_name, static_url_path, static_folder, static_host,
|
||||
assert inventory
|
||||
super().__init__(config, db, inventory, import_name, static_url_path, static_folder,
|
||||
static_host,
|
||||
host_matching, subdomain_matching, template_folder, instance_path,
|
||||
instance_relative_config, root_path, Auth)
|
||||
self.tag_provider = DevicehubClient(**self.config.get_namespace('TAG_'))
|
||||
self.id = inventory
|
||||
"""The Inventory ID of this instance. In Teal is the app.schema."""
|
||||
self.dummy = Dummy(self)
|
||||
self.before_request(self.register_db_events_listeners)
|
||||
self.cli.command('regenerate-search')(self.regenerate_search)
|
||||
self.cli.command('init-db')(self.init_db)
|
||||
self.before_request(self._prepare_request)
|
||||
|
||||
def register_db_events_listeners(self):
|
||||
"""Registers the SQLAlchemy event listeners."""
|
||||
# todo can I make it with a global Session only?
|
||||
event.listen(db.session, 'before_commit', DeviceSearch.update_modified_devices)
|
||||
|
||||
def _init_db(self, exclude_schema=None, check=False):
|
||||
created = super()._init_db(exclude_schema, check)
|
||||
if created:
|
||||
# noinspection PyMethodOverriding
|
||||
@click.option('--name', '-n',
|
||||
default='Test 1',
|
||||
help='The human name of the inventory.')
|
||||
@click.option('--org-name', '-on',
|
||||
default='My Organization',
|
||||
help='The name of the default organization that owns this inventory.')
|
||||
@click.option('--org-id', '-oi',
|
||||
default='foo-bar',
|
||||
help='The Tax ID of the organization.')
|
||||
@click.option('--tag-url', '-tu',
|
||||
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
|
||||
default='http://example.com',
|
||||
help='The base url (scheme and host) of the tag provider.')
|
||||
@click.option('--tag-token', '-tt',
|
||||
type=click.UUID,
|
||||
default='899c794e-1737-4cea-9232-fdc507ab7106',
|
||||
help='The token provided by the tag provider. It is an UUID.')
|
||||
@click.option('--erase/--no-erase',
|
||||
default=False,
|
||||
help='Delete the full database before? Including all schemas and users.')
|
||||
@click.option('--common',
|
||||
default=False,
|
||||
help='Creates common databases. Only execute if the database is empty.')
|
||||
def init_db(self, name: str,
|
||||
org_name: str,
|
||||
org_id: str,
|
||||
tag_url: boltons.urlutils.URL,
|
||||
tag_token: uuid.UUID,
|
||||
erase: bool,
|
||||
common: bool):
|
||||
"""Initializes this inventory with the provided configurations."""
|
||||
assert _app_ctx_stack.top, 'Use an app context.'
|
||||
print('Initializing database...'.ljust(30), end='')
|
||||
with click_spinner.spinner():
|
||||
if erase:
|
||||
self.db.drop_all()
|
||||
exclude_schema = 'common' if not common else None
|
||||
self._init_db(exclude_schema=exclude_schema)
|
||||
InventoryDef.set_inventory_config(name, org_name, org_id, tag_url, tag_token)
|
||||
DeviceSearch.set_all_devices_tokens_if_empty(self.db.session)
|
||||
return created
|
||||
self._init_resources(exclude_schema=exclude_schema)
|
||||
self.db.session.commit()
|
||||
print('done.')
|
||||
|
||||
def regenerate_search(self):
|
||||
"""Re-creates from 0 all the search tables."""
|
||||
DeviceSearch.regenerate_search_table(self.db.session)
|
||||
db.session.commit()
|
||||
print('Done.')
|
||||
|
||||
def _prepare_request(self):
|
||||
"""Prepares request stuff."""
|
||||
inv = g.inventory = Inventory.current # type: Inventory
|
||||
g.tag_provider = DevicehubClient(base_url=inv.tag_provider, token=inv.tag_token)
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
from threading import Lock
|
||||
|
||||
import sqlalchemy as sa
|
||||
import werkzeug.exceptions
|
||||
from werkzeug import wsgi
|
||||
|
||||
import ereuse_devicehub.config
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
from ereuse_devicehub.resources.inventory import Inventory
|
||||
|
||||
|
||||
class PathDispatcher:
|
||||
NOT_FOUND = werkzeug.exceptions.NotFound()
|
||||
INV = Inventory
|
||||
|
||||
def __init__(self, config_cls=ereuse_devicehub.config.DevicehubConfig) -> None:
|
||||
self.lock = Lock()
|
||||
self.instances = {}
|
||||
self.CONFIG = config_cls
|
||||
self.engine = sa.create_engine(self.CONFIG.SQLALCHEMY_DATABASE_URI)
|
||||
with self.lock:
|
||||
self.instantiate()
|
||||
if not self.instances:
|
||||
raise ValueError('There are no Devicehub instances! Please, execute `dh init-db`.')
|
||||
self.one_app = next(iter(self.instances.values()))
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
if wsgi.get_path_info(environ).startswith('/users'):
|
||||
# Not nice solution but it works well for now
|
||||
# Return any app, as all apps can handle login
|
||||
return self.call(self.one_app, environ, start_response)
|
||||
inventory = wsgi.pop_path_info(environ)
|
||||
with self.lock:
|
||||
if inventory not in self.instances:
|
||||
self.instantiate()
|
||||
app = self.instances.get(inventory, self.NOT_FOUND)
|
||||
return self.call(app, environ, start_response)
|
||||
|
||||
@staticmethod
|
||||
def call(app, environ, start_response):
|
||||
return app(environ, start_response)
|
||||
|
||||
def instantiate(self):
|
||||
sel = sa.select([self.INV.id]).where(self.INV.id.notin_(self.instances.keys()))
|
||||
for row in self.engine.execute(sel):
|
||||
self.instances[row.id] = Devicehub(inventory=row.id)
|
|
@ -5,6 +5,7 @@ from typing import Set
|
|||
|
||||
import click
|
||||
import click_spinner
|
||||
import ereuse_utils.cli
|
||||
import yaml
|
||||
from ereuse_utils.test import ANY
|
||||
|
||||
|
@ -41,11 +42,25 @@ class Dummy:
|
|||
self.app = app
|
||||
self.app.cli.command('dummy', short_help='Creates dummy devices and users.')(self.run)
|
||||
|
||||
@click.option('--tag-url', '-tu',
|
||||
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
|
||||
default='http://localhost:8081',
|
||||
help='The base url (scheme and host) of the tag provider.')
|
||||
@click.option('--tag-token', '-tt',
|
||||
type=click.UUID,
|
||||
default='899c794e-1737-4cea-9232-fdc507ab7106',
|
||||
help='The token provided by the tag provider. It is an UUID.')
|
||||
@click.confirmation_option(prompt='This command (re)creates the DB from scratch.'
|
||||
'Do you want to continue?')
|
||||
def run(self):
|
||||
def run(self, tag_url, tag_token):
|
||||
runner = self.app.test_cli_runner()
|
||||
self.app.init_db(erase=True)
|
||||
self.app.init_db('Dummy',
|
||||
'ACME',
|
||||
'acme-id',
|
||||
tag_url,
|
||||
tag_token,
|
||||
erase=True,
|
||||
common=True)
|
||||
print('Creating stuff...'.ljust(30), end='')
|
||||
with click_spinner.spinner():
|
||||
out = runner.invoke(args=['create-org', *self.ORG], catch_exceptions=False).output
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import json
|
||||
|
||||
import click
|
||||
from flask import current_app as app
|
||||
from teal.db import SQLAlchemy
|
||||
from teal.resource import Converters, Resource
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
|
@ -46,11 +44,6 @@ class OrganizationDef(AgentDef):
|
|||
print(json.dumps(o, indent=2))
|
||||
return o
|
||||
|
||||
def init_db(self, db: SQLAlchemy, exclude_schema=None):
|
||||
"""Creates the default organization."""
|
||||
org = models.Organization(**app.config.get_namespace('ORGANIZATION_'))
|
||||
db.session.add(org)
|
||||
|
||||
|
||||
class Membership(Resource):
|
||||
SCHEMA = schemas.Membership
|
||||
|
|
|
@ -3,17 +3,17 @@ from operator import attrgetter
|
|||
from uuid import uuid4
|
||||
|
||||
from citext import CIText
|
||||
from flask import current_app as app, g
|
||||
from sqlalchemy import Column, Enum as DBEnum, ForeignKey, Unicode, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.orm import backref, relationship, validates
|
||||
from sqlalchemy_utils import EmailType, PhoneNumberType
|
||||
from teal import enums
|
||||
from teal.db import DBError, INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON, check_lower
|
||||
from teal.db import INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON, check_lower
|
||||
from teal.marshmallow import ValidationError
|
||||
from werkzeug.exceptions import NotImplemented, UnprocessableEntity
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.inventory import Inventory
|
||||
from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
|
||||
|
@ -46,6 +46,7 @@ class Agent(Thing):
|
|||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
||||
UniqueConstraint(tax_id, name, name='One tax ID with one name.')
|
||||
)
|
||||
|
||||
@declared_attr
|
||||
|
@ -80,21 +81,18 @@ class Agent(Thing):
|
|||
|
||||
|
||||
class Organization(JoinedTableMixin, Agent):
|
||||
default_of = db.relationship(Inventory,
|
||||
single_parent=True,
|
||||
uselist=False,
|
||||
primaryjoin=lambda: Organization.id == Inventory.org_id)
|
||||
|
||||
def __init__(self, name: str, **kwargs) -> None:
|
||||
super().__init__(**kwargs, name=name)
|
||||
|
||||
@classmethod
|
||||
def get_default_org_id(cls) -> UUID:
|
||||
"""Retrieves the default organization."""
|
||||
try:
|
||||
return g.setdefault('org_id',
|
||||
Organization.query.filter_by(
|
||||
**app.config.get_namespace('ORGANIZATION_')
|
||||
).one().id)
|
||||
except (DBError, UnprocessableEntity):
|
||||
# todo test how well this works
|
||||
raise NotImplemented('Error in getting the default organization. '
|
||||
'Is the DB initialized?')
|
||||
return cls.query.filter_by(default_of=Inventory.current).one().id
|
||||
|
||||
|
||||
class Individual(JoinedTableMixin, Agent):
|
||||
|
|
|
@ -21,6 +21,7 @@ class Agent(Thing):
|
|||
|
||||
class Organization(Agent):
|
||||
members = NestedOn('Membership')
|
||||
default_of = NestedOn('Inventory')
|
||||
|
||||
|
||||
class Membership(Thing):
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import uuid
|
||||
|
||||
import boltons.urlutils
|
||||
import click
|
||||
import ereuse_utils.cli
|
||||
from flask import current_app
|
||||
from teal.db import ResourceNotFound
|
||||
from teal.resource import Resource
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.inventory import schema
|
||||
from ereuse_devicehub.resources.inventory.model import Inventory
|
||||
|
||||
|
||||
class InventoryDef(Resource):
|
||||
SCHEMA = schema.Inventory
|
||||
VIEW = None
|
||||
|
||||
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None,
|
||||
static_url_path=None,
|
||||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||
root_path=None):
|
||||
cli_commands = (
|
||||
(self.set_inventory_config_cli, 'set-inventory-config'),
|
||||
)
|
||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||
|
||||
@click.option('--name', '-n',
|
||||
default='Test 1',
|
||||
help='The human name of the inventory.')
|
||||
@click.option('--org-name', '-on',
|
||||
default=None,
|
||||
help='The name of the default organization that owns this inventory.')
|
||||
@click.option('--org-id', '-oi',
|
||||
default=None,
|
||||
help='The Tax ID of the organization.')
|
||||
@click.option('--tag-url', '-tu',
|
||||
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
|
||||
default=None,
|
||||
help='The base url (scheme and host) of the tag provider.')
|
||||
@click.option('--tag-token', '-tt',
|
||||
type=click.UUID,
|
||||
default=None,
|
||||
help='The token provided by the tag provider. It is an UUID.')
|
||||
def set_inventory_config_cli(self, **kwargs):
|
||||
"""Sets the inventory configuration. Only updates passed-in
|
||||
values.
|
||||
"""
|
||||
self.set_inventory_config(**kwargs)
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def set_inventory_config(cls,
|
||||
name: str = None,
|
||||
org_name: str = None,
|
||||
org_id: str = None,
|
||||
tag_url: boltons.urlutils.URL = None,
|
||||
tag_token: uuid.UUID = None):
|
||||
try:
|
||||
inventory = Inventory.current
|
||||
except ResourceNotFound: # No inventory defined in db yet
|
||||
inventory = Inventory(id=current_app.id,
|
||||
name=name,
|
||||
tag_provider=tag_url,
|
||||
tag_token=tag_token)
|
||||
db.session.add(inventory)
|
||||
if org_name or org_id:
|
||||
from ereuse_devicehub.resources.agent.models import Organization
|
||||
try:
|
||||
org = Organization.query.filter_by(tax_id=org_id, name=org_name).one()
|
||||
except ResourceNotFound:
|
||||
org = Organization(tax_id=org_id, name=org_name)
|
||||
org.default_of = inventory
|
||||
db.session.add(org)
|
||||
if tag_url:
|
||||
inventory.tag_provider = tag_url
|
||||
if tag_token:
|
||||
inventory.tag_token = tag_token
|
|
@ -1,3 +1,6 @@
|
|||
from boltons.typeutils import classproperty
|
||||
from flask import current_app
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.models import Thing
|
||||
|
||||
|
@ -8,5 +11,12 @@ class Inventory(Thing):
|
|||
id.comment = """The name of the inventory as in the URL and schema."""
|
||||
name = db.Column(db.CIText(), nullable=False, unique=True)
|
||||
name.comment = """The human name of the inventory."""
|
||||
tag_token = db.Column(db.UUID(as_uuid=True), unique=True)
|
||||
tag_provider = db.Column(db.URL(), nullable=False)
|
||||
tag_token = db.Column(db.UUID(as_uuid=True), unique=True, nullable=False)
|
||||
tag_token.comment = """The token to access a Tag service."""
|
||||
org_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey('organization.id'), nullable=False)
|
||||
|
||||
@classproperty
|
||||
def current(cls) -> 'Inventory':
|
||||
"""The inventory of the current_app."""
|
||||
return Inventory.query.filter_by(id=current_app.id).one()
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import teal.marshmallow
|
||||
from marshmallow import fields as mf
|
||||
|
||||
from ereuse_devicehub.resources.schemas import Thing
|
||||
|
||||
|
||||
class Inventory(Thing):
|
||||
id = mf.String(dump_only=True)
|
||||
name = mf.String(dump_only=True)
|
||||
tag_provider = teal.marshmallow.URL(dump_only=True, data_key='tagProvider')
|
||||
tag_token = mf.UUID(dump_only=True, data_key='tagToken')
|
|
@ -1,5 +1,4 @@
|
|||
from ereuse_utils.session import DevicehubClient
|
||||
from flask import Response, current_app, current_app as app, jsonify, redirect, request
|
||||
from flask import Response, current_app as app, g, jsonify, redirect, request
|
||||
from teal.marshmallow import ValidationError
|
||||
from teal.resource import View, url_for_resource
|
||||
|
||||
|
@ -19,9 +18,8 @@ class TagView(View):
|
|||
return res
|
||||
|
||||
def _create_many_regular_tags(self, num: int):
|
||||
tag_provider = current_app.tag_provider # type: DevicehubClient
|
||||
tags_id, _ = tag_provider.post('/', {}, query=[('num', num)])
|
||||
tags = [Tag(id=tag_id, provider=current_app.config['TAG_BASE_URL']) for tag_id in tags_id]
|
||||
tags_id, _ = g.tag_provider.post('/', {}, query=[('num', num)])
|
||||
tags = [Tag(id=tag_id, provider=g.inventory.tag_provider) for tag_id in tags_id]
|
||||
db.session.add_all(tags)
|
||||
db.session.commit()
|
||||
response = jsonify(items=self.schema.dump(tags, many=True, nested=1)) # type: Response
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from typing import Iterable
|
||||
|
||||
from click import argument, option
|
||||
from flask import current_app
|
||||
from teal.resource import Converters, Resource
|
||||
|
||||
from ereuse_devicehub.db import db
|
||||
from ereuse_devicehub.resources.inventory import Inventory
|
||||
from ereuse_devicehub.resources.user import schemas
|
||||
from ereuse_devicehub.resources.user.models import User
|
||||
from ereuse_devicehub.resources.user.views import UserView, login
|
||||
|
@ -23,13 +26,21 @@ class UserDef(Resource):
|
|||
self.add_url_rule('/login/', view_func=login, methods={'POST'})
|
||||
|
||||
@argument('email')
|
||||
@option('-i', '--inventory',
|
||||
multiple=True,
|
||||
help='Inventories user has access to. By default this one.')
|
||||
@option('-a', '--agent', help='The name of an agent to create with the user.')
|
||||
@option('-c', '--country', help='The country of the agent (if --agent is set).')
|
||||
@option('-t', '--telephone', help='The telephone of the agent (if --agent is set).')
|
||||
@option('-t', '--tax-id', help='The tax id of the agent (if --agent is set).')
|
||||
@option('-p', '--password', prompt=True, hide_input=True, confirmation_prompt=True)
|
||||
def create_user(self, email: str, password: str, agent: str = None, country: str = None,
|
||||
telephone: str = None, tax_id: str = None) -> dict:
|
||||
def create_user(self, email: str,
|
||||
password: str,
|
||||
inventory: Iterable[str] = tuple(),
|
||||
agent: str = None,
|
||||
country: str = None,
|
||||
telephone: str = None,
|
||||
tax_id: str = None) -> dict:
|
||||
"""Creates an user.
|
||||
|
||||
If ``--agent`` is passed, it creates an ``Individual`` agent
|
||||
|
@ -38,7 +49,9 @@ class UserDef(Resource):
|
|||
from ereuse_devicehub.resources.agent.models import Individual
|
||||
u = self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \
|
||||
.load({'email': email, 'password': password})
|
||||
user = User(**u)
|
||||
if inventory:
|
||||
inventory = Inventory.query.filter(Inventory.id.in_(inventory))
|
||||
user = User(**u, inventories=inventory)
|
||||
agent = Individual(**current_app.resources[Individual.t].schema.load(
|
||||
dict(name=agent, email=email, country=country, telephone=telephone, taxId=tax_id)
|
||||
))
|
||||
|
|
|
@ -19,16 +19,24 @@ class User(Thing):
|
|||
schemes=app.config['PASSWORD_SCHEMES'],
|
||||
**kwargs
|
||||
)))
|
||||
"""
|
||||
Password field.
|
||||
From `here <https://sqlalchemy-utils.readthedocs.io/en/latest/
|
||||
data_types.html#module-sqlalchemy_utils.types.password>`_
|
||||
"""
|
||||
token = Column(UUID(as_uuid=True), default=uuid4, unique=True)
|
||||
token = Column(UUID(as_uuid=True), default=uuid4, unique=True, nullable=False)
|
||||
inventories = db.relationship(Inventory,
|
||||
backref=db.backref('users', lazy=True, collection_class=set),
|
||||
secondary=lambda: UserInventory.__table__,
|
||||
collection_class=set)
|
||||
# todo set restriction that user has, at least, one active db
|
||||
|
||||
def __init__(self, email, password=None, inventories=None) -> None:
|
||||
"""
|
||||
Creates an user.
|
||||
:param email:
|
||||
:param password:
|
||||
:param inventories: A set of Inventory where the user has
|
||||
access to. If none, the user is granted access to the current
|
||||
inventory.
|
||||
"""
|
||||
inventories = inventories or {Inventory.current}
|
||||
super().__init__(email=email, password=password, inventories=inventories)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '<User {0.email}>'.format(self)
|
||||
|
|
|
@ -2,9 +2,11 @@ from typing import Set, Union
|
|||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy_utils import Password
|
||||
|
||||
from ereuse_devicehub.resources.agent.models import Individual
|
||||
from ereuse_devicehub.resources.inventory import Inventory
|
||||
from ereuse_devicehub.resources.models import Thing
|
||||
|
||||
|
||||
|
@ -13,14 +15,17 @@ class User(Thing):
|
|||
email = ... # type: Column
|
||||
password = ... # type: Column
|
||||
token = ... # type: Column
|
||||
inventories = ... # type: relationship
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
def __init__(self, email: str, password: str = None,
|
||||
inventories: Set[Inventory] = None) -> None:
|
||||
super().__init__()
|
||||
self.id = ... # type: UUID
|
||||
self.email = ... # type: str
|
||||
self.password = ... # type: Password
|
||||
self.individuals = ... # type: Set[Individual]
|
||||
self.token = ... # type: UUID
|
||||
self.inventories = ... # type: Set[Inventory]
|
||||
|
||||
@property
|
||||
def individual(self) -> Union[Individual, None]:
|
||||
|
|
|
@ -5,6 +5,7 @@ from teal.marshmallow import SanitizedStr
|
|||
from ereuse_devicehub import auth
|
||||
from ereuse_devicehub.marshmallow import NestedOn
|
||||
from ereuse_devicehub.resources.agent.schemas import Individual
|
||||
from ereuse_devicehub.resources.inventory.schema import Inventory
|
||||
from ereuse_devicehub.resources.schemas import Thing
|
||||
|
||||
|
||||
|
@ -17,6 +18,7 @@ class User(Thing):
|
|||
token = String(dump_only=True,
|
||||
description='Use this token in an Authorization header to access the app.'
|
||||
'The token can change overtime.')
|
||||
inventories = NestedOn(Inventory, many=True, dump_only=True)
|
||||
|
||||
def __init__(self,
|
||||
only=None,
|
||||
|
|
|
@ -8,7 +8,7 @@ Define appdir /home/devicetag/sites/${servername}/source/
|
|||
# The path where the app directory is. Apache must have access to this folder.
|
||||
Define wsgipath ${appdir}/wsgi.wsgi
|
||||
# The location of the .wsgi file
|
||||
Define pyvenv ${appdir}/venv/
|
||||
Define pyvenv ${appdir}../venv/
|
||||
# The path where the virtual environment is (the folder containing bin/activate)
|
||||
|
||||
<VirtualHost *:80>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from ereuse_devicehub.config import DevicehubConfig
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
|
||||
"""
|
||||
|
@ -7,10 +6,4 @@ Example app with minimal configuration.
|
|||
Use this as a starting point.
|
||||
"""
|
||||
|
||||
|
||||
class MyConfig(DevicehubConfig):
|
||||
ORGANIZATION_NAME = 'My org'
|
||||
ORGANIZATION_TAX_ID = 'foo-bar'
|
||||
|
||||
|
||||
app = Devicehub(MyConfig())
|
||||
app = Devicehub(inventory='db1')
|
||||
|
|
|
@ -5,7 +5,7 @@ click==6.7
|
|||
click-spinner==0.1.8
|
||||
colorama==0.3.9
|
||||
colour==0.1.5
|
||||
ereuse-utils==0.4.0b14
|
||||
ereuse-utils[naming, test, session, cli]==0.4.0b14
|
||||
Flask==1.0.2
|
||||
Flask-Cors==3.0.6
|
||||
Flask-SQLAlchemy==2.3.2
|
||||
|
@ -15,7 +15,6 @@ marshmallow==3.0.0b11
|
|||
marshmallow-enum==1.4.1
|
||||
passlib==1.7.1
|
||||
phonenumbers==8.9.11
|
||||
pySMART.smartx==0.3.9
|
||||
pytest==3.7.2
|
||||
pytest-runner==4.2
|
||||
python-dateutil==2.7.3
|
||||
|
@ -25,9 +24,10 @@ requests==2.19.1
|
|||
requests-mock==1.5.2
|
||||
SQLAlchemy==1.2.14
|
||||
SQLAlchemy-Utils==0.33.6
|
||||
teal==0.2.0a34
|
||||
teal==0.2.0a35
|
||||
webargs==4.0.0
|
||||
Werkzeug==0.14.1
|
||||
sqlalchemy-citext==1.3.post0
|
||||
flask-weasyprint==0.5
|
||||
weasyprint==44
|
||||
psycopg2-binary==2.7.5
|
||||
|
|
9
setup.py
9
setup.py
|
@ -29,10 +29,10 @@ setup(
|
|||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
install_requires=[
|
||||
'teal>=0.2.0a34', # teal always first
|
||||
'teal>=0.2.0a35', # teal always first
|
||||
'click',
|
||||
'click-spinner',
|
||||
'ereuse-utils[Naming]>=0.4b14',
|
||||
'ereuse-utils[naming, test, session, cli]>=0.4b14',
|
||||
'hashids',
|
||||
'marshmallow_enum',
|
||||
'psycopg2-binary',
|
||||
|
@ -57,6 +57,11 @@ setup(
|
|||
'test': test_requires
|
||||
},
|
||||
tests_require=test_requires,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'dh = ereuse_devicehub.cli:cli'
|
||||
]
|
||||
},
|
||||
setup_requires=[
|
||||
'pytest-runner'
|
||||
],
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import io
|
||||
import uuid
|
||||
from contextlib import redirect_stdout
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import boltons.urlutils
|
||||
import pytest
|
||||
import yaml
|
||||
from psycopg2 import IntegrityError
|
||||
|
@ -26,13 +28,8 @@ T = {'start_time': STARTT, 'end_time': ENDT}
|
|||
|
||||
class TestConfig(DevicehubConfig):
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://dhub:ereuse@localhost/dh_test'
|
||||
SCHEMA = 'test'
|
||||
TESTING = True
|
||||
ORGANIZATION_NAME = 'FooOrg'
|
||||
ORGANIZATION_TAX_ID = 'foo-org-id'
|
||||
SERVER_NAME = 'localhost'
|
||||
TAG_BASE_URL = 'https://example.com'
|
||||
TAG_TOKEN = 'tagToken'
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
|
@ -42,7 +39,7 @@ def config():
|
|||
|
||||
@pytest.fixture(scope='session')
|
||||
def _app(config: TestConfig) -> Devicehub:
|
||||
return Devicehub(config=config, db=db)
|
||||
return Devicehub(inventory='test', config=config, db=db)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
@ -52,14 +49,23 @@ def app(request, _app: Devicehub) -> Devicehub:
|
|||
with _app.app_context():
|
||||
db.drop_all()
|
||||
|
||||
def _init():
|
||||
_app.init_db(name='Test Inventory',
|
||||
org_name='FooOrg',
|
||||
org_id='foo-org-id',
|
||||
tag_url=boltons.urlutils.URL('https://example.com'),
|
||||
tag_token=uuid.UUID('52dacef0-6bcb-4919-bfed-f10d2c96ecee'),
|
||||
erase=False,
|
||||
common=True)
|
||||
|
||||
with _app.app_context():
|
||||
try:
|
||||
with redirect_stdout(io.StringIO()):
|
||||
_app.init_db()
|
||||
_init()
|
||||
except (ProgrammingError, IntegrityError):
|
||||
print('Database was not correctly emptied. Re-empty and re-installing...')
|
||||
_drop()
|
||||
_app.init_db()
|
||||
_init()
|
||||
|
||||
request.addfinalizer(_drop)
|
||||
return _app
|
||||
|
|
|
@ -107,8 +107,7 @@ def test_default_org_exists(config: DevicehubConfig):
|
|||
initialization and that is accessible for the method
|
||||
:meth:`ereuse_devicehub.resources.user.Organization.get_default_org`.
|
||||
"""
|
||||
assert models.Organization.query.filter_by(name=config.ORGANIZATION_NAME,
|
||||
tax_id=config.ORGANIZATION_TAX_ID).one()
|
||||
assert models.Organization.query.filter_by(name='FooOrg', tax_id='foo-org-id').one()
|
||||
assert isinstance(models.Organization.get_default_org_id(), UUID)
|
||||
|
||||
|
||||
|
|
|
@ -42,4 +42,4 @@ def test_api_docs(client: Client):
|
|||
'scheme': 'basic',
|
||||
'name': 'Authorization'
|
||||
}
|
||||
assert 95 == len(docs['definitions'])
|
||||
assert len(docs['definitions']) == 96
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from ereuse_devicehub.devicehub import Devicehub
|
||||
from ereuse_devicehub.dispatchers import PathDispatcher
|
||||
from tests.conftest import TestConfig
|
||||
|
||||
|
||||
def noop():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def dispatcher(app: Devicehub, config: TestConfig) -> PathDispatcher:
|
||||
print('whoho')
|
||||
PathDispatcher.call = Mock(side_effect=lambda *args: args[0])
|
||||
return PathDispatcher(config_cls=config)
|
||||
|
||||
|
||||
def test_dispatcher_default(dispatcher: PathDispatcher):
|
||||
"""The dispatcher returns not found for an URL that does not
|
||||
route to an app.
|
||||
"""
|
||||
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/'}, noop)
|
||||
assert app == PathDispatcher.NOT_FOUND
|
||||
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/foo/foo'}, noop)
|
||||
assert app == PathDispatcher.NOT_FOUND
|
||||
|
||||
|
||||
def test_dispatcher_return_app(dispatcher: PathDispatcher):
|
||||
"""The dispatcher returns the correct app for the URL"""
|
||||
# Note that the dispatcher does not check if the URL points
|
||||
# to a well-known endpoint for the app.
|
||||
# Only if can route it to an app. And then the app checks
|
||||
# if the path exists
|
||||
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/test/foo/'}, noop)
|
||||
assert isinstance(app, Devicehub)
|
||||
assert app.id == 'test'
|
||||
|
||||
|
||||
def test_dispatcher_users(dispatcher: PathDispatcher):
|
||||
"""Users special endpoint returns an app"""
|
||||
# For now returns the first app, as all apps
|
||||
# can answer {}/users/login
|
||||
app = dispatcher({'SCRIPT_NAME:': '/', 'PATH_INFO': '/users/'}, noop)
|
||||
assert isinstance(app, Devicehub)
|
||||
assert app.id == 'test'
|
|
@ -0,0 +1,16 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Test not developed')
|
||||
def test_create_inventory():
|
||||
"""Tests creating an inventory with an user."""
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Test not developed')
|
||||
def test_create_existing_inventory():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Test not developed')
|
||||
def test_delete_inventory():
|
||||
pass
|
|
@ -241,7 +241,9 @@ def test_crate_num_regular_tags(user: UserClient, requests_mock: requests_mock.m
|
|||
"""
|
||||
requests_mock.post('https://example.com/',
|
||||
# request
|
||||
request_headers={'Authorization': 'Basic tagToken'},
|
||||
request_headers={
|
||||
'Authorization': 'Basic 52dacef0-6bcb-4919-bfed-f10d2c96ecee'
|
||||
},
|
||||
# response
|
||||
json=['tag1id', 'tag2id'],
|
||||
status_code=201)
|
||||
|
|
|
@ -82,6 +82,7 @@ def test_login_success(client: Client, app: Devicehub):
|
|||
assert user['individuals'][0]['name'] == 'Timmy'
|
||||
assert user['individuals'][0]['type'] == 'Person'
|
||||
assert len(user['individuals']) == 1
|
||||
assert user['inventories'][0]['id'] == 'test'
|
||||
|
||||
|
||||
def test_login_failure(client: Client, app: Devicehub):
|
||||
|
@ -99,3 +100,8 @@ def test_login_failure(client: Client, app: Devicehub):
|
|||
client.post({'email': 'this is not an email', 'password': 'nope'},
|
||||
uri='/users/login/',
|
||||
status=ValidationError)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Test not developed')
|
||||
def test_user_at_least_one_inventory():
|
||||
pass
|
||||
|
|
Reference in New Issue