<commit_before>from django.conf.urls import url
from django.db import connection
from tastypie.resources import Resource
from tastypie.authentication import ApiKeyAuthentication
from tastypie.exceptions import ImmediateHttpResponse
from tastypie.http import (HttpAccepted, HttpForbidden,
                           HttpBadRequest, HttpNotFound)
from tastypie import fields
from social.apps.django_app.default.models import UserSocialAuth
from django.contrib.auth.models import User
from .models import Floor
from .kNN import kNN
import json
import facebook
import logging

# Most stuff from here:
# http://stackoverflow.com/questions/20933214/creating-a-tastypie-resource-for-a-singleton-non-model-object
# http://django-tastypie.readthedocs.org/en/latest/non_orm_data_sources.html

class LocateMeObject(object):

    """ Represents a computed location
        Could very possibly be replaced with a Model instance in the future.
    """

    def __init__(self, **kwargs):
        self.__dict__['_data'] = {}
        if kwargs:
            self.update(kwargs)

    def __getattr__(self, name):
        return self._data.get(name, None)

    def __setattr__(self, name, value):
        self.__dict__['_data'][name] = value

    def update(self, other):
        for key in other:
            self.__setattr__(key, other[key])

    def to_dict(self):
        return self._data

class LocateMeResource(Resource):

    """ The API to compute a location from known access points
        Subclasses from Resource and not ModelResource because we need to
            transform data from one type to another
    """

    building_name = fields.CharField(
        attribute='building_name',
        help_text='The name of the building you\'re in')

    floor_number = fields.IntegerField(attribute='floor_number',
                                       help_text='The floor you\'re on')

    x_coordinate = fields.IntegerField(
        attribute='x_coordinate',
        help_text='The x coordinate on the image')

    y_coordinate = fields.IntegerField(
        attribute='y_coordinate',
        help_text='The y coordinate on the image')

    class Meta:
        resource_name = 'user_location'
        fields = ['building_name', 'floor_number',
                  'x_coordinate', 'y_coordinate']
        allowed_methods = ['post']
        always_return_data = True
        authentication = ApiKeyAuthentication()

    def _is_authorized(self, request):
        """Determine if the request is authorized to perform the action"""
        # TODO: Make this actually function
        return True

    def _save_user_location(self, user, location):
        """ Will be used to attach the location to the given user.
        Currently, this does nothing because we don't have the
        location <-> user association yet """
        pass

    def _locate_user(self, access_points):
        """ Will use Brett's kNN algorithm with the list of access points to
        determine the location. Currently, returned a random location within
        Halligan"""

        modified_aps = {ap['mac_address']: ap['signal_strength']
                        for ap in access_points}

        found, x_coord, y_coord, floor_id = kNN(connection.cursor(), modified_aps)

        if not found:
            msg = 'Could not locate you'
            raise ImmediateHttpResponse(HttpNotFound(msg))

        try:
            floor = Floor.objects.get(pk=floor_id)
        except Floor.DoesNotExist:
            msg = 'Could not locate you'
            raise ImmediateHttpResponse(HttpNotFound(msg))

        # Convert information to a location object
        location_object = LocateMeObject()
        location_object.building_name = floor.building_name
        location_object.floor_number = floor.floor_number
        location_object.x_coordinate = int(x_coord)
        location_object.y_coordinate = int(y_coord)
        location_object.image_url = floor.image.url

        return location_object

    def _notify_user(self, device, message):
        """Sends the message provided as a GCM push notification to all
        of the devices in device

        device can either be a single device or a queryset of devices
        """
        _, response = device.send_message(json.dumps(message))

        if response['failure'] == 1:
            # TODO: Log the failure
            # May change in the future to something more appropriate
            return HttpAccepted
        else:
            # TODO: Log the success
            return HttpAccepted

    def _verify(self, request):
        """Checks that the request should be allowed to go through.
        If not, raises an error and terminates the request"""
        self.method_check(request, allowed=self._meta.allowed_methods)
        self.is_authenticated(request)
        self._is_authorized(request)
        self.throttle_check(request)

    def base_urls(self):
        """Override the TastyPie URLs for this resource
            because we don't need them
        """
        find_friend_url = r'^(?P<resource_name>{})/friend/$'
        tell_friend_url = r'^(?P<resource_name>{})/respond/$'
        find_me_url = r'^(?P<resource_name>{})/me/$'
        return [
            url(find_friend_url.format(self._meta.resource_name),
                self.wrap_view('find_friend'),
                name='find_friend'),
            url(tell_friend_url.format(self._meta.resource_name),
                self.wrap_view('respond_to_request'),
                name='respond_to_request'),
            url(find_me_url.format(self._meta.resource_name),
                self.wrap_view('locate_me'),
                name='locate_me'),
        ]

    def find_friend(self, request, **kwargs):
        """An endpoint that allows a user to request the location of their friend

           Does not allow users to request the location of people they're not
           friends with on facebook.

           Does not allow users to request the location of people who do not
           have a phone registered to their account (for push notifications)
        """
        self._verify(request)
        data = self.deserialize(request,
                                request.body,
                                format=request.META.get('Content-Type',
                                                        'application/json'))
        # Was the Facebook ID of a friend supplied?
        friend_fb_id = data.get('friend_fb_id', None)
        if friend_fb_id is None:
            raise ImmediateHttpResponse(
                HttpBadRequest('Must specify a friend\'s FB ID'))

        # Does the requestor have a facebook account associated with their
        # profile?
        #
        # Should always be yes with the exception of the admin accounts
        my_user = request.user.social_auth.filter(provider='facebook')
        if not my_user.exists():
            raise ImmediateHttpResponse(
                HttpForbidden('Could not find you as a facebook user'))

        # Does the friend have an account in our system?
        try:
            friend_user = UserSocialAuth.objects.get(provider='facebook',
                                                     uid=friend_fb_id)
        except UserSocialAuth.DoesNotExist:
            msg = 'Could not find user with FB id {}'.format(friend_fb_id)
            raise ImmediateHttpResponse(HttpNotFound(msg))

        # Are the requestor and the requested friend *actually* friends?
        graph = facebook.GraphAPI(access_token=my_user.first().tokens)
        response = graph.get_object('/me/friends/{}'.format(friend_fb_id))
        if len(response['data']) == 0:
            msg = 'You can not request a location from that user'
            raise ImmediateHttpResponse(HttpForbidden(msg))

        # Does the friend have a phone associated with their profile?
        # This should also always be yes, with the exception of the admins
        friend_devices = friend_user.user.userdevice_set.all()
        if not friend_devices.exists():
            msg = 'Could not find friend with FB id {}'.format(friend_fb_id)
            raise ImmediateHttpResponse(HttpNotFound(msg))

        # Send the request for location to the friend
        msg_data = {
            'type': 'request_location',
            'requestor': request.user.get_full_name(),
            'requestor_id': request.user.pk
        }

        response_class = self._notify_user(friend_devices, msg_data)
        return self.create_response(request, data={},
                                    response_class=response_class)

    def respond_to_request(self, request, **kwargs):
        """Allows a user to respond to a request for their location"""
        self._verify(request)
        data = self.deserialize(request,
                                request.body,
                                format=request.META.get('Content-Type',
                                                        'application/json'))
        allow_request = data.get('allow_request', None)
        requestor_id = data.get('requestor_id', None)
        access_points = data.get('access_points', None)
        if allow_request is None or requestor_id is None:
            msg = 'Need both allow_request and requestor_id'
            raise ImmediateHttpResponse(HttpBadRequest(msg))
        if allow_request and access_points is None or len(access_points) < 1:
            msg = 'If allow_request, then access_points must be provided'
            raise ImmediateHttpResponse(HttpBadRequest(msg))

        # Make sure the requestor actually exists
        try:
            requestor = User.objects.get(pk=requestor_id)
            requestor_device_set = requestor.userdevice_set.all()
        except User.DoesNotExist:
            msg = 'Could not find User with id {}'.format(requestor_id)
            raise ImmediateHttpResponse(HttpNotFound(msg))

        # And also make sure the requestor has a phone
        if not requestor_device_set.exists():
            msg = 'Could not find User with id {}'.format(requestor_id)
            raise ImmediateHttpResponse(HttpNotFound(msg))

        if allow_request:
            location = self._locate_user(access_points)
            self._save_user_location(request.user, location)

            msg = {
                'type': 'request_granted',
                'friend': request.user.get_full_name(),
                'building_name': location.building_name,
                'floor_number': location.floor_number,
                'x_coordinate': location.x_coordinate,
                'y_coordinate': location.y_coordinate,
                'requestor_id': request.user.pk
            }
        else:
            msg = {
                'type': 'request_denied',
                'friend': request.user.get_full_name(),
            }

        response_class = self._notify_user(requestor_device_set, msg)
        return self.create_response(request, data={},
                                    response_class=response_class)

    def locate_me(self, request, **kwargs):
        """Used by a user to locate themselves within a building"""
        self._verify(request)
        data = self.deserialize(request,
                                request.body,
                                format=request.META.get('Content-Type',
                                                        'application/json'))

        access_points = data.get('access_points', None)
        if access_points is None or len(access_points) < 1:
            msg = 'No access points provided'
            raise ImmediateHttpResponse(HttpBadRequest(msg))

        location = self._locate_user(access_points)
        self._save_user_location(request.user, location)

        return self.create_response(request,
                                    data=location.to_dict(),
                                    response_class=HttpAccepted)
<commit_msg>Add friend_id to request responses<commit_after>from django.conf.urls import url
from django.db import connection
from tastypie.resources import Resource
from tastypie.authentication import ApiKeyAuthentication
from tastypie.exceptions import ImmediateHttpResponse
from tastypie.http import (HttpAccepted, HttpForbidden,
                           HttpBadRequest, HttpNotFound)
from tastypie import fields
from social.apps.django_app.default.models import UserSocialAuth
from django.contrib.auth.models import User
from .models import Floor
from .kNN import kNN
import json
import facebook
import logging

# Most stuff from here:
# http://stackoverflow.com/questions/20933214/creating-a-tastypie-resource-for-a-singleton-non-model-object
# http://django-tastypie.readthedocs.org/en/latest/non_orm_data_sources.html

