class Resource(object):
    model = None
    resource_name = None
    resource_type = None
    queryset = None
    model_fields = []
    related_fields = {}
    stub_fields = {}
    required_fields = []
    page_size = 30

    def __init__(self, obj=None, is_list=None, data=None, request=None,
                 filters=None, limit=None, offset=None):
        if len([arg for arg in [obj, is_list, data] if arg]) != 1:
            logging.error(
                'Exactly 1 object, queryset, or primitive data is required')
        if obj:
            self._state = 'object'
            self._obj = obj
        elif is_list:
            self._state = 'list'
            self._queryset = self.__class__.queryset
        else:
            self._state = 'data'
            self._data = data

        self._filters = filters or {}
        self._request = request
        self._limit = limit or self.page_size
        self._offset = offset or 0

    @classmethod
    def get_object_by_id(cls, id):
        return cls.queryset.get(id=id)

    def get_content_type_id(self):
        return ContentType.objects.get_for_model(self.model).id

    def serialize_single(self, embed=True, cache=None, rels=True, *args, **kwargs):
        '''Serializes this object, assuming that there is a single instance to
        be serialized. Note that this only gets called from the top-level
        serialize() method, which handles checking whether we're in the
        cache'''
        data = {}
        if not embed:
            # this is just a link, don't embed the full object
            data['href'] = self.get_single_href()
            data['title'] = self.get_title()
            return data
        if rels:
            data['_links'] = {
                'self': {
                    'href': self.get_single_href(),
                },
                'ch:websocketStream': {
                    'href': self.get_websocket_href(),
                    'title': 'Websocket Stream'
                },
                'curies': CHAIN_CURIES
            }
            if kwargs.get('edit', True):
                data['_links']['editForm'] = {
                    'href': self.get_edit_href(),
                    'title': 'Edit %s' % capitalize(self.resource_type)
                }

            for field_name, collection in self.related_fields.items():
                # collection is a CollectionField or ResourceField here
                data['_links'][field_name] = collection.serialize(
                    self, self._request, cache)

            data['_links']['self']['title'] = self.get_title()

        for field_name in self.model_fields:
            data[field_name] = self.serialize_field(
                getattr(self._obj, field_name))
        for stub in self.stub_fields.keys():
            stub_data = getattr(self._obj, stub)
            data[stub] = getattr(stub_data, self.stub_fields[stub])
        # check to see whether this object has a geolocation
        try:
            loc = self._obj.geo_location
            if loc is not None:
                data['geoLocation'] = serialize_geo_location(loc)
        except AttributeError:
            # guess this model doesn't support geo_location
            pass
        return data

    def serialize_stream(self):
        '''By default resources are serialized for streams in their normal
        format. Resource subclasses can override this if they want a different
        representation for streaming'''
        return self.serialize()

    def serialize_field(self, field_value):
        '''some fields require special handling to be serialized. Handle
        that here'''
        if isinstance(field_value, datetime):
            return field_value.isoformat()
        return field_value

    def get_total_count(self):
        '''Gets the total number of objects in the queryset for this request,
        ignoring pagination. We cache the result because this query is actually
        pretty slow.'''
        try:
            return self._total_count
        except AttributeError:
            pass
        qs = self._queryset
        if self._filters:
            qs = qs.filter(**self._filters)
        self._total_count = qs.count()
        return self._total_count

    def get_title(self):
        tfield = self.display_field
        if tfield in self.model_fields:
            return self.serialize_field(getattr(self._obj, tfield))
        elif tfield in self.stub_fields.keys():
            stub_data = getattr(self._obj, tfield)
            return getattr(stub_data, self.stub_fields[tfield])
        else:
            raise NotImplementedError(
                'display_field must be a model field or stub field')


    def get_queryset(self):
        '''Returns the queryset resulting from this request, including
        all filtering, and pagination'''
        queryset = self._queryset.filter(**self._filters)
        return queryset[self._offset:self._offset + self._limit]

    def get_single_href(self):
        '''Gives the URL for this single element, assuming we have an
        object'''
        return full_reverse(self.resource_name + '-single',
                            self._request, args=(self._obj.id,))

    def get_edit_href(self):
        '''Gives the URL for this single element, assuming we have an
        object'''
        return full_reverse(self.resource_name + '-edit',
                            self._request, args=(self._obj.id,))

    def update_href(self, href, **kwargs):
        '''Takes a link href as a string, and updates the query string with the
        given query parameters'''
        scheme, netloc, path, params, query, fragment = urlparse(href)
        # Note that values from parse_qs are actually lists in order to
        # accomodate possible duplicate keys. make sure you encode with
        # doseq=True
        query_params = parse_qs(query)
        query_params.update(kwargs)
        query = urlencode(query_params, doseq=True)
        return urlunparse((scheme, netloc, path, params, query, fragment))

    def get_websocket_href(self):
        '''Gives the URL for the websockets stream that provides updates
        on this resource as well as nested resources.'''
        try:
            hostname = WEBSOCKET_HOST or self._request.META['HTTP_HOST']
        except KeyError:
            # TODO: raise an error that gets handled and returns a 400 response
            raise

        return "ws://%s/%s%s-%d" % (hostname,
                                    WEBSOCKET_PATH,
                                    self.resource_type, self._obj.id)

    def get_list_href(self):
        '''Gives the URL for this resource, including any filtering
        and pagination query parameters'''
        # TODO: use this for single views as well
        href = full_reverse(self.resource_name + '-list', self._request)
        query_params = self._filters.items()
        href += '?' + urlencode(query_params)
        return href

    def get_create_href(self):
        href = full_reverse(self.resource_name + '-create', self._request)
        query_params = self._filters.items()
        if query_params:
            href += '?' + urlencode(query_params)
        return href

    def get_tags(self):
        '''Returns a list of tags applicable to this instance of the resource.
        this gets called on a resource after POSTing (either creating a new one
        or editing an existing one'''
        return []

    def add_page_links(self, data, href):
        offset = self._offset
        limit = self._limit
        total_count = self.get_total_count()
        if offset > 0:
            # make previous link
            prev_offset = offset - limit if offset - limit > 0 else 0
            data['_links']['previous'] = {
                'href': self.update_href(href,
                                         offset=prev_offset, limit=limit),
                'title': '%d through %d' % (
                    prev_offset, prev_offset + limit - 1),
            }
            # make first link
            data['_links']['first'] = {
                'href': self.update_href(href, offset=0, limit=limit),
                'title': '0 through %d' % (limit - 1),
            }

        if offset + limit < total_count:
            # make next link
            if offset + 2 * limit < total_count:
                next_page_end = offset + 2 * limit
            else:
                next_page_end = total_count
            data['_links']['next'] = {
                'href': self.update_href(href,
                                         offset=(offset + limit),
                                         limit=limit),
                'title': '%d through %d' % (
                    offset + limit, next_page_end - 1),
            }
            last_page_start = int(total_count / limit) * limit
            data['_links']['last'] = {
                'href': self.update_href(href,
                                         offset=last_page_start,
                                         limit=limit),
                'title': '%d through %d' % (
                    last_page_start, total_count - 1),
            }
        return data

    def serialize_list(self, embed, cache):
        '''Serializes this object, assuming that there is a queryset that needs
        to be serialized as a collection'''

        href = self.get_list_href()

        if not embed:
            # the actual items aren't embedded, we're just providing a link
            # to this collection
            data = {
                'href': href,
                'title': capitalize(self.resource_name)
            }
            return data

        serialized_data = {
            '_links': {
                'self': {'href': href},
                'curies': CHAIN_CURIES,
                'createForm': {
                    'href': self.get_create_href(),
                    'title': 'Create %s' % capitalize(self.resource_type)
                }
            },
            'totalCount': self.get_total_count()
        }
        queryset = self.get_queryset()
        serialized_data['_links']['items'] = [
            self.__class__(obj=obj, request=self._request).
            serialize(cache=cache, embed=False) for obj in queryset]

        serialized_data = self.add_page_links(serialized_data, href)
        return serialized_data

    def serialize(self, embed=True, cache=None, *args, **kwargs):
        '''Serializes this instance into a dictionary that can be rendered'''
        if cache is None:
            cache = {}

        # first check to see if we're already in the cache, in which case we
        # can just return what was already calculated. Note that the key for
        # the cache includes whether we're embedded. This means that the same
        # object may be in the cache in both the embedded and non-embedded
        # forms, but that is likely a small price to pay for simplicity

        if self._state == 'list':
            # we don't currently handle cacheing whole collection lists.
            self._data = self.serialize_list(embed, cache,
                                             *args, **kwargs)

        elif self._state == 'object':
            cache_key = self.get_cache_key()
            if (cache_key, embed) in cache:
                return cache[(cache_key, embed)]
            else:
                self._data = self.serialize_single(embed, cache,
                                                   *args, **kwargs)
                cache[(cache_key, embed)] = self._data
        return self._data

    def get_cache_key(self):
        return self._obj.__class__, self._obj.id

    def stub_object_finding(self, obj, field_name, field_value):
        '''Looks up the matching related field, and creates it if it doesn't
        already exist.'''
        stub_field = self.stub_fields[field_name]
        field = getattr(self.model, field_name).field
        # so now we have a model we can run queries against
        related_class = field.rel.to

        query_args = {stub_field: field_value}
        try:
            matching_related_obj = related_class.objects.get(**query_args)
        except related_class.DoesNotExist:
            # a matching object doesn't exist, so we'll create it
            # TODO: this will crash if we can't build up an object based on the
            # given data
            matching_related_obj = related_class(**query_args)
            matching_related_obj.save()

        return matching_related_obj

    @classmethod
    def model_has_field(cls, field_name):
        try:
            cls.model._meta.get_field_by_name(field_name)
            return True
        except models.FieldDoesNotExist:
            return False

    @classmethod
    def sanitize_field_value(cls, field_name, value):
        '''Converts the given value to the correct python tyhttp://localhost:8080/people/1pe, for instance if
        the field is supposed to be a float field and the string "23" is given,
        it will be converted to 23.0

        NOTE - this currently only works for vanilla model fields, which serves
        our purposes for now'''
        field = cls.model._meta.get_field_by_name(field_name)[0]
        field_class = field.__class__
        if field_class == models.ForeignKey:
            lookup = lookup_associated_model_object(value)
            if lookup is None:
                raise BadRequestException(
                    "The url to the given resource does not exist.")
            return lookup
        # TextField missing to_python in Django 1.6
        elif field_class == models.TextField:
            if isinstance(value, six.string_types) or value is None:
                return value
            return smart_text(value)
        return field.to_python(value)

    def deserialize(self):
        '''Deserializes this instance and returns the object representation'''

        if self._state == 'object':
            return self._obj
        new_obj_data = {}

        filtered_fields = get_filtered_fields(self._filters)

        # take the intersection of the fields given and the fields in
        # self.model_fields

        for field_name in [f for f in self.model_fields
                           if f in self._data]:
            if field_name in filtered_fields:
                continue
            value = self.sanitize_field_value(field_name,
                                              self._data[field_name])
            new_obj_data[field_name] = value

        for stub_field_name in self.stub_fields.keys():
            new_obj_data[stub_field_name] = self.stub_object_finding(
                new_obj_data, stub_field_name, self._data[stub_field_name])
        if self.model_has_field('geo_location') and \
                'geoLocation' in self._data:
            dataloc = self._data['geoLocation']
            loc = GeoLocation(elevation=dataloc.get('elevation', None),
                              latitude=dataloc['latitude'],
                              longitude=dataloc['longitude'])
            loc.save()
            new_obj_data['geo_location'] = loc
        # the query string may contain more object data, for instance if
        # we're posting to a child collection resource

        new_obj_data.update(self._filters)
        self._obj = self.model(**new_obj_data)
        self._state = 'object'
        return self._obj

    def update(self, data):
        '''Updates this resource given the data, which is expected to be a dict
        in the format given by this resource's schema'''
        for k, v in data.iteritems():
            if k in self.model_fields:
                sanitized_value = type(self).sanitize_field_value(k, v)
                setattr(self._obj, k, sanitized_value)
            elif k in self.stub_fields:
                setattr(self._obj, k,
                        self.stub_object_finding(self._obj, k, v))
            elif k == 'geoLocation':
                loc = self._obj.geo_location
                if loc is None:
                    loc = GeoLocation(elevation=v.get('elevation', None),
                                      latitude=v['latitude'],
                                      longitude=v['longitude'])
                    loc.save()
                    self._obj.geo_location = loc
                else:
                    for field, value in v.iteritems():
                        setattr(loc, field, value)
                    loc.save()
        self._obj.save()

    def save(self):
        if not getattr(self, '_obj', False):
            # here we're using the side-effect of serialization that we save
            # the object after deserialization
            self.deserialize()
        self._obj.save()

    def get_filled_schema(self):
        '''Returns a schema dict with default values filled from the object's
        values'''
        schema = self.get_schema()
        schema['title'] = 'Edit ' + capitalize(self.resource_type)
        props = schema['properties']
        for field in self.model_fields:
            props[field]['default'] = self.serialize_field(
                getattr(self._obj, field))
        for stub in self.stub_fields.keys():
            stub_data = getattr(self._obj, stub)
            props[stub]['default'] = getattr(stub_data, self.stub_fields[stub])
        try:
            geo_loc = self._obj.geo_location
        except AttributeError:
            geo_loc = None
        if geo_loc is not None:
            props['geoLocation']['properties']['latitude']['default'] = \
                geo_loc.latitude
            props['geoLocation']['properties']['longitude']['default'] = \
                geo_loc.longitude
            elevation = geo_loc.elevation
            if elevation is not None:
                props['geoLocation']['properties']['elevation']['default'] = \
                    elevation

        return schema

    @classmethod
    def render_response(cls, data, request, status=None):
        # TODO: there's got to be a more robust library to parse accept headers
        if 'HTTP_ACCEPT' not in request.META:
            request.META['HTTP_ACCEPT'] = 'application/json'
        for accept in request.META['HTTP_ACCEPT'].split(','):
            accept = accept.strip()
            # first handle possible wildcards
            if accept in ['*/*', 'application/*', '*/json', '*/hal+json']:
                accept = 'application/hal+json'
            if accept in ['application/hal+json', 'application/json']:
                resp = HttpResponse(json.dumps(data), status=status,
                                    content_type=accept)
                patch_vary_headers(resp, ['Accept'])
                return resp
            elif accept == 'text/html':
                context = {'resource': data,
                           'json_str': json.dumps(data, indent=2)}
                template = jinja_env.get_template('resource.html')
                resp = HttpResponse(template.render(**context),
                                    status=status,
                                    content_type=accept)
                patch_vary_headers(resp, ['Accept'])
                return resp
        err_data = {
            'message': "MIME type not supported.\ Try text/html, \
            application/json, or application/hal+json",
        }
        return HttpResponse(json.dumps(err_data),
                            status=HTTP_STATUS_NOT_ACCEPTABLE,
                            content_type="application/hal+json")

    @classmethod
    @csrf_exempt
    def list_view(cls, request):
        offset = None
        limit = None
        filters = request.GET.dict()
        if 'offset' in filters:
            try:
                offset = int(filters.pop('offset'))
            except ValueError:
                pass
        if 'limit' in filters:
            try:
                limit = int(filters.pop('limit'))
            except ValueError:
                pass
        try:
            response_data = cls(is_list=True, request=request,
                                filters=filters, offset=offset,
                                limit=limit).serialize()
            return cls.render_response(response_data, request)
        except BadRequestException as e:
            return render_error(HTTP_STATUS_BAD_REQUEST, e.message, request)

    @classmethod
    def get_field_schema_type(cls, field_name):
        '''Returns the type string and format for a given field name, usable in
        the schema definition. Format may be None'''
        if field_name in cls.model_fields:
            field = cls.model._meta.get_field_by_name(field_name)[0]
        elif field_name in cls.stub_fields.keys():
            stub_data = cls.model._meta.get_field_by_name(field_name)[0]
            field = stub_data.rel.to._meta.get_field_by_name(
                cls.stub_fields[field_name])[0]
        else:
            raise NotImplementedError(
                "tried to look up field %s but didn't know where" % field_name)
        # returns a (type, format) tuple
        return schema_type_from_model_field(field)

    @classmethod
    def get_schema(cls, filters=None):
        '''Returns the JSON schema for this resource as a dictionary.
        Subclasses should override this method'''
        if filters is None:
            filters = {}
        filtered_fields = get_filtered_fields(filters)
        schema = {
            'type': 'object',
            'title': 'Create ' +
            capitalize(
                cls.resource_type),
            'properties': {},
            'required': [
                field for field in cls.required_fields if field not in filtered_fields]}
        for field_name in cls.model_fields + cls.stub_fields.keys():
            if field_name in filtered_fields:
                continue
            sch_type, sch_format = cls.get_field_schema_type(field_name)
            schema['properties'][field_name] = {
                'type': sch_type,
                'title': field_name
            }
            if sch_format:
                schema['properties'][field_name]['format'] = sch_format
            # don't allow empty strings in required fields
            if field_name in cls.required_fields and sch_type == 'string':
                schema['properties'][field_name]['minLength'] = 1
        if cls.model_has_field('geo_location'):
            schema['properties']['geoLocation'] = {
                'type': 'object',
                'title': 'geoLocation',
                'properties': {
                    'latitude': {'type': 'number', 'title': 'latitude'},
                    'longitude': {'type': 'number', 'title': 'longitude'},
                    'elevation': {'type': 'number', 'title': 'elevation'}
                },
                'required': ['latitude', 'longitude']
            }
        return schema

    @classmethod
    def single_view(cls, request, id):
        try:
            response_data = cls(obj=cls.get_object_by_id(id),
                                request=request).serialize()
            return cls.render_response(response_data, request)
        except cls.model.DoesNotExist:
            return handle404(request)

    @classmethod
    @csrf_exempt
    def edit_view(cls, request, id):
        if request.method == 'GET':
            resource = cls(obj=cls.get_object_by_id(id), request=request)
            schema = resource.get_filled_schema()
            return cls.render_response(schema, request)

        elif request.method == 'POST':
            # if not request.user.is_authenticated():
            #    return render_401(request)
            resource = cls(obj=cls.get_object_by_id(id), request=request)
            try:
                data = json.loads(request.body)
            except ValueError:
                return render_error(
                    HTTP_STATUS_BAD_REQUEST,
                    "The edit operation could not be performed because the data provided in the request body cannot be parsed as legal JSON.",
                    request)
            try:
                resource.update(data)
            except IntegrityError:
                return render_error(
                    400, 'Error storing object. Either required fields are '
                    'missing data or a matching object already exists',
                    request)
            response_data = resource.serialize()
            # push to the appropriate streams
            tags = resource.get_tags()
            if tags:
                stream_data = json.dumps(resource.serialize_stream())
            for tag in tags:
                zmq_socket.send_string(tag + ' ' + stream_data)
            return cls.render_response(response_data, request)

    @classmethod
    @csrf_exempt
    def create_view(cls, request):
        if request.method == 'GET':
            schema = cls.get_schema(request.GET.dict())
            return cls.render_response(schema, request)

        elif request.method == 'POST':
            # if not request.user.is_authenticated():
            #    return render_401(request)
            try:
                data = json.loads(request.body)
            except ValueError:
                return render_error(
                    HTTP_STATUS_BAD_REQUEST,
                    "The create operation could not be performed because the data provided in the request body cannot be parsed as legal JSON.",
                    request)
            if isinstance(data, list):
                return cls.create_list(data, request)
            else:
                return cls.create_single(data, request)

    @classmethod
    def create_resource(cls, data, request):
        obj_params = request.GET.dict()
        new_resource = cls(data=data, request=request, filters=obj_params)
        new_resource.save()
        response_data = new_resource.serialize()
        tags = new_resource.get_tags()
        if tags:
            stream_data = json.dumps(new_resource.serialize_stream())
        for tag in tags:
            zmq_socket.send_string(tag + ' ' + stream_data)
        return response_data

    @classmethod
    def create_single(cls, data, request):
        try:
            response_data = cls.create_resource(data, request)
        except IntegrityError:
            return render_error(
                400, 'Error storing object. Either required fields are '
                'missing data or a matching object already exists',
                request)
        except AmbiguousTimeError:
            return render_error(
                400, 'Error storing object. Timestamp is ambiguous',
                request)
        return cls.render_response(response_data, request,
                                   status=HTTP_STATUS_CREATED)
    @classmethod
    def create_list(cls, data, request):
        response_data = []
        for item in data:
            try:
                response_data.append(cls.create_resource(item, request))
            except IntegrityError:
                return render_error(
                    400, 'Error storing object. Either required fields are '
                    'missing data or a matching object already exists',
                    request)
            except AmbiguousTimeError:
                return render_error(
                    400, 'Error storing object. Timestamp is ambiguous',
                    request)
        return cls.render_response(response_data, request,
                                   status=HTTP_STATUS_CREATED)

    @classmethod
    def urls(cls):
        base_name = cls.resource_name
        return patterns('',
                        url(r'^$',
                            cls.list_view, name=base_name + '-list'),
                        url(r'^(\d+)$',
                            cls.single_view, name=base_name + '-single'),
                        url(r'^(\d+)/edit$',
                            cls.edit_view, name=base_name + '-edit'),
                        url(r'^create$',
                            cls.create_view, name=base_name + '-create')) class ResourceField(object):

    '''Describes a related single resource field, e.g. a Book resource might
    have an 'author' field that links (or embeds) the author resource'''

    def __init__(self, related_resource_class, parent_field_name, embed=False):
        self._related_resource_class = related_resource_class
        self._parent_field_name = parent_field_name
        self._embed = embed

    def serialize(self, parent, request, cache):
        self._related_resource_class = unlazy(self._related_resource_class)
        # TODO: shouldn't be reaching directly into parent._obj! refactor
        obj = getattr(parent._obj, self._parent_field_name)
        return self._related_resource_class(obj=obj,
                                            request=request).serialize(
                                                embed=self._embed, cache=cache) class CollectionField(object):

    '''A Collection field is a field on a resource that points to a child
    collection, e.g. an Author resource might have a 'books' field that is a
    collection of all the books by that author. By default the serialized data
    will only have a link to the child collection, but by setting embed=True
    you can actually embed the full collection inside the parent object.'''

    def __init__(self, child_resource_class, reverse_name, embed=False):
        self._reverse_name = reverse_name
        self._child_resource_class = child_resource_class
        self._embed = embed

    def create_parent_filter(self, parent):
        return {self._reverse_name + '_id': parent._obj.id}

    def serialize(self, parent, request, cache):
        queryset = self._child_resource_class.queryset

        # generate a filter on the child collection so we get the actual
        # children, and not all the resources

        parent_filter = self.create_parent_filter(parent)
        return self._child_resource_class(is_list=True, request=request,
                                          filters=parent_filter).serialize(
                                              embed=self._embed, cache=cache) class MetadataCollectionField(CollectionField):

    '''A MetadataCollection field is a field on a resource that points to a
    metadata collection'''

    def __init__(self, child_resource_class, embed=False):
        self._child_resource_class = child_resource_class
        self._embed = embed

    def create_parent_filter(self, parent):
        return {'object_id': parent._obj.id, 'content_type_id': parent.get_content_type_id()} def full_reverse(view_name, request, *args, **kwargs):
    partial_reverse = reverse(view_name, *args, **kwargs)
    return request.build_absolute_uri(partial_reverse) def render_error(status, msg, request):
    err_data = {
        'status': status,
        'message': msg,
    }
    return HttpResponse(json.dumps(err_data), status=status,
                        content_type="application/json") CHAIN_CURIES = [{
    'name': 'ch',
    'href': 'http://chain-api.media.mit.edu/rels/{rel}',
    'templated': True
}] class BadRequestException(Exception):

    def __init__(self, message):
        self.message = message

    def __str__(self):
        return "[Bad Request: " + repr(self.message) + "]" HTTP_STATUS_BAD_REQUEST = 400 def register_resource(resource):
    url_resource_map[resource.resource_name] = resource class Site(models.Model):
    '''An installation of Chain API, usually on the scale of several or many
    buildings. Sites might be hosted on a remote server, in which case the URL
    field will point to that resource on that server. If the site is hosted
    locally the URL can be blank'''
    name = models.CharField(max_length=255)
    url = models.CharField(max_length=255, default='', blank=True)
    geo_location = models.OneToOneField(GeoLocation, null=True, blank=True)
    raw_zmq_stream = models.CharField(max_length=255, default='', blank=True)

    def __repr__(self):
        return 'Site(name=%r)' % (self.name)

    def __str__(self):
        return self.name class Device(models.Model):
    '''A set of co-located sensors, often sharing a PCB'''
    name = models.CharField(max_length=255)
    site = models.ForeignKey(Site, related_name='devices')
    description = models.TextField(blank=True)
    building = models.CharField(max_length=255, blank=True)
    floor = models.CharField(max_length=10, blank=True)
    room = models.CharField(max_length=255, blank=True)
    geo_location = models.OneToOneField(GeoLocation, null=True, blank=True)
    active = models.BooleanField(default=True)

    class Meta:
        unique_together = ['site', 'name', 'building', 'floor', 'room']
        ordering = ["name"]

    def __repr__(self):
        return ('Device(site=%r, name=%r, description=%r, building=%r, ' +
                'floor=%r, room=%r)') % (
                    self.site, self.name, self.description, self.building,
                    self.floor, self.room)

    def __str__(self):
        return self.name class ScalarSensor(models.Model):
    '''An individual sensor. There may be multiple sensors on a single device.
    The metadata field is used to store information that might be necessary to
    tie the Sensor data to the physical Sensor in the real world, such as a MAC
    address, serial number, etc.'''
    device = models.ForeignKey(Device, related_name='sensors')
    metric = models.ForeignKey(Metric, related_name='sensors')
    unit = models.ForeignKey(Unit, related_name='sensors')
    metadata = models.CharField(max_length=255, blank=True)
    geo_location = models.OneToOneField(GeoLocation, null=True, blank=True)
    active = models.BooleanField(default=True)

    class Meta:
        unique_together = ['device', 'metric']

    def __repr__(self):
        return 'Sensor(device=%r, metric=%r, unit=%r)' % (
            self.device, self.metric, self.unit)

    def __str__(self):
        return self.metric.name class PresenceSensor(models.Model):
    '''An individual sensor. There may be multiple sensors on a single device.
    The metadata field is used to store information that might be necessary to
    tie the Sensor data to the physical Sensor in the real world, such as a MAC
    address, serial number, etc.'''
    device = models.ForeignKey(Device, related_name='presence_sensors')
    metric = models.ForeignKey(Metric, related_name='presence_sensors')
    # unit = models.ForeignKey(Unit, related_name='sensors')
    metadata = models.CharField(max_length=255, blank=True)
    geo_location = models.OneToOneField(GeoLocation, null=True, blank=True)

    class Meta:
        unique_together = ['device', 'metric']

    def __repr__(self):
        return 'PresenceSensor(device=%r, id=%r)' % (
            self.device, self.id)

    def __str__(self):
        return str(self.metric)
        # self.metric.name class PresenceData(models.Model):
    '''Sensor data indicating that a given Person was detected by the sensor at
    the given time, for instance using RFID or face recognition. Note that this
    is also used to indicate that a person was NOT seen by a given sensor by
    setting present=False. Typically a Presence sensor should indicate once
    when a person is first detected, then again when they are first absent.'''

    sensor = models.ForeignKey(PresenceSensor, related_name='presence_data')
    timestamp = models.DateTimeField(default=timezone.now, blank=True)
    person = models.ForeignKey(Person, related_name='presense_data')
    present = models.BooleanField(default=None)

    class Meta:
        verbose_name_plural = "presence data"

    def __repr__(self):
        return ('PresenceData(timestamp=%r, sensor=%r, ' +
                'person=%r, present=%r)') % (
                    self.timestamp, self.sensor, self.person, self.present)

    def __str__(self):
        return '%s %spresent' % (self.person,
                                 'not ' if not self.present else '') class Person(models.Model):
    '''A Person involved with the site. Some sensors might detect presence of a
    person, so they can reference this model with person-specific
    information'''
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)
    picture_url = models.CharField(max_length=255, blank=True)
    twitter_handle = models.CharField(max_length=255, blank=True)
    rfid = models.CharField(max_length=255, blank=True)
    site = models.ForeignKey(Site, related_name='people')
    geo_location = models.OneToOneField(GeoLocation, null=True, blank=True)

    class Meta:
        verbose_name_plural = "people"

    def __repr__(self):
        return ('Person(first_name=%s, last_name=%s, picture_url=%s, ' +
                'twitter_handle=%s, rfid=%s)') % (
                    self.first_name, self.last_name, self.picture_url,
                    self.twitter_handle, self.rfid)

    def __str__(self):
        return " ".join([self.first_name, self.last_name]) class Metadata(models.Model):
    '''Metadata assoicated with a site, a device, or a sensor'''
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    key = models.CharField(max_length=255)
    value = models.TextField(blank=True)
    timestamp = models.DateTimeField(default=timezone.now, blank=True) class InfluxClient(object):

    def __init__(self, host, port, database, measurement):
        self._host = host
        self._port = port
        self._database = database
        self._measurement = measurement
        # Persist TCP connection
        self._session = requests
        self._url = 'http://' + self._host + ':' + self._port

        if self._database not in self.get_databases():
            self.get('CREATE DATABASE ' + self._database)

    def request(self, method, url, params=None, data=None, headers=None, retries=4):
        sleeptime = 1
        while True:
            response = self._session.request(method=method,
                                             url=url,
                                             params=params,
                                             data=data,
                                             headers=headers)
            if response.status_code / 100 == 2:
                return response
            retries -= 1
            if retries == 0:
                return response
            sleep(sleeptime)
            sleeptime *= 2

    def post(self, endpoint, data, query=False):
        if endpoint == 'write':
            url = self._url + '/write'
        else:
            url = self._url + '/query'
        if query:
            data = {'q': data}
        response = self.request('POST',
                                url,
                                {'db': self._database},
                                data)
        return response

    def make_post_query_string(self, site_id, device_id, sensor_id, metric, value, timestamp=None):
        data = '{0},sensor_id={1},site_id={2},device_id={3},metric={4} value={5}'.format(self._measurement,
                                                                              sensor_id,
                                                                              site_id,
                                                                              device_id,
                                                                              metric,
                                                                              value)
        if timestamp:
            timestamp = InfluxClient.convert_timestamp(timestamp)
            data += ' ' + str(timestamp)
        return data

    def post_data(self, site_id, device_id, sensor_id, metric, value, timestamp=None):
        query = self.make_post_query_string(site_id, device_id, sensor_id, metric, value, timestamp)
        response = self.post('write', query)
        if response.status_code != HTTP_STATUS_SUCCESSFUL_WRITE:
            querylines = query.splitlines()
            if len(querylines) > 20:
                querylines = querylines[0:20] + ["...{} more".format(len(querylines)-20)]
            raise IntegrityError('Failed Query(status {}):\nQuery:\n{}\n\nResponse:\n{}\n'.format(
                response.status_code, "\n".join(querylines), response.json()))
        return response

    def post_data_bulk(self, site_id, device_id, sensor_id, metric, values, timestamps):
        query = ""
        for (value, timestamp) in izip(values, timestamps):
            query += self.make_post_query_string(site_id, device_id, sensor_id, metric, value, timestamp) + "\n"
        # print("posting query:\n{}\n".format(query))
        response = self.post('write', query)
        if response.status_code != HTTP_STATUS_SUCCESSFUL_WRITE:
            querylines = query.splitlines()
            if len(querylines) > 20:
                querylines = querylines[0:20] + ["...{} more".format(len(querylines)-20)]
            raise IntegrityError('Failed Query(status {}):\nQuery:{}\nResponse:\n{}'.format(
                response.status_code, "\n".join(querylines), response.json()))
        return response

    def get(self, query, database=False, **kwargs):
        # database argument should be true for any sensor data queries
        kwargs['q'] = query
        if database:
            kwargs['db'] = self._database
        return self.request('GET', self._url + '/query', kwargs)

    def get_sensor_data(self, filters):
        if 'aggtime' not in filters:
            measurement = self._measurement
        # arguements are unicode strings
        elif filters['aggtime'] == u'1h':
            measurement = self._measurement + '_1h'
        elif filters['aggtime'] == u'1d':
            measurement = self._measurement + '_1d'
        elif filters['aggtime'] == u'1w':
            measurement = self._measurement + '_1w'
        else:
            raise BadRequestException('Invalid argument for aggtime. Must be 1h, 1d, or 1w')

        # exclude the old values that don't have metrics
        query = "SELECT * FROM {0} WHERE sensor_id = '{1}' AND metric != ''".format(measurement,
                                                                   filters['sensor_id'])
        if 'timestamp__gte' in filters:
            timestamp_gte = InfluxClient.convert_timestamp(filters['timestamp__gte'])
            query += ' AND time >= {}'.format(timestamp_gte)
        if 'timestamp__lt' in filters:
            timestamp_lt = InfluxClient.convert_timestamp(filters['timestamp__lt'])
            query += ' AND time < {}'.format(timestamp_lt)


        result = self.get_values(self.get(query, True))
        return result

    def get_last_sensor_data(self, sensor_id):
        query = "SELECT LAST(value) FROM {0} WHERE sensor_id = \'{1}\'".format(self._measurement,
                                                                           sensor_id)
        result = self.get_values(self.get(query, True))
        return result

    def get_last_data_from_all_sensors(self, site_id):
        query = "SELECT LAST(*) FROM {0} WHERE site_id = \'{1}\' GROUP BY sensor_id".format(self._measurement,
                                                                                           site_id)
        result = self.get_values(self.get(query, True))
        return result

    def get_databases(self):
        response = self.get('SHOW DATABASES', False)
        series = response.json()['results'][0]['series'][0]
        if 'values' not in series:
            # there's only a values list if there's at least one value
            return []

        return [sub[0] for sub in series['values']]

    # returns a list of dictionaries, with one dictionary for each series in the
    # query result. Each dictionary maps the column name to a list of data
    def get_values(self,response):
        json = response.json()
        try:
            if len(json['results'])==0:
                return []
        except:
            import pdb
            pdb.set_trace()
        if 'series' not in json['results'][0]:
            return []
        if len(json['results'][0]['series']) == 0:
            return []
        if 'tags' in json['results'][0]['series'][0]:
            series = json['results'][0]['series']
            result = []
            for d in series:
                values = d['values'][0]
                columns = d['columns']
                data = dict(itertools.izip(columns,values))
                data.update(d['tags'])
                result.append(data)
        else:
            series = json['results'][0]['series'][0]
            values = series['values']
            columns = series['columns']
            result = [dict(itertools.izip(columns, value)) for value in values]
        return result


    @classmethod
    def convert_timestamp(cls, timestamp):
        if not timestamp.tzinfo:
            timestamp = UTC.localize(timestamp)
        return int((timestamp - EPOCH).total_seconds() * 1e9)
