__init__.py 4.06 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from contextlib import contextmanager

from sqlalchemy import Column, Integer, String, Float, JSON, ForeignKey, Enum
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.orm import relationship, validates

from .field_types import FieldType
from .meta import Session, engine


@contextmanager
def session_context():
    session = Session()
    try:
        yield session
        session.commit()
    except BaseException:
        session.rollback()
        raise
    finally:
        session.close()


def with_session(func):
    """
    Decorator for convenience when building endpoints.  The first argument to the
    decorated function will be a safe-to-use, autocommitting Session instance.
    :param func: the view function to wrap
    :return: the wrapped function
    """

    def wrapper(*args, **kwargs):
        with session_context() as session:
            return func(session, *args, **kwargs)

    # Flask identifies endpoint handlers based on their name
    wrapper.__name__ = func.__name__
    return wrapper


@as_declarative()
class Base(object):
    pass


class Point(Base):
    """
    Represents actual instances of any and all points on the map.
    """
    __tablename__ = 'point'
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, nullable=True)
    lat = Column(Float, nullable=False)
    lon = Column(Float, nullable=False)
    attributes = Column(JSON, nullable=False)

    # Relationships
    category_id = Column(Integer, ForeignKey('category.id'), nullable=False)
    category = relationship('Category')
    parent_id = Column(Integer, ForeignKey('point.id'), nullable=True)
    parent = relationship('Point', remote_side=[id])
    children = relationship('Point')

64
65
66
67
68
69
70
    def __init__(self, **kwargs):
        # Need to load category first or else attribute validation will fail
        if 'category' in kwargs:
            self.category = kwargs.pop('category')
        
        super(Point, self).__init__(**kwargs)

71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
    @validates('attributes')
    def validate_data(self, _, data):
        if data is None:
            return

        fields = self.category.fields

        for key in data:
            # Find Field object that corresponds to this key
            for field in fields:
                if field.slug == key:
                    break
            else:
                raise ValueError(f'extra data "{key}" must be a registered field')
            field.validate_data(data[key])
        return data

    def as_json(self, children=True):
        if children:
            children = [child.as_json(children=False) for child in self.children]
        return {
            "name": self.name,
            "lat": self.lat,
            "lon": self.lon,
            "category": self.category.id,
            "attributes": self.attributes,
            "children": children
        }


class Category(Base):
    """
    Represent a schema for a single category of objects (e.g. water fountain or bathroom)
    """
    __tablename__ = 'category'

    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, nullable=False, unique=True)
    icon = Column(String, nullable=True)

    fields = relationship("Field")

    def as_json(self):
        return {
            "id": self.id,
            "name": self.name,
            "icon": self.icon,
            "attributes": {attr.slug: attr.as_json() for attr in self.fields}
        }


class Field(Base):
    """
    Represents a single field in the Category schema.
    """
    __tablename__ = 'field'

    id = Column(Integer, primary_key=True, autoincrement=True)
    slug = Column(String, nullable=False)
    name = Column(String, nullable=False)
    type = Column(Enum(FieldType), nullable=False)

    # Relationship
    category_id = Column(Integer, ForeignKey('category.id'))

    def validate_data(self, data):
        """
        Verify that data is the correct type for this Field.
        """
        self.type.validate(data)

    def as_json(self):
        return {
            "slug": self.slug,
            "name": self.name,
            "type": self.type.name
        }