class LocateMeObject(object):

    """ Represents a computed location
        Could very possibly be replaced with a Model instance in the future.
    """

    def __init__(self, **kwargs):
        self.__dict__['_data'] = {}
        if kwargs:
            self.update(kwargs)

    def __getattr__(self, name):
        return self._data.get(name, None)

    def __setattr__(self, name, value):
        self.__dict__['_data'][name] = value

    def update(self, other):
        for key in other:
            self.__setattr__(key, other[key])

    def to_dict(self):
        return self._data

class LocateMeResource(Resource):

    """ The API to compute a location from known access points
        Subclasses from Resource and not ModelResource because we need to
            transform data from one type to another
    """

    building_name = fields.CharField(
        attribute='building_name',
        help_text='The name of the building you\'re in')

    floor_number = fields.IntegerField(attribute='floor_number',
                                       help_text='The floor you\'re on')

    x_coordinate = fields.IntegerField(
        attribute='x_coordinate',
        help_text='The x coordinate on the image')

    y_coordinate = fields.IntegerField(
        attribute='y_coordinate',
        help_text='The y coordinate on the image')

    class Meta:
        resource_name = 'user_location'
        fields = ['building_name', 'floor_number',
                  'x_coordinate', 'y_coordinate']
        allowed_methods = ['post']
        always_return_data = True
        authentication = ApiKeyAuthentication()

    def _is_authorized(self, request):
        """Determine if the request is authorized to perform the action"""
        # TODO: Make this actually function
        return True

    def _save_user_location(self, user, location):
        """ Will be used to attach the location to the given user.
        Currently, this does nothing because we don't have the
        location <-> user association yet """
        pass

    def _locate_user(self, access_points):
        """ Will use Brett's kNN algorithm with the list of access points to
        determine the location. Currently, returned a random location within
        Halligan"""

        modified_aps = {ap['mac_address']: ap['signal_strength']
                        for ap in access_points}

        found, x_coord, y_coord, floor_id = kNN(connection.cursor(), modified_aps)

        if not found:
            msg = 'Could not locate you'
            raise ImmediateHttpResponse(HttpNotFound(msg))

        try:
            floor = Floor.objects.get(pk=floor_id)
        except Floor.DoesNotExist:
            msg = 'Could not locate you'
            raise ImmediateHttpResponse(HttpNotFound(msg))

        # Convert information to a location object
        location_object = LocateMeObject()
        location_object.building_name = floor.building_name
        location_object.floor_number = floor.floor_number
        location_object.x_coordinate = int(x_coord)
        location_object.y_coordinate = int(y_coord)
        location_object.image_url = floor.image.url

        return location_object

    def _notify_user(self, device, message):
        """Sends the message provided as a GCM push notification to all
        of the devices in device

        device can either be a single device or a queryset of devices
        """
        _, response = device.send_message(json.dumps(message))

        if response['failure'] == 1:
            # TODO: Log the failure
            # May change in the future to something more appropriate
            return HttpAccepted
        else:
            # TODO: Log the success
            return HttpAccepted

    def _verify(self, request):
        """Checks that the request should be allowed to go through.
        If not, raises an error and terminates the request"""
        self.method_check(request, allowed=self._meta.allowed_methods)
        self.is_authenticated(request)
        self._is_authorized(request)
        self.throttle_check(request)

    def base_urls(self):
        """Override the TastyPie URLs for this resource
            because we don't need them
        """
        find_friend_url = r'^(?P<resource_name>{})/friend/$'
        tell_friend_url = r'^(?P<resource_name>{})/respond/$'
        find_me_url = r'^(?P<resource_name>{})/me/$'
        return [
            url(find_friend_url.format(self._meta.resource_name),
                self.wrap_view('find_friend'),
                name='find_friend'),
            url(tell_friend_url.format(self._meta.resource_name),
                self.wrap_view('respond_to_request'),
                name='respond_to_request'),
            url(find_me_url.format(self._meta.resource_name),
                self.wrap_view('locate_me'),
                name='locate_me'),
        ]

    def find_friend(self, request, **kwargs):
        """An endpoint that allows a user to request the location of their friend

           Does not allow users to request the location of people they're not
           friends with on facebook.

           Does not allow users to request the location of people who do not
           have a phone registered to their account (for push notifications)
        """
        self._verify(request)
        data = self.deserialize(request,
                                request.body,
                                format=request.META.get('Content-Type',
                                                        'application/json'))
        # Was the Facebook ID of a friend supplied?
        friend_fb_id = data.get('friend_fb_id', None)
        if friend_fb_id is None:
            raise ImmediateHttpResponse(
                HttpBadRequest('Must specify a friend\'s FB ID'))

        # Does the requestor have a facebook account associated with their
        # profile?
        #
        # Should always be yes with the exception of the admin accounts
        my_user = request.user.social_auth.filter(provider='facebook')
        if not my_user.exists():
            raise ImmediateHttpResponse(
                HttpForbidden('Could not find you as a facebook user'))

        # Does the friend have an account in our system?
        try:
            friend_user = UserSocialAuth.objects.get(provider='facebook',
                                                     uid=friend_fb_id)
        except UserSocialAuth.DoesNotExist:
            msg = 'Could not find user with FB id {}'.format(friend_fb_id)
            raise ImmediateHttpResponse(HttpNotFound(msg))

        # Are the requestor and the requested friend *actually* friends?
        graph = facebook.GraphAPI(access_token=my_user.first().tokens)
        response = graph.get_object('/me/friends/{}'.format(friend_fb_id))
        if len(response['data']) == 0:
            msg = 'You can not request a location from that user'
            raise ImmediateHttpResponse(HttpForbidden(msg))

        # Does the friend have a phone associated with their profile?
        # This should also always be yes, with the exception of the admins
        friend_devices = friend_user.user.userdevice_set.all()
        if not friend_devices.exists():
            msg = 'Could not find friend with FB id {}'.format(friend_fb_id)
            raise ImmediateHttpResponse(HttpNotFound(msg))

        # Send the request for location to the friend
        msg_data = {
            'type': 'request_location',
            'requestor': request.user.get_full_name(),
            'requestor_id': request.user.pk
        }

        response_class = self._notify_user(friend_devices, msg_data)
        return self.create_response(request, data={},
                                    response_class=response_class)

    def respond_to_request(self, request, **kwargs):
        """Allows a user to respond to a request for their location"""
        self._verify(request)
        data = self.deserialize(request,
                                request.body,
                                format=request.META.get('Content-Type',
                                                        'application/json'))
        allow_request = data.get('allow_request', None)
        requestor_id = data.get('requestor_id', None)
        access_points = data.get('access_points', None)
        if allow_request is None or requestor_id is None:
            msg = 'Need both allow_request and requestor_id'
            raise ImmediateHttpResponse(HttpBadRequest(msg))
        if allow_request and access_points is None or len(access_points) < 1:
            msg = 'If allow_request, then access_points must be provided'
            raise ImmediateHttpResponse(HttpBadRequest(msg))

        # Make sure the requestor actually exists
        try:
            requestor = User.objects.get(pk=requestor_id)
            requestor_device_set = requestor.userdevice_set.all()
        except User.DoesNotExist:
            msg = 'Could not find User with id {}'.format(requestor_id)
            raise ImmediateHttpResponse(HttpNotFound(msg))

        # And also make sure the requestor has a phone
        if not requestor_device_set.exists():
            msg = 'Could not find User with id {}'.format(requestor_id)
            raise ImmediateHttpResponse(HttpNotFound(msg))

        if allow_request:
            location = self._locate_user(access_points)
            self._save_user_location(request.user, location)

            msg = {
                'type': 'request_granted',
                'friend': request.user.get_full_name(),
                'building_name': location.building_name,
                'floor_number': location.floor_number,
                'x_coordinate': location.x_coordinate,
                'y_coordinate': location.y_coordinate,
                'friend_id': request.user.pk
            }
        else:
            msg = {
                'type': 'request_denied',
                'friend': request.user.get_full_name(),
                'friend_id': request.user.pk
            }

        response_class = self._notify_user(requestor_device_set, msg)
        return self.create_response(request, data={},
                                    response_class=response_class)

    def locate_me(self, request, **kwargs):
        """Used by a user to locate themselves within a building"""
        self._verify(request)
        data = self.deserialize(request,
                                request.body,
                                format=request.META.get('Content-Type',
                                                        'application/json'))

        access_points = data.get('access_points', None)
        if access_points is None or len(access_points) < 1:
            msg = 'No access points provided'
            raise ImmediateHttpResponse(HttpBadRequest(msg))

        location = self._locate_user(access_points)
        self._save_user_location(request.user, location)

        return self.create_response(request,
                                    data=location.to_dict(),
                                    response_class=HttpAccepted)
<|endoftext|>