from chain.core.api import Resource, ResourceField, CollectionField, \
    MetadataCollectionField
from chain.core.api import full_reverse, render_error
from chain.core.api import CHAIN_CURIES
from chain.core.api import BadRequestException, HTTP_STATUS_BAD_REQUEST
from chain.core.api import register_resource
from chain.core.models import Site, Device, ScalarSensor, \
    PresenceSensor, PresenceData, Person, Metadata
from django.conf.urls import include, patterns, url
from django.utils import timezone
from datetime import timedelta, datetime
from chain.localsettings import INFLUX_HOST, INFLUX_PORT, INFLUX_DATABASE, INFLUX_MEASUREMENT
from chain.influx_client import InfluxClient
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.cache import cache_control
from django.utils.dateparse import parse_datetime
            from django.db import models
import calendar
import json
    resource_type = 'scalar_data'
    model_fields = ['timestamp', 'value']
    schema_type = {'timestamp': ('string', 'date-time'),
                   'value': ('number', None)}
    required_fields = ['value']

    def __init__(self, *args, **kwargs):
        super(ScalarSensorDataResource, self).__init__(*args, **kwargs)
        if self._state == 'data':
            # deserialize data
            self.sensor_id = self._filters.get('sensor_id')
            self.value = self.sanitize_field_value('value', self._data.get('value'))
            self.timestamp = self.sanitize_field_value('timestamp', self._data.get('timestamp'))
            # add ids up the hierarchy
            sensor = ScalarSensor.objects.select_related('device', 'metric').get(id=self.sensor_id)
            self.device_id = sensor.device.id
            self.metric = sensor.metric
            self.site_id = sensor.device.site_id
            # treat sensor data like an object
            self._state = 'object'
        if 'queryset' in kwargs:
            # we want to default to the last page, not the first page
            pass

    def serialize_single(self, embed=True, cache=None, rels=True):
        data = {}
        for field_name in self.model_fields:
            data[field_name] = self.serialize_field(getattr(self, field_name))
        return data

    @classmethod
    def sanitize_field_value(cls, field_name, value):
        if field_name == 'value':
            return float(value)
        if field_name == 'timestamp':
            if value == None:
                return timezone.now()
            timestamp = parse_datetime(value)
            if timezone.is_aware(timestamp):
                return timestamp
            return timezone.make_aware(timestamp, timezone.get_current_timezone())


    # we store the metric as a tag in Influx for convenience of querying
    # for clients that are using influx directly. It's not a real field that's
    # handled by Chain
    def save(self):
        response = influx_client.post_data(self.site_id, self.device_id, self.sensor_id,
                                           self.metric, self.value, self.timestamp)
        return response

    def serialize_list(self, embed, cache):
        '''a "list" of SensorData resources is actually represented
        as a single resource with a list of data points'''
        if not embed:
            return super(
                ScalarSensorDataResource,
                self).serialize_list(
                embed,
                cache)

        href = self.get_list_href()

        serialized_data = {
            '_links': {
                'curies': CHAIN_CURIES,
                'createForm': {
                    'href': self.get_create_href(),
                    'title': 'Add Data'
                }
            },
            'dataType': 'float'
        }
        request_time = timezone.now()

        # if the time filters aren't given then use the most recent timespan,
        # if they are given, then we need to convert them from unix time to use
        # in the queryset filter

        if 'timestamp__gte' in self._filters:
            try:
                page_start = datetime.utcfromtimestamp(
                    float(self._filters['timestamp__gte'])).replace(
                        tzinfo=timezone.utc)
            except ValueError:
                raise BadRequestException(
                    "Invalid timestamp format for lower bound of date range.")
        else:
            page_start = request_time - self.default_timespan()

        if 'timestamp__lt' in self._filters:
            try:
                page_end = datetime.utcfromtimestamp(
                    float(self._filters['timestamp__lt'])).replace(
                        tzinfo=timezone.utc)
            except ValueError:
                raise BadRequestException(
                    "Invalid timestamp format for upper bound of date range.")
        else:
            page_end = request_time

        self._filters['timestamp__gte'] = page_start
        self._filters['timestamp__lt'] = page_end

        objs = influx_client.get_sensor_data(self._filters)
        serialized_data = self.add_page_links(serialized_data, href,
                                              page_start, page_end)
        serialized_data['data'] = [{
            'value': obj['value'],
            'timestamp': obj['time']}
            for obj in objs]
        return serialized_data

    def get_cache_key(self):
        return self.sensor_id, self.timestamp

    def serialize_stream(self):
        '''Serialize this resource for a stream'''
        data = self.serialize_single(rels=False)
        data['_links'] = {

            'ch:sensor': {'href': full_reverse(