<commit_before>"""
Copyright (c) 2008-2011, Anthony Garcia <lagg@lavabit.com>

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""

import web
import re
try: from collections import OrderedDict as odict
except ImportError: odict = dict
from urlparse import urljoin
from optf2.backend import config

virtual_root = config.ini.get("resources", "virtual-root")
static_prefix = config.ini.get("resources", "static-prefix")
particles = config.ini.options("particle-modes")
htmldesc = re.compile("<(?P<tag>.+) ?.*>.+</(?P=tag)>")

celldims = {}
for mode, dims in config.ini.items("page-dimensions"):
    sep = dims.lower().find('x')
    if sep == -1:
        val = int(dims)
        celldims[mode] = {"width": val, "height": val}
    else:
        celldims[mode] = {"width": int(dims[:sep]), "height": int(dims[sep + 1:])}

classoverrides = {
    "tf2": odict([
            (1, "Scout"),
            (3, "Soldier"),
            (7, "Pyro"),
            (4, "Demoman"),
            (6, "Heavy"),
            (9, "Engineer"),
            (5, "Medic"),
            (2, "Sniper"),
            (8, "Spy")
            ]),
    "p2": odict([
            (1, "P-body"),
            (2, "Atlas")
            ])
    }
overridealiases = {
    "tf2b": "tf2"
}
reverrides = {}
for k, v in classoverrides.iteritems(): reverrides[k + "_swap"] = dict(zip(v.values(), v.keys()))
classoverrides.update(reverrides)

def get_class_for_id(cid, mode = None):
    overrides, swapped_overrides = get_class_overrides(mode)
    try: realcid = int(cid)
    except ValueError: realcid = cid

    if not overrides: return realcid, cid

    if realcid in overrides:
        return (realcid, overrides[realcid])
    elif realcid in swapped_overrides:
        return (swapped_overrides[realcid], cid)
    else:
        return (realcid, cid)

def get_class_overrides(mode = None):
    if not mode: mode = web.ctx.current_game or "tf2"
    mode = overridealiases.get(mode, mode)

    if mode not in classoverrides: return None, None

    return (classoverrides.get(mode),
            reverrides.get(mode + "_swap"))

def sorted_class_list(classes, mode = None):
    overrides, swappedoverrides = get_class_overrides(mode)
    validclasses = [get_class_for_id(c, mode) for c in classes]

    if not overrides or not validclasses: return validclasses

    return filter(lambda x: x in validclasses, overrides.iteritems())

def get_page_sizes():
    return celldims

def absolute_url(relative_url):
    return urljoin(web.ctx.homedomain, relative_url)

def generate_mode_url(path = "", mode = None):
    """ Generates a URL appropriate for the current mode
    with path appended to it. """

    cg = mode
    default = "tf2"
    try:
        if not mode:
            cg = web.ctx.current_game or default
    except AttributeError:
        print("Couldn't get current game mode, falling back to tf2")
        cg = default

    return virtual_root + cg + "/" + path

def generate_particle_icon_url(pid, mode = None):
    if not mode: mode = web.ctx.current_game or "tf2"
    if mode in particles: mode = config.ini.get("particle-modes", mode)

    return static_prefix + mode + "_particle_icons/" + str(pid) + ".png"

def generate_item_description(item):
    desc = web.websafe(item.get_custom_description())

    if not desc:
        desc = item.get_description()
        if desc and not htmldesc.search(desc):
            desc = web.websafe(desc)

    if desc: return '
' + desc.replace('\n', '
') + '
'

    return ''

def generate_item_type_line(item, classic = True):
    """ If classic is true the full Level X Y
    formatting will be used, if not only the type and other relevant information
    will be """

    origin_name = ""
    levelprefix = ""
    rank = item.get_rank()
    itemorigin = item.get_origin_name()

    if classic and "level" in item.optf2:
        levelprefix = "Level " + str(item.optf2["level"])

    if itemorigin:
        origin_name = " - " + itemorigin

    itype = item.get_type()
    if itype.startswith("TF_"): itype = ""

    rankname = ""
    if rank: rankname = rank["name"]
    if rankname: levelprefix = ""

    return '
{0}{1} {2}{3}
'.format(levelprefix,
                                                                web.websafe(rankname),
                                                                web.websafe(itype),
                                                                web.websafe(origin_name))

def generate_item_url(item, user = None, mode = None):
    """ Intelligently generates a URL linking to
    the given item """

    itemid = item.get_id()
    pathuser = ""

    if not mode: mode = web.ctx.current_game

    if itemid and user:
        try: pathuser = str(user.get_id64())
        except AttributeError: pathuser = str(user)

    if pathuser: pathuser += "/"

    return generate_mode_url("item/" + pathuser + str(itemid or item.get_schema_id()), mode = mode)

def generate_item_paint_line(item):
    """ Returns a "Painted with X" line if available """

    if "paint_name" in item.optf2:
        paintcan = item.optf2["paint_name"]
        return '
{0} with {1}
'.format(item.optf2["painted_text"], web.websafe(paintcan))
    else:
        return ''

def generate_item_price_string(item, stats):
    assets = None

    if not stats: return None

    if isinstance(stats, dict): assets = stats["assets"]
    else: assets = stats

    try: return "Store price: ${0}".format(assets[item].get_price()["USD"])
    except: return None

def generate_attribute_list(item, showlinks = False):
    markup = ''
    list_open = '
'
    list_close = '
'
    extra = item.optf2

    morestr = ""
    if showlinks: morestr = ' (more)'

    for attr in extra["attrs"]:
        desc = attr.get_description_formatted()
        style = ""
        acct = extra.get("{0}_account".format(attr.get_id()))
        contents = extra.get("contents")

        if "color" in attr.optf2: style = ' style="color: #{0};"'.format(attr.optf2["color"])
        markup += '
'.format(attr.get_type(), style)

        if contents and attr.get_name() == "referenced item def":
            markup += extra["content_string"]
            markup += morestr.format(web.http.changequery(contents = 1))
        else:
            if attr.get_value_type() != "html": desc = web.websafe(desc).replace('\n', "
")
            markup += desc

        if acct: markup += morestr.format(generate_mode_url("user/"  + str(acct["id64"])))

        markup += '
'

    for eater in extra["eaters"]: markup += '
{0}
'.format(eater)

    style = item.get_current_style_name()
    if style: markup += '
Style: {0}
'.format(style)

    quantity = item.get_quantity()
    if quantity > 1: markup += '
Quantity: {0}
'.format(quantity)

    if item.is_untradable(): markup += '
Untradable
'
    if item.is_uncraftable(): markup += '
Uncraftable
'

    if not markup:
        # XHTML compliance
        markup = '

'

    return (list_open + markup + list_close)

def generate_cell(item, invalid = False, show_equipped = True, user = None, pricestats = None, mode = None):
    if not item: return '

'

    item_id = item.get_id()
    schema_item = False
    equipped = (len(item.get_equipped_classes()) > 0)
    if not mode: mode = web.ctx.current_game or "tf2"

    if not show_equipped: equipped = False
    if not item_id:
        schema_item = True
        item_id = item.get_schema_id()

    item_link = generate_item_url(item, user, mode = mode)
    quality = item.get_quality()["str"]
    equippedstr = ""
    quantity = item.get_quantity()

    cell_class = "item_cell"
    if not schema_item and item.get_position() <= -1: cell_class += " undropped"

    style = ""
    if "namecolor" in item.optf2:
        style = ' style="border-color: #{0};"'.format(item.optf2["namecolor"])

    markup = ('
' +
              '' +
              '' +
              ''
              ).format(cell_class, quality, item_id, item_link, item.optf2["image_url"], item_id, style)

    contents = item.optf2.get("contents")
    if contents:
        markup += ''
    if item.get_custom_name():
        markup += ''
    if item.get_custom_description():
        markup += ''
    if item.optf2.get("gift"):
        markup += ''
    if "color" in item.optf2:
        markup += ' '
    if "color_2" in item.optf2:
        markup += ' '
    if "series" in item.optf2:
        markup += '' + str(item.optf2["series"]) + ''
    if "craft_number" in item.optf2:
        markup += '
' + str(item.optf2["craft_number"]) + '
'
    if "particle-id" in item.optf2:
        markup += ''
    if "custom texture" in item.optf2:
        markup += ''
    if equipped:
        markup += ''
    if quantity > 1:
        markup += '' + str(quantity) + ''

    markup += '
' + item.optf2["cell_name"]

    painty = item.optf2.get("painted_text", "")

    markup += generate_item_type_line(item)

    markup += generate_item_description(item)

    markup += generate_attribute_list(item, showlinks = False)

    markup += generate_item_paint_line(item)

    pricestr = generate_item_price_string(item, pricestats)
    if pricestr: markup += '
{0}
'.format(pricestr)

    markup += '

\n'

    return markup

def generate_class_sprite_img(c, styleextra = "", mode = None):
    if not mode: mode = web.ctx.current_game or "tf2"

    aliasmap = {
        "tf2b": "tf2",
        "d2b": "d2"
        }
    sheetwidth = 512
    spritesize = 16

    try:
        mode = aliasmap.get(mode, mode)
        spriteindex, name = get_class_for_id(c, mode = mode)

        spriteindex *= spritesize
        row, x = divmod(spriteindex, sheetwidth)
        row *= spritesize
        column = x

        style = "background: url('{0}') -{1}px -{2}px;".format(static_prefix + mode + "_class_icons.png", column, row)
    except (ValueError, KeyError):
        style = ""

    return ''.format(static_prefix + "pixel.png", style, styleextra, c)
<commit_msg>sorted_class_list sorts by class ID if no override, add override for multi-hero CID, saner get_class_overrides empty return values<commit_after>"""
Copyright (c) 2008-2011, Anthony Garcia <lagg@lavabit.com>

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""

import web
import re
try: from collections import OrderedDict as odict
except ImportError: odict = dict
from urlparse import urljoin
from optf2.backend import config

virtual_root = config.ini.get("resources", "virtual-root")
static_prefix = config.ini.get("resources", "static-prefix")
particles = config.ini.options("particle-modes")
htmldesc = re.compile("<(?P<tag>.+) ?.*>.+</(?P=tag)>")

celldims = {}
for mode, dims in config.ini.items("page-dimensions"):
    sep = dims.lower().find('x')
    if sep == -1:
        val = int(dims)
        celldims[mode] = {"width": val, "height": val}
    else:
        celldims[mode] = {"width": int(dims[:sep]), "height": int(dims[sep + 1:])}

classoverrides = {
    "tf2": odict([
            (1, "Scout"),
            (3, "Soldier"),
            (7, "Pyro"),
            (4, "Demoman"),
            (6, "Heavy"),
            (9, "Engineer"),
            (5, "Medic"),
            (2, "Sniper"),
            (8, "Spy")
            ]),
    "p2": odict([
            (1, "P-body"),
            (2, "Atlas")
            ]),
    "d2": {
        1000: "Multi-hero"
        }
    }
overridealiases = {
    "tf2b": "tf2",
    "d2b": "d2"
}
reverrides = {}
for k, v in classoverrides.iteritems(): reverrides[k + "_swap"] = dict(zip(v.values(), v.keys()))
classoverrides.update(reverrides)

def get_class_for_id(cid, mode = None):
    overrides, swapped_overrides = get_class_overrides(mode)
    try: realcid = int(cid)
    except ValueError: realcid = cid

    if not overrides: return realcid, cid

    if realcid in overrides:
        return (realcid, overrides[realcid])
    elif realcid in swapped_overrides:
        return (swapped_overrides[realcid], cid)
    else:
        return (realcid, cid)

def get_class_overrides(mode = None):
    if not mode: mode = web.ctx.current_game or "tf2"
    mode = overridealiases.get(mode, mode)

    if mode not in classoverrides: return {}, {}

    return (classoverrides.get(mode),
            reverrides.get(mode + "_swap"))

def sorted_class_list(classes, mode = None):
    overrides, swappedoverrides = get_class_overrides(mode)
    validclasses = [get_class_for_id(c, mode) for c in classes]
    overridelist = list(overrides.iteritems())

    def sort(tup):
        if tup in overridelist: return overridelist.index(tup)
        else: return tup[0]

    validclasses.sort(key = sort)

    return validclasses

def get_page_sizes():
    return celldims

def absolute_url(relative_url):
    return urljoin(web.ctx.homedomain, relative_url)

def generate_mode_url(path = "", mode = None):
    """ Generates a URL appropriate for the current mode
    with path appended to it. """

    cg = mode
    default = "tf2"
    try:
        if not mode:
            cg = web.ctx.current_game or default
    except AttributeError:
        print("Couldn't get current game mode, falling back to tf2")
        cg = default

    return virtual_root + cg + "/" + path

def generate_particle_icon_url(pid, mode = None):
    if not mode: mode = web.ctx.current_game or "tf2"
    if mode in particles: mode = config.ini.get("particle-modes", mode)

    return static_prefix + mode + "_particle_icons/" + str(pid) + ".png"

def generate_item_description(item):
    desc = web.websafe(item.get_custom_description())

    if not desc:
        desc = item.get_description()
        if desc and not htmldesc.search(desc):
            desc = web.websafe(desc)

    if desc: return '
' + desc.replace('\n', '
') + '
'

    return ''

def generate_item_type_line(item, classic = True):
    """ If classic is true the full Level X Y
    formatting will be used, if not only the type and other relevant information
    will be """

    origin_name = ""
    levelprefix = ""
    rank = item.get_rank()
    itemorigin = item.get_origin_name()

    if classic and "level" in item.optf2:
        levelprefix = "Level " + str(item.optf2["level"])

    if itemorigin:
        origin_name = " - " + itemorigin

    itype = item.get_type()
    if itype.startswith("TF_"): itype = ""

    rankname = ""
    if rank: rankname = rank["name"]
    if rankname: levelprefix = ""

    return '
{0}{1} {2}{3}
'.format(levelprefix,
                                                                web.websafe(rankname),
                                                                web.websafe(itype),
                                                                web.websafe(origin_name))

def generate_item_url(item, user = None, mode = None):
    """ Intelligently generates a URL linking to
    the given item """

    itemid = item.get_id()
    pathuser = ""

    if not mode: mode = web.ctx.current_game

    if itemid and user:
        try: pathuser = str(user.get_id64())
        except AttributeError: pathuser = str(user)

    if pathuser: pathuser += "/"

    return generate_mode_url("item/" + pathuser + str(itemid or item.get_schema_id()), mode = mode)

def generate_item_paint_line(item):
    """ Returns a "Painted with X" line if available """

    if "paint_name" in item.optf2:
        paintcan = item.optf2["paint_name"]
        return '
{0} with {1}
'.format(item.optf2["painted_text"], web.websafe(paintcan))
    else:
        return ''

def generate_item_price_string(item, stats):
    assets = None

    if not stats: return None

    if isinstance(stats, dict): assets = stats["assets"]
    else: assets = stats

    try: return "Store price: ${0}".format(assets[item].get_price()["USD"])
    except: return None

def generate_attribute_list(item, showlinks = False):
    markup = ''
    list_open = '
'
    list_close = '
'
    extra = item.optf2

    morestr = ""
    if showlinks: morestr = ' (more)'

    for attr in extra["attrs"]:
        desc = attr.get_description_formatted()
        style = ""
        acct = extra.get("{0}_account".format(attr.get_id()))
        contents = extra.get("contents")

        if "color" in attr.optf2: style = ' style="color: #{0};"'.format(attr.optf2["color"])
        markup += '
'.format(attr.get_type(), style)

        if contents and attr.get_name() == "referenced item def":
            markup += extra["content_string"]
            markup += morestr.format(web.http.changequery(contents = 1))
        else:
            if attr.get_value_type() != "html": desc = web.websafe(desc).replace('\n', "
")
            markup += desc

        if acct: markup += morestr.format(generate_mode_url("user/"  + str(acct["id64"])))

        markup += '
'

    for eater in extra["eaters"]: markup += '
{0}
'.format(eater)

    style = item.get_current_style_name()
    if style: markup += '
Style: {0}
'.format(style)

    quantity = item.get_quantity()
    if quantity > 1: markup += '
Quantity: {0}
'.format(quantity)

    if item.is_untradable(): markup += '
Untradable
'
    if item.is_uncraftable(): markup += '
Uncraftable
'

    if not markup:
        # XHTML compliance
        markup = '

'

    return (list_open + markup + list_close)

def generate_cell(item, invalid = False, show_equipped = True, user = None, pricestats = None, mode = None):
    if not item: return '

'

    item_id = item.get_id()
    schema_item = False
    equipped = (len(item.get_equipped_classes()) > 0)
    if not mode: mode = web.ctx.current_game or "tf2"

    if not show_equipped: equipped = False
    if not item_id:
        schema_item = True
        item_id = item.get_schema_id()

    item_link = generate_item_url(item, user, mode = mode)
    quality = item.get_quality()["str"]
    equippedstr = ""
    quantity = item.get_quantity()

    cell_class = "item_cell"
    if not schema_item and item.get_position() <= -1: cell_class += " undropped"

    style = ""
    if "namecolor" in item.optf2:
        style = ' style="border-color: #{0};"'.format(item.optf2["namecolor"])

    markup = ('
' +
              '' +
              '' +
              ''
              ).format(cell_class, quality, item_id, item_link, item.optf2["image_url"], item_id, style)

    contents = item.optf2.get("contents")
    if contents:
        markup += ''
    if item.get_custom_name():
        markup += ''
    if item.get_custom_description():
        markup += ''
    if item.optf2.get("gift"):
        markup += ''
    if "color" in item.optf2:
        markup += ' '
    if "color_2" in item.optf2:
        markup += ' '
    if "series" in item.optf2:
        markup += '' + str(item.optf2["series"]) + ''
    if "craft_number" in item.optf2:
        markup += '
' + str(item.optf2["craft_number"]) + '
'
    if "particle-id" in item.optf2:
        markup += ''
    if "custom texture" in item.optf2:
        markup += ''
    if equipped:
        markup += ''
    if quantity > 1:
        markup += '' + str(quantity) + ''

    markup += '
' + item.optf2["cell_name"]

    painty = item.optf2.get("painted_text", "")

    markup += generate_item_type_line(item)

    markup += generate_item_description(item)

    markup += generate_attribute_list(item, showlinks = False)

    markup += generate_item_paint_line(item)

    pricestr = generate_item_price_string(item, pricestats)
    if pricestr: markup += '
{0}
'.format(pricestr)

    markup += '

\n'

    return markup

def generate_class_sprite_img(c, styleextra = "", mode = None):
    if not mode: mode = web.ctx.current_game or "tf2"

    aliasmap = {
        "tf2b": "tf2",
        "d2b": "d2"
        }
    sheetwidth = 512
    spritesize = 16

    try:
        mode = aliasmap.get(mode, mode)
        spriteindex, name = get_class_for_id(c, mode = mode)

        spriteindex *= spritesize
        row, x = divmod(spriteindex, sheetwidth)
        row *= spritesize
        column = x

        style = "background: url('{0}') -{1}px -{2}px;".format(static_prefix + mode + "_class_icons.png", column, row)
    except (ValueError, KeyError):
        style = ""

    return ''.format(static_prefix + "pixel.png", style, styleextra, c)
<|endoftext|>


<commit_before>"""
This module contains all HTTP status codes implemented as WSGI2 applications
which can also be raised as exceptions.
"""
from __future__ import unicode_literals

class HTTPException(Exception):
    def __init__(self, headers, body):
        self.headers = headers
        self.body = body

    def __call__(self, environ):
        return b'%d %s' % (self.code, self.status), self.headers, self.body

class HTTPError(HTTPException):
    template = '''\

<title>{code} {status}</title>

{code} {status}

{explanation}

{detail}

'''

    def __init__(self, explanation, detail):
        if explanation:
            self.explanation = explanation
        self.detail = detail
        self.headers = [(b'Content-Type', b'text/html; charset=iso-8859-1')]

    def __call__(self, environ):
        if self.template:
            vars = dict([(k, v) for k, v in environ.items()])
            explanation = self.explanation.format(**vars)
            self.body = self.template.format(code=self.code, status=self.status,
                                             explanation=explanation, detail=self.detail)

        return HTTPException.__call__(self, environ)

    def __str__(self):
        return self.detail or self.explanation

#
# Successful 2xx
#

class HTTPOk(HTTPException):
    code = 200
    status = b'OK'

class HTTPCreated(HTTPException):
    code = 201
    status = b'Created'

class HTTPAccepted(HTTPException):
    code = 201
    status = b'Created'

class HTTPNonAuthoritative(HTTPException):
    code = 203
    status = b'Non-Authoritative Information'

class HTTPNoContent(HTTPException):
    code = 204
    status = b'No content'

class HTTPResetContent(HTTPException):
    code = 205
    status = b'Reset Content'

class HTTPPartialContent(HTTPException):
    code = 206
    status = b'Partial Content'

#
# Redirection 3xx
#

class HTTPRedirection(HTTPException):
    def __init__(self, headers=[], body=[], location=None):
        if location:
            headers.append((b'Location', location))
        HTTPException.__init__(self, headers, body)

class HTTPMultipleChoices(HTTPRedirection):
    code = 300
    status = b'Multiple Choices'

class HTTPMovedPermanently(HTTPRedirection):
    code = 301
    status = b'Moved Permanently'

class HTTPFound(HTTPRedirection):
    code = 302
    status = b'Found'

class HTTPSeeOther(HTTPRedirection):
    code = 303
    status = b'See Other'

class HTTPNotModified(HTTPRedirection):
    code = 304
    status = b'Not Modified'

    # TODO make sure this is ok
    def __init__(self, date=None):
        if date:
            tz = b'+0000'
            if date.tzinfo:
                tz = b'%+05d' % date.tzinfo.utcoffset(date)
            headers = [(b'Date', date.strftime(b'%a, %d %b %Y %H:%M:%S ' + tz))]
        HTTPRedirection.__init__(self, headers)

class HTTPUseProxy(HTTPException):
    code = 305
    status = b'Use Proxy'

class HTTPTemporaryRedirect(HTTPException):
    code = 307
    status = b'Temporary Redirect'

#
# Client Error 4xx
#

class HTTPBadRequest(HTTPError):
    """
    Code: 401

    The request could not be understood by the server due to malformed syntax.
    """
    code = 400
    status = b'Bad Request'
    explanation = ('The server could not comply with the request since '
                   'it is either malformed or otherwise incorrect.')

class HTTPUnauthorized(HTTPError):
    """
    Code: 401

    The request requires user authentication.
    """
    code = 401
    status = b'Unauthorized'
    explanation = ('This server could not verify that you are authorized to '
                   'access the document you requested.  Either you supplied the '
                   'wrong credentials (e.g., bad password), or your browser '
                   'does not understand how to supply the credentials required.')

class HTTPForbidden(HTTPError):
    """
    Code: 403

    The server understood the request, but is refusing to fulfill it.
    Authorization will not help and the request SHOULD NOT be repeated. If the
    request method was not HEAD and the server wishes to make public why the
    request has not been fulfilled, it SHOULD describe the reason for the
    refusal in the entity. If the server does not wish to make this information
    available to the client, the status code 404 (Not Found) can be used instead.
    """
    code = 403
    status = b'Unauthorized'
    explanation = 'Access was denied to this resource.'

class HTTPNotFound(HTTPError):
    """
    Code: 404

    The server has not found anything matching the Request-URI. No indication
    is given of whether the condition is temporary or permanent. The 410 (Gone)
    status code SHOULD be used if the server knows, through some internally
    configurable mechanism, that an old resource is permanently unavailable and
    has no forwarding address.
    """
    code = 404
    status = b'Not Found'
    explanation = 'The resource could not be found.'

class HTTPMethodNotAllowed(HTTPError):
    """
    Code: 405

    The method specified in the Request-Line is not allowed for the resource
    identified by the Request-URI.
    """
    code = 405
    status = b'Method Not Allowed'
    explanation = 'The method {REQUEST_METHOD} is not allowed for this resource.'

class HTTPNotAcceptable(HTTPError):
    """
    Code: 406

    The resource identified by the request is only capable of generating
    response entities which have content characteristics not acceptable
    according to the accept headers sent in the request.
    """
    code = 406
    status = b'Not Acceptable'
    explanation = ('The resource could not be generated that was acceptable '
                   'to your browser (content of type {HTTP_ACCEPT}).')

class HTTPProxyAuthenticationRequired(HTTPError):
    """
    Code: 407

    This code is similar to 401 (Unauthorized), but indicates that the client
    must first authenticate itself with the proxy.
    """
    code = 407
    status = b'Proxy Authentication Required'
    explanation = 'Authentication with a local proxy is needed.'

class HTTPRequestTimeout(HTTPError):
    """
    The client did not produce a request within the time that the server was
    prepared to wait.
    """
    code = 408
    status = b'Request Timeout'
    explanation = 'The server has waited too long for the request to be sent by the client.'

class HTTPConflict(HTTPError):
    """
    Code: 409

    The request could not be completed due to a conflict with the current state
    of the resource.
    """
    code = 409
    status = b'Conflict'
    explanation = 'There was a conflict when trying to complete your request.'

class HTTPGone(HTTPError):
    """
    Code: 410

    The requested resource is no longer available at the server and no
    forwarding address is known. This condition is expected to be considered
    permanent.
    """
    code = 410
    status = b'Gone'
    explanation = 'This resource is no longer available. No forwarding address is given.'

class HTTPLengthRequired(HTTPError):
    """
    Code: 411

    The server refuses to accept the request without a defined Content- Length.
    """
    code = 411
    status = b'Length Required'
    explanation = 'Content-Length header required.'

class HTTPPreconditionFailed(HTTPError):
    """
    Code: 412

    The precondition given in one or more of the request-header fields
    evaluated to false when it was tested on the server. This response code
    allows the client to place preconditions on the current resource
    metainformation (header field data) and thus prevent the requested method
    from being applied to a resource other than the one intended.
    """
    code = 412
    status = b'Precondition Failed'
    explanation = 'Request precondition failed.'

class HTTPRequestEntityTooLarge(HTTPError):
    """
    Code: 413

    The server is refusing to process a request because the request entity is
    larger than the server is willing or able to process.
    """
    code = 413
    status = b'Request Entity Too Large'
    explanation = 'The body of your request was too large for this server.'

class HTTPRequestURITooLong(HTTPError):
    """
    Code: 414

    The server is refusing to service the request because the Request-URI is
    longer than the server is willing to interpret.
    """
    code = 414
    status = b'Request-URI Too Long'
    explanation = 'The request URI was too long for this server.'

class HTTPUnsupportedMediaType(HTTPError):
    """
    Code: 415

    The server is refusing to service the request because the entity of the
    request is in a format not supported by the requested resource for the
    requested method.
    """
    code = 415
    status = b'Unsupported Media Type'
    explanation = 'The request media type is not supported by this server.'

class HTTPRequestedRangeNotSatisfiable(HTTPError):
    """
    Code: 416

    The server is refusing to service the request because the entity of the
    request is in a format not supported by the requested resource for the
    requested method.
    """
    code = 416
    status = b'Requested Range Not Satisfiable'
    explanation = 'The Range requested is not available.'

class HTTPExpectationFailed(HTTPError):
    """
    Code: 417

    This indidcates that the expectation given in an Expect
    request-header field could not be met by this server.
    """
    code = 417
    status = b'Expectation Failed'
    explanation = 'Expectation failed.'

#
# WebDAV only 4xx exceptions
#

class HTTPUnprocessableEntity(HTTPError):
    """
    Code: 422

    This indicates that the server is unable to process the contained
    instructions.
    Only for WebDAV.
    """
    code = 422
    status = 'Unprocessable Entity'
    explanation = 'Unable to process the contained instructions'

class HTTPLocked(HTTPError):
    """
    Code: 424

    This indicates that the resource is locked.
    Only for WebDAV.
    """
    code = 423
    status = 'Locked'
    explanation = 'The resource is locked'

class HTTPFailedDependency(HTTPError):
    """
    Code: 424

    This indicates that the method could not be performed because the
    requested action depended on another action and that action failed.
    Only for WebDAV.
    """
    code = 424
    status = 'Failed Dependency'
    explanation = ('The method could not be performed because the requested '
                   'action dependended on another action and that action failed')

#
# Server Error 5xx
#

class HTTPInternalServerError(HTTPError):
    """
    Code: 500

    The server encountered an unexpected condition which prevented it from
    fulfilling the request.
    """
    code = 500
    status = b'Internal Server Error'
    explanation = (
        'The server has either erred or is incapable of performing the '
        'requested operation.')

class HTTPNotImplemented(HTTPError):
    """
    Code: 501

    The server does not support the functionality required to fulfill the request.
    This is the appropriate response when the server does not recognize the request
    method and is not capable of supporting it for any resource.
    """
    code = 501
    status = b'Not Implemented'
    explanation = 'The request method {REQUEST_METHOD} is not implemented for this server.'

class HTTPBadGateway(HTTPError):
    """
    Code: 502

    The server, while acting as a gateway or proxy, received an invalid response
    from the upstream server it accessed in attempting to fulfill the request.
    """
    code = 502
    status = b'Bad Gateway'
    explanation = 'The upstream server is currently unavailable.'

class HTTPServiceUnavailable(HTTPError):
    """
    Code: 503

    The server is currently unable to handle the request due to a temporary
    overloading or maintenance of the server.
    """
    code = 503
    status = b'Service Unavailable'
    explanation = 'The server is currently unavailable. Please try again at a later time.'

class HTTPGatewayTimeout(HTTPError):
    """
    Code: 504

    The server, while acting as a gateway or proxy, did not receive a timely
    response from the upstream server specified by the URI (e.g. HTTP, FTP, LDAP)
    or some other auxiliary server (e.g. DNS) it needed to access in attempting
    to complete the request.
    """
    code = 504
    status = b'Gateway Timeout'
    explanation = 'The gateway has timed out.'

class HTTPVersionNotSupported(HTTPError):
    """
    Code: 505

    The server does not support, or refuses to support, the HTTP protocol
    version that was used in the request message.
    """
    code = 505
    status = b'HTTP Version Not Supported'
    explanation = 'The HTTP version is not supported.'
<commit_msg>Copy-pasted explanation in docstring of code 417 from the HTTP/1.1 spec<commit_after>"""
This module contains all HTTP status codes implemented as WSGI2 applications
which can also be raised as exceptions.
"""
from __future__ import unicode_literals

class HTTPException(Exception):
    def __init__(self, headers, body):
        self.headers = headers
        self.body = body

    def __call__(self, environ):
        return b'%d %s' % (self.code, self.status), self.headers, self.body

class HTTPError(HTTPException):
    template = '''\

<title>{code} {status}</title>

{code} {status}

{explanation}

{detail}

'''

    def __init__(self, explanation, detail):
        if explanation:
            self.explanation = explanation
        self.detail = detail
        self.headers = [(b'Content-Type', b'text/html; charset=iso-8859-1')]

    def __call__(self, environ):
        if self.template:
            vars = dict([(k, v) for k, v in environ.items()])
            explanation = self.explanation.format(**vars)
            self.body = self.template.format(code=self.code, status=self.status,
                                             explanation=explanation, detail=self.detail)

        return HTTPException.__call__(self, environ)

    def __str__(self):
        return self.detail or self.explanation

#
# Successful 2xx
#

class HTTPOk(HTTPException):
    code = 200
    status = b'OK'

class HTTPCreated(HTTPException):
    code = 201
    status = b'Created'

class HTTPAccepted(HTTPException):
    code = 201
    status = b'Created'

class HTTPNonAuthoritative(HTTPException):
    code = 203
    status = b'Non-Authoritative Information'

class HTTPNoContent(HTTPException):
    code = 204
    status = b'No content'

class HTTPResetContent(HTTPException):
    code = 205
    status = b'Reset Content'

class HTTPPartialContent(HTTPException):
    code = 206
    status = b'Partial Content'

#
# Redirection 3xx
#

class HTTPRedirection(HTTPException):
    def __init__(self, headers=[], body=[], location=None):
        if location:
            headers.append((b'Location', location))
        HTTPException.__init__(self, headers, body)

class HTTPMultipleChoices(HTTPRedirection):
    code = 300
    status = b'Multiple Choices'

class HTTPMovedPermanently(HTTPRedirection):
    code = 301
    status = b'Moved Permanently'

class HTTPFound(HTTPRedirection):
    code = 302
    status = b'Found'

class HTTPSeeOther(HTTPRedirection):
    code = 303
    status = b'See Other'

class HTTPNotModified(HTTPRedirection):
    code = 304
    status = b'Not Modified'

    # TODO make sure this is ok
    def __init__(self, date=None):
        if date:
            tz = b'+0000'
            if date.tzinfo:
                tz = b'%+05d' % date.tzinfo.utcoffset(date)
            headers = [(b'Date', date.strftime(b'%a, %d %b %Y %H:%M:%S ' + tz))]
        HTTPRedirection.__init__(self, headers)

class HTTPUseProxy(HTTPException):
    code = 305
    status = b'Use Proxy'

class HTTPTemporaryRedirect(HTTPException):
    code = 307
    status = b'Temporary Redirect'

#
# Client Error 4xx
#

class HTTPBadRequest(HTTPError):
    """
    Code: 401

    The request could not be understood by the server due to malformed syntax.
    """
    code = 400
    status = b'Bad Request'
    explanation = ('The server could not comply with the request since '
                   'it is either malformed or otherwise incorrect.')

class HTTPUnauthorized(HTTPError):
    """
    Code: 401

    The request requires user authentication.
    """
    code = 401
    status = b'Unauthorized'
    explanation = ('This server could not verify that you are authorized to '
                   'access the document you requested.  Either you supplied the '
                   'wrong credentials (e.g., bad password), or your browser '
                   'does not understand how to supply the credentials required.')

class HTTPForbidden(HTTPError):
    """
    Code: 403

    The server understood the request, but is refusing to fulfill it.
    Authorization will not help and the request SHOULD NOT be repeated. If the
    request method was not HEAD and the server wishes to make public why the
    request has not been fulfilled, it SHOULD describe the reason for the
    refusal in the entity. If the server does not wish to make this information
    available to the client, the status code 404 (Not Found) can be used instead.
    """
    code = 403
    status = b'Unauthorized'
    explanation = 'Access was denied to this resource.'

class HTTPNotFound(HTTPError):
    """
    Code: 404

    The server has not found anything matching the Request-URI. No indication
    is given of whether the condition is temporary or permanent. The 410 (Gone)
    status code SHOULD be used if the server knows, through some internally
    configurable mechanism, that an old resource is permanently unavailable and
    has no forwarding address.
    """
    code = 404
    status = b'Not Found'
    explanation = 'The resource could not be found.'

class HTTPMethodNotAllowed(HTTPError):
    """
    Code: 405

    The method specified in the Request-Line is not allowed for the resource
    identified by the Request-URI.
    """
    code = 405
    status = b'Method Not Allowed'
    explanation = 'The method {REQUEST_METHOD} is not allowed for this resource.'

class HTTPNotAcceptable(HTTPError):
    """
    Code: 406

    The resource identified by the request is only capable of generating
    response entities which have content characteristics not acceptable
    according to the accept headers sent in the request.
    """
    code = 406
    status = b'Not Acceptable'
    explanation = ('The resource could not be generated that was acceptable '
                   'to your browser (content of type {HTTP_ACCEPT}).')

class HTTPProxyAuthenticationRequired(HTTPError):
    """
    Code: 407

    This code is similar to 401 (Unauthorized), but indicates that the client
    must first authenticate itself with the proxy.
    """
    code = 407
    status = b'Proxy Authentication Required'
    explanation = 'Authentication with a local proxy is needed.'

class HTTPRequestTimeout(HTTPError):
    """
    The client did not produce a request within the time that the server was
    prepared to wait.
    """
    code = 408
    status = b'Request Timeout'
    explanation = 'The server has waited too long for the request to be sent by the client.'

class HTTPConflict(HTTPError):
    """
    Code: 409

    The request could not be completed due to a conflict with the current state
    of the resource.
    """
    code = 409
    status = b'Conflict'
    explanation = 'There was a conflict when trying to complete your request.'

class HTTPGone(HTTPError):
    """
    Code: 410

    The requested resource is no longer available at the server and no
    forwarding address is known. This condition is expected to be considered
    permanent.
    """
    code = 410
    status = b'Gone'
    explanation = 'This resource is no longer available. No forwarding address is given.'

class HTTPLengthRequired(HTTPError):
    """
    Code: 411

    The server refuses to accept the request without a defined Content- Length.
    """
    code = 411
    status = b'Length Required'
    explanation = 'Content-Length header required.'

class HTTPPreconditionFailed(HTTPError):
    """
    Code: 412

    The precondition given in one or more of the request-header fields
    evaluated to false when it was tested on the server. This response code
    allows the client to place preconditions on the current resource
    metainformation (header field data) and thus prevent the requested method
    from being applied to a resource other than the one intended.
    """
    code = 412
    status = b'Precondition Failed'
    explanation = 'Request precondition failed.'

class HTTPRequestEntityTooLarge(HTTPError):
    """
    Code: 413

    The server is refusing to process a request because the request entity is
    larger than the server is willing or able to process.
    """
    code = 413
    status = b'Request Entity Too Large'
    explanation = 'The body of your request was too large for this server.'

class HTTPRequestURITooLong(HTTPError):
    """
    Code: 414

    The server is refusing to service the request because the Request-URI is
    longer than the server is willing to interpret.
    """
    code = 414
    status = b'Request-URI Too Long'
    explanation = 'The request URI was too long for this server.'

class HTTPUnsupportedMediaType(HTTPError):
    """
    Code: 415

    The server is refusing to service the request because the entity of the
    request is in a format not supported by the requested resource for the
    requested method.
    """
    code = 415
    status = b'Unsupported Media Type'
    explanation = 'The request media type is not supported by this server.'

class HTTPRequestedRangeNotSatisfiable(HTTPError):
    """
    Code: 416

    The server is refusing to service the request because the entity of the
    request is in a format not supported by the requested resource for the
    requested method.
    """
    code = 416
    status = b'Requested Range Not Satisfiable'
    explanation = 'The Range requested is not available.'

class HTTPExpectationFailed(HTTPError):
    """
    Code: 417

    The expectation given in an Expect request-header field could not be met by
    this server, or, if the server is a proxy, the server has unambiguous
    evidence that the request could not be met by the next-hop server.
    """
    code = 417
    status = b'Expectation Failed'
    explanation = 'Expectation failed.'

#
# WebDAV only 4xx exceptions
#

class HTTPUnprocessableEntity(HTTPError):
    """
    Code: 422

    This indicates that the server is unable to process the contained
    instructions.
    Only for WebDAV.
    """
    code = 422
    status = 'Unprocessable Entity'
    explanation = 'Unable to process the contained instructions'

class HTTPLocked(HTTPError):
    """
    Code: 424

    This indicates that the resource is locked.
    Only for WebDAV.
    """
    code = 423
    status = 'Locked'
    explanation = 'The resource is locked'

class HTTPFailedDependency(HTTPError):
    """
    Code: 424

    This indicates that the method could not be performed because the
    requested action depended on another action and that action failed.
    Only for WebDAV.
    """
    code = 424
    status = 'Failed Dependency'
    explanation = ('The method could not be performed because the requested '
                   'action dependended on another action and that action failed')

#
# Server Error 5xx
#

class HTTPInternalServerError(HTTPError):
    """
    Code: 500

    The server encountered an unexpected condition which prevented it from
    fulfilling the request.
    """
    code = 500
    status = b'Internal Server Error'
    explanation = (
        'The server has either erred or is incapable of performing the '
        'requested operation.')

class HTTPNotImplemented(HTTPError):
    """
    Code: 501

    The server does not support the functionality required to fulfill the request.
    This is the appropriate response when the server does not recognize the request
    method and is not capable of supporting it for any resource.
    """
    code = 501
    status = b'Not Implemented'
    explanation = 'The request method {REQUEST_METHOD} is not implemented for this server.'

class HTTPBadGateway(HTTPError):
    """
    Code: 502

    The server, while acting as a gateway or proxy, received an invalid response
    from the upstream server it accessed in attempting to fulfill the request.
    """
    code = 502
    status = b'Bad Gateway'
    explanation = 'The upstream server is currently unavailable.'

class HTTPServiceUnavailable(HTTPError):
    """
    Code: 503

    The server is currently unable to handle the request due to a temporary
    overloading or maintenance of the server.
    """
    code = 503
    status = b'Service Unavailable'
    explanation = 'The server is currently unavailable. Please try again at a later time.'

class HTTPGatewayTimeout(HTTPError):
    """
    Code: 504

    The server, while acting as a gateway or proxy, did not receive a timely
    response from the upstream server specified by the URI (e.g. HTTP, FTP, LDAP)
    or some other auxiliary server (e.g. DNS) it needed to access in attempting
    to complete the request.
    """
    code = 504
    status = b'Gateway Timeout'
    explanation = 'The gateway has timed out.'

class HTTPVersionNotSupported(HTTPError):
    """
    Code: 505

    The server does not support, or refuses to support, the HTTP protocol
    version that was used in the request message.
    """
    code = 505
    status = b'HTTP Version Not Supported'
    explanation = 'The HTTP version is not supported.'
<|endoftext|>


<commit_before>#!/usr/bin/env python
#
# Copyright 2010 Hunter Freyer and Michael Kelly
#
# 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.
#

import logging
import random
import urllib2

from google.appengine.api import users
from google.appengine.ext import db

class EntityValidationError(Exception):
  """There was an error validating an entity."""
  pass

class Entity(db.Model):
  name = db.StringProperty(required=True)
  data = db.BlobProperty(required=True)
  query = db.StringProperty(required=False)
  creator = db.UserProperty()

  # Where images live on the server.
  BASE_URL = '/i/'

  # TODO(mjkelly): do something with this or get rid of it
  type = db.StringProperty(choices=set(["abstract", "person", "object"]))

  def __str__(self):
    return self.name

  def __repr__(self):
    return "Entity(%r)" % self.name

  def json(self):
    return {'name': self.name, 'url': self.get_full_url()}

  def get_full_url(self):
    logging.info("get_full_url: returning %s",
        Entity.BASE_URL + str(self.key()))
    return Entity.BASE_URL + str(self.key())

  def get_stats_url(self, w=160, h=85):
    """Returns the URL to a chart of MFK stats for this Entity.

    Args:
      w: (int) image width
      h: (int) image height
    """
    m = self.assignment_reference_marry_set.count()
    f = self.assignment_reference_fuck_set.count()
    k = self.assignment_reference_kill_set.count()

    url = ('http://chart.apis.google.com/chart'
        '?chxr=0,0,%(max)d'
        '&chxt=y'
        '&chbh=a'
        '&chs=%(w)dx%(h)d'
        '&cht=bvg'
        '&chco=9911BB,C76FDD,63067A'
        '&chds=0,%(max)d,0,%(max)d,0,%(max)d'
        '&chd=t:%(m)d|%(f)d|%(k)d'
        '&chdl=Marry|Fuck|Kill'
        '&chdlp=r' % (dict(m=m, f=f, k=k, max=max([m,f,k]), w=w, h=h)))
    return url

def PutEntity(name, url, query):
  if not url.startswith('http://images.google.com/images?q'):
    raise EntityValidationError("URL must come from Google image search.")

  # This could throw URLError. We'll let it bubble up.
  fh = urllib2.urlopen(url)
  logging.info("Downloading %s" % url)
  data = fh.read()
  logging.info("Downloaded %s bytes" % len(data))

  entity = Entity(name=name, data=data, query=query)

  entity.put()
  return entity

class Triple(db.Model):
  # User who created the Triple, or None if no user was logged in.
  creator = db.UserProperty()
  # A measure of quality. Currently ignored.
  quality = db.FloatProperty(default=1.0)
  # A random value used to determine a randomized, repeatable display order.
  # Not necessarily unique. 'default' value is for backwards-compatibility.
  rand = db.FloatProperty(required=True, default=0.0)

  one = db.ReferenceProperty(Entity,
                 collection_name="triple_reference_one_set")
  two = db.ReferenceProperty(Entity,
                 collection_name="triple_reference_two_set")
  three = db.ReferenceProperty(Entity,
                 collection_name="triple_reference_three_set")

  CONTEXT_COOKIE_NAME = 'mfkcontext'

  @staticmethod
  def get_next_id(request, response):
    """Gets the next Triple ID, apparently at random.

    We use cookies in the request, if available, to determine what the next
    Triple will be.

    Args:
      request: request object from the client
      response: the response object that will be sent to the client.

    Returns:
      (ID) The ID of the next Triple, and a cookie to set on the client
           to maintain state.
    """
    # We _could_ also store the random value on the client side if we want to
    # cut down on the number of DB requests we make.
    prev_id = None
    next_id = None
    prev_id = long(request.cookies.get(Triple.CONTEXT_COOKIE_NAME))
    logging.info('get_next_id: prev_id = %s', prev_id)
    if prev_id is not None:
      prev_triple = Triple.get_by_id(long(prev_id))
      if prev_triple is not None:
        prev_rand = prev_triple.rand
        next_id = Triple._get_next_id_after_rand(prev_rand)

    # If we failed to intelligently determine the next triple, pick at random.
    if next_id is None:
      next_id = Triple._get_random_id()

    # TODO(mjkelly): Try to find a lightweight wrapper for cookie-setting
    # instead of manually constructing them.
    response.headers.add_header('Set-Cookie', '%s=%d' % (Triple.CONTEXT_COOKIE_NAME, next_id))

    return next_id

  @staticmethod
  def _get_next_id_after_rand(prev_rand):
    """Given the rand value of the last Triple we saw, determine the next.

    This method contains all the real logic.
    """
    logging.debug("_get_next_id_after_rand: prev_rand = %s", prev_rand)
    query = db.Query(Triple, keys_only=True).filter('rand >', prev_rand).order('rand')
    if not query.count():
      if prev_rand >= 0.0:
        # If we were using a rand value > 0, we probably hit the end of the
        # list naturally. Retry with -1, which should return everything.
        return Triple._get_next_id_after_rand(-1.0)
      else:
        # If we returned no results and yet were using a rand value < 0, the
        # database is empty or contains only invalid rand values. Give up. This
        # is the base case for the recursion above.
        return None
    else:
      return query.get().id()

  @staticmethod
  def _get_random_id():
    """Returns a truly random Triple.

    This is a fallback used by get_next_id for when we don't have any context
    on previously-seen Triples from the client.
    """
    rand = random.random()
    logging.debug('_get_random_id: rand = %f', rand)
    query = db.Query(Triple, keys_only=True).filter('rand >', rand).order('rand')
    if query.count():
        return query.get().id()
    else:
      logging.debug('_get_random_id: no results, trying other direction')
      query = db.Query(Triple, keys_only=True).filter('rand <', rand).order('-rand')
      if query.count():
        return query.get().id()

  def __str__(self):
    return Triple.name_from_entities(self.one, self.two, self.three)

  def __repr__(self):
    return "Triple(one=%r, two=%r, three=%r)" % (self.one, self.two, self.three)

  def json(self):
    return {'one': self.one.json(),
        'two': self.two.json(),
        'three': self.three.json(),
        'key': str(self.key())}

  @staticmethod
  def name_from_entities(one, two, three):
    """Given three Entities, generate the canonical key name for a Triple
    containing them.
    """
    keys = [one.name, two.name, three.name]
    keys.sort()
    return "%s.%s.%s" % (keys[0], keys[1], keys[2])

  @staticmethod
  def validate(one, two, three):
    """Verify that the 3 given entities can make a valid triple.

    Raises EntityValidationError if there was a problem.
    """

    if len(set([one.name, two.name, three.name])) < 3:
      raise EntityValidationError("All item names must be distinct.")
    if len(set([one.data, two.data, three.data])) < 3:
      raise EntityValidationError("All item URLs must be distinct.")
    # TODO(mjkelly): implement me
    #raise EntityValidationError("test error")
    pass

def PutTriple(one, two, three, creator):
  """Put a triple in the DB with canonical key.

  See Triple.key_name_from_entities().
  """
  triple = Triple(one=one,
                  two=two,
                  three=three,
                  creator=creator)
  triple.put()
  return triple

class Assignment(db.Model):
  user = db.UserProperty()

  triple = db.ReferenceProperty(Triple)

  marry = db.ReferenceProperty(Entity,
                 collection_name="assignment_reference_marry_set")
  fuck = db.ReferenceProperty(Entity,
                 collection_name="assignment_reference_fuck_set")
  kill = db.ReferenceProperty(Entity,
                 collection_name="assignment_reference_kill_set")

  def __str__(self):
    return "Assignment(marry=%s, fuck=%s, kill=%s)" % (
        self.marry.key().name(),
        self.fuck.key().name(),
        self.kill.key().name())

  def __repr__(self):
    return str(self)

  @staticmethod
  def make_assignment(request):
    triple_key = request.get('key')
    values = [request.get('v1'), request.get('v2'), request.get('v3')]

    logging.debug("triple_key = %s", triple_key)
    logging.debug("values = %s", values)

    if set(values) != set(['m', 'f', 'k']):
      return None

    # We get an entity->action map from the client, but we need to reverse
    # it to action->entity to update the DB.
    triple = Triple.get_by_id(int(triple_key))
    logging.debug("triple = %s", triple)
    if triple is None:
      logging.error("No triple with key %s", triple_key)
      return None

    triple_entities = [triple.one,
                       triple.two,
                       triple.three]
    entities = {}
    for i in range(len(values)):
      # Items in values are guaranteed to be 'm', 'f', 'k' (check above)
      entities[values[i]] = triple_entities[i]

    if (entities['m'] is None or entities['f'] is None
        or entities['k'] is None):
      logging.error("Not all non-None: marry = %s, fuck = %s, kill = %s",
              entities['m'], entities['f'], entities['k'])
      return None

    assign = Assignment(marry=entities['m'],
                        fuck=entities['f'],
                        kill=entities['k'])
    assign.put()
    logging.info("Assigned m=%s, f=%s, k=%s to %s", entities['m'],
        entities['f'], entities['k'], triple)
    return assign
<commit_msg>Order by __key__ as well as rand to ensure stable sorts.<commit_after>#!/usr/bin/env python
#
# Copyright 2010 Hunter Freyer and Michael Kelly
#
# 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.
#

import logging
import random
import urllib2

from google.appengine.api import users
from google.appengine.ext import db

class EntityValidationError(Exception):
  """There was an error validating an entity."""
  pass

class Entity(db.Model):
  name = db.StringProperty(required=True)
  data = db.BlobProperty(required=True)
  query = db.StringProperty(required=False)
  creator = db.UserProperty()

  # Where images live on the server.
  BASE_URL = '/i/'

  # TODO(mjkelly): do something with this or get rid of it
  type = db.StringProperty(choices=set(["abstract", "person", "object"]))

  def __str__(self):
    return self.name

  def __repr__(self):
    return "Entity(%r)" % self.name

  def json(self):
    return {'name': self.name, 'url': self.get_full_url()}

  def get_full_url(self):
    logging.info("get_full_url: returning %s",
        Entity.BASE_URL + str(self.key()))
    return Entity.BASE_URL + str(self.key())

  def get_stats_url(self, w=160, h=85):
    """Returns the URL to a chart of MFK stats for this Entity.

    Args:
      w: (int) image width
      h: (int) image height
    """
    m = self.assignment_reference_marry_set.count()
    f = self.assignment_reference_fuck_set.count()
    k = self.assignment_reference_kill_set.count()

    url = ('http://chart.apis.google.com/chart'
        '?chxr=0,0,%(max)d'
        '&chxt=y'
        '&chbh=a'
        '&chs=%(w)dx%(h)d'
        '&cht=bvg'
        '&chco=9911BB,C76FDD,63067A'
        '&chds=0,%(max)d,0,%(max)d,0,%(max)d'
        '&chd=t:%(m)d|%(f)d|%(k)d'
        '&chdl=Marry|Fuck|Kill'
        '&chdlp=r' % (dict(m=m, f=f, k=k, max=max([m,f,k]), w=w, h=h)))
    return url

def PutEntity(name, url, query):
  if not url.startswith('http://images.google.com/images?q'):
    raise EntityValidationError("URL must come from Google image search.")

  # This could throw URLError. We'll let it bubble up.
  fh = urllib2.urlopen(url)
  logging.info("Downloading %s" % url)
  data = fh.read()
  logging.info("Downloaded %s bytes" % len(data))

  entity = Entity(name=name, data=data, query=query)

  entity.put()
  return entity

class Triple(db.Model):
  # User who created the Triple, or None if no user was logged in.
  creator = db.UserProperty()
  # A measure of quality. Currently ignored.
  quality = db.FloatProperty(default=1.0)
  # A random value used to determine a randomized, repeatable display order.
  # Not necessarily unique. 'default' value is for backwards-compatibility.
  rand = db.FloatProperty(required=True, default=0.0)

  one = db.ReferenceProperty(Entity,
                 collection_name="triple_reference_one_set")
  two = db.ReferenceProperty(Entity,
                 collection_name="triple_reference_two_set")
  three = db.ReferenceProperty(Entity,
                 collection_name="triple_reference_three_set")

  CONTEXT_COOKIE_NAME = 'mfkcontext'

  @staticmethod
  def get_next_id(request, response):
    """Gets the next Triple ID, apparently at random.

    We use cookies in the request, if available, to determine what the next
    Triple will be.

    Args:
      request: request object from the client
      response: the response object that will be sent to the client.

    Returns:
      (ID) The ID of the next Triple, and a cookie to set on the client
           to maintain state.
    """
    # We _could_ also store the random value on the client side if we want to
    # cut down on the number of DB requests we make.
    prev_id = None
    next_id = None
    prev_id = long(request.cookies.get(Triple.CONTEXT_COOKIE_NAME))
    logging.info('get_next_id: prev_id = %s', prev_id)
    if prev_id is not None:
      prev_triple = Triple.get_by_id(long(prev_id))
      if prev_triple is not None:
        prev_rand = prev_triple.rand
        next_id = Triple._get_next_id_after_rand(prev_rand)

    # If we failed to intelligently determine the next triple, pick at random.
    if next_id is None:
      next_id = Triple._get_random_id()

    # TODO(mjkelly): Try to find a lightweight wrapper for cookie-setting
    # instead of manually constructing them.
    response.headers.add_header('Set-Cookie', '%s=%d' % (Triple.CONTEXT_COOKIE_NAME, next_id))

    return next_id

  @staticmethod
  def _get_next_id_after_rand(prev_rand):
    """Given the rand value of the last Triple we saw, determine the next.

    This method contains all the real logic.
    """
    logging.debug("_get_next_id_after_rand: prev_rand = %s", prev_rand)
    query = db.Query(Triple, keys_only=True).filter('rand >',
        prev_rand).order('rand').order('__key__')
    if not query.count():
      if prev_rand >= 0.0:
        # If we were using a rand value > 0, we probably hit the end of the
        # list naturally. Retry with -1, which should return everything.
        return Triple._get_next_id_after_rand(-1.0)
      else:
        # If we returned no results and yet were using a rand value < 0, the
        # database is empty or contains only invalid rand values. Give up. This
        # is the base case for the recursion above.
        return None
    else:
      return query.get().id()

  @staticmethod
  def _get_random_id():
    """Returns a truly random Triple.

    This is a fallback used by get_next_id for when we don't have any context
    on previously-seen Triples from the client.
    """
    rand = random.random()
    logging.debug('_get_random_id: rand = %f', rand)
    query = db.Query(Triple, keys_only=True).filter('rand >',
        rand).order('rand').order('__key__')
    if query.count():
        return query.get().id()
    else:
      logging.debug('_get_random_id: no results, trying other direction')
      query = db.Query(Triple, keys_only=True).filter('rand <',
          rand).order('-rand').order('__key__')
      if query.count():
        return query.get().id()

  def __str__(self):
    return Triple.name_from_entities(self.one, self.two, self.three)

  def __repr__(self):
    return "Triple(one=%r, two=%r, three=%r)" % (self.one, self.two, self.three)

  def json(self):
    return {'one': self.one.json(),
        'two': self.two.json(),
        'three': self.three.json(),
        'key': str(self.key())}

  @staticmethod
  def name_from_entities(one, two, three):
    """Given three Entities, generate the canonical key name for a Triple
    containing them.
    """
    keys = [one.name, two.name, three.name]
    keys.sort()
    return "%s.%s.%s" % (keys[0], keys[1], keys[2])

  @staticmethod
  def validate(one, two, three):
    """Verify that the 3 given entities can make a valid triple.

    Raises EntityValidationError if there was a problem.
    """

    if len(set([one.name, two.name, three.name])) < 3:
      raise EntityValidationError("All item names must be distinct.")
    if len(set([one.data, two.data, three.data])) < 3:
      raise EntityValidationError("All item URLs must be distinct.")
    # TODO(mjkelly): implement me
    #raise EntityValidationError("test error")
    pass

def PutTriple(one, two, three, creator):
  """Put a triple in the DB with canonical key.

  See Triple.key_name_from_entities().
  """
  triple = Triple(one=one,
                  two=two,
                  three=three,
                  creator=creator)
  triple.put()
  return triple

class Assignment(db.Model):
  user = db.UserProperty()

  triple = db.ReferenceProperty(Triple)

  marry = db.ReferenceProperty(Entity,
                 collection_name="assignment_reference_marry_set")
  fuck = db.ReferenceProperty(Entity,
                 collection_name="assignment_reference_fuck_set")
  kill = db.ReferenceProperty(Entity,
                 collection_name="assignment_reference_kill_set")

  def __str__(self):
    return "Assignment(marry=%s, fuck=%s, kill=%s)" % (
        self.marry.key().name(),
        self.fuck.key().name(),
        self.kill.key().name())

  def __repr__(self):
    return str(self)

  @staticmethod
  def make_assignment(request):
    triple_key = request.get('key')
    values = [request.get('v1'), request.get('v2'), request.get('v3')]

    logging.debug("triple_key = %s", triple_key)
    logging.debug("values = %s", values)

    if set(values) != set(['m', 'f', 'k']):
      return None

    # We get an entity->action map from the client, but we need to reverse
    # it to action->entity to update the DB.
    triple = Triple.get_by_id(int(triple_key))
    logging.debug("triple = %s", triple)
    if triple is None:
      logging.error("No triple with key %s", triple_key)
      return None

    triple_entities = [triple.one,
                       triple.two,
                       triple.three]
    entities = {}
    for i in range(len(values)):
      # Items in values are guaranteed to be 'm', 'f', 'k' (check above)
      entities[values[i]] = triple_entities[i]

    if (entities['m'] is None or entities['f'] is None
        or entities['k'] is None):
      logging.error("Not all non-None: marry = %s, fuck = %s, kill = %s",
              entities['m'], entities['f'], entities['k'])
      return None

    assign = Assignment(marry=entities['m'],
                        fuck=entities['f'],
                        kill=entities['k'])
    assign.put()
    logging.info("Assigned m=%s, f=%s, k=%s to %s", entities['m'],
        entities['f'], entities['k'], triple)
    return assign
<|endoftext|>


<commit_before>import requests
import bs4
from six.moves import urllib

class Browser:
    def __init__(self, session=None):
        self.session = session or requests.Session()

    @staticmethod
    def add_soup(response):
        if 'text/html' in response.headers.get('Content-Type', ''):
            try:
                response.soup = bs4.BeautifulSoup(response.content)
            except:
                pass

    def get(self, *args, **kwargs):
        response = self.session.get(*args, **kwargs)
        Browser.add_soup(response)
        return response

    def post(self, *args, **kwargs):
        response = self.session.post(*args, **kwargs)
        Browser.add_soup(response)
        return response

    def _build_request(self, form, url=None):
        method = form['method']
        action = form['action']
        url = urllib.parse.urljoin(url, action)

        # read http://www.w3.org/TR/html5/forms.html
        data = dict()
        files = dict()

        for input in form.select("input"):
            name = input.get('name')
            value = input.get('value', '') # web browsers use empty string for inputs with missing values
            if not name:
                continue

            if input.get('type') in ('radio', 'checkbox') and 'checked' not in input.attrs:
                continue

            if input.get('type') == 'checkbox':
                if not name in data:
                    data[name] = list()
                data[name].append(value)

            elif input.get('type') == 'file':
                # read http://www.cs.tut.fi/~jkorpela/forms/file.html
                # in web browsers, file upload only happens if the form's (or submit button's) enctype attribute is set to 'multipart/form-data'. we don't care, simplify.
                if not value:
                    continue
                files[name] = open(value, 'rb')

            else:
                data[name] = value

        for textarea in form.select("textarea"):
            name = textarea.get('name')
            if not name:
                continue
            data[name] = textarea.text

        return requests.Request(method, url, data=data, files=files)

    def _prepare_request(self, form, url=None):
        request = self._build_request(form, url)
        return self.session.prepare_request(request)

    def submit(self, form, url=None):
        request = self._prepare_request(form, url)
        response = self.session.send(request)
        Browser.add_soup(response)
        return response
<commit_msg>Open file only if the value is a string<commit_after>import requests
import bs4
from six.moves import urllib
from six import string_types

class Browser:
    def __init__(self, session=None):
        self.session = session or requests.Session()

    @staticmethod
    def add_soup(response):
        if 'text/html' in response.headers.get('Content-Type', ''):
            try:
                response.soup = bs4.BeautifulSoup(response.content)
            except:
                pass

    def get(self, *args, **kwargs):
        response = self.session.get(*args, **kwargs)
        Browser.add_soup(response)
        return response

    def post(self, *args, **kwargs):
        response = self.session.post(*args, **kwargs)
        Browser.add_soup(response)
        return response

    def _build_request(self, form, url=None):
        method = form['method']
        action = form['action']
        url = urllib.parse.urljoin(url, action)

        # read http://www.w3.org/TR/html5/forms.html
        data = dict()
        files = dict()

        for input in form.select("input"):
            name = input.get('name')
            value = input.get('value', '') # web browsers use empty string for inputs with missing values
            if not name:
                continue

            if input.get('type') in ('radio', 'checkbox') and 'checked' not in input.attrs:
                continue

            if input.get('type') == 'checkbox':
                if not name in data:
                    data[name] = list()
                data[name].append(value)

            elif input.get('type') == 'file':
                # read http://www.cs.tut.fi/~jkorpela/forms/file.html
                # in web browsers, file upload only happens if the form's (or submit button's) enctype attribute is set to 'multipart/form-data'. we don't care, simplify.
                if not value:
                    continue
                if isinstance(value, string_types):
                    value = open(value, 'rb')
                files[name] = value

            else:
                data[name] = value

        for textarea in form.select("textarea"):
            name = textarea.get('name')
            if not name:
                continue
            data[name] = textarea.text

        return requests.Request(method, url, data=data, files=files)

    def _prepare_request(self, form, url=None):
        request = self._build_request(form, url)
        return self.session.prepare_request(request)

    def submit(self, form, url=None):
        request = self._prepare_request(form, url)
        response = self.session.send(request)
        Browser.add_soup(response)
        return response
<|endoftext|>