prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  [plugin] Regression: Webhook Test Button
  Somehow during transfer to the new plugin architecture, the webhook plugin's Test button has disappeared.
  
  </issue>
  <code>
  [start of src/sentry/api/endpoints/project_plugin_details.py]
  1 from __future__ import absolute_import
  2 
  3 import six
  4 
  5 from django import forms
  6 from django.core.urlresolvers import reverse
  7 from rest_framework import serializers
  8 from rest_framework.response import Response
  9 
  10 from sentry.exceptions import PluginError, PluginIdentityRequired
  11 from sentry.plugins import plugins
  12 from sentry.api.bases.project import ProjectEndpoint
  13 from sentry.api.exceptions import ResourceDoesNotExist
  14 from sentry.api.serializers import serialize
  15 from sentry.api.serializers.models.plugin import (
  16     PluginSerializer, PluginWithConfigSerializer, serialize_field
  17 )
  18 from sentry.signals import plugin_enabled
  19 
  20 ERR_ALWAYS_ENABLED = 'This plugin is always enabled.'
  21 ERR_FIELD_REQUIRED = 'This field is required.'
  22 
  23 OK_UPDATED = 'Successfully updated configuration.'
  24 
  25 
  26 class ProjectPluginDetailsEndpoint(ProjectEndpoint):
  27     def _get_plugin(self, plugin_id):
  28         try:
  29             return plugins.get(plugin_id)
  30         except KeyError:
  31             raise ResourceDoesNotExist
  32 
  33     def get(self, request, project, plugin_id):
  34         plugin = self._get_plugin(plugin_id)
  35 
  36         try:
  37             context = serialize(plugin, request.user, PluginWithConfigSerializer(project))
  38         except PluginIdentityRequired as e:
  39             context = serialize(plugin, request.user, PluginSerializer(project))
  40             context['config_error'] = e.message
  41             context['auth_url'] = reverse('socialauth_associate', args=[plugin.slug])
  42 
  43         return Response(context)
  44 
  45     def post(self, request, project, plugin_id):
  46         """
  47         Enable plugin
  48         """
  49         plugin = self._get_plugin(plugin_id)
  50 
  51         if not plugin.can_disable:
  52             return Response({'detail': ERR_ALWAYS_ENABLED}, status=400)
  53 
  54         plugin.enable(project)
  55 
  56         return Response(status=201)
  57 
  58     def delete(self, request, project, plugin_id):
  59         """
  60         Disable plugin
  61         """
  62         plugin = self._get_plugin(plugin_id)
  63 
  64         if not plugin.can_disable:
  65             return Response({'detail': ERR_ALWAYS_ENABLED}, status=400)
  66 
  67         plugin.disable(project)
  68 
  69         return Response(status=204)
  70 
  71     def put(self, request, project, plugin_id):
  72         plugin = self._get_plugin(plugin_id)
  73 
  74         config = [
  75             serialize_field(project, plugin, c)
  76             for c in plugin.get_config(
  77                 project=project,
  78                 user=request.user,
  79             )
  80         ]
  81 
  82         cleaned = {}
  83         errors = {}
  84         for field in config:
  85             key = field['name']
  86             value = request.DATA.get(key)
  87 
  88             if field.get('required') and not value:
  89                 errors[key] = ERR_FIELD_REQUIRED
  90 
  91             try:
  92                 value = plugin.validate_config_field(
  93                     project=project,
  94                     name=key,
  95                     value=value,
  96                     actor=request.user,
  97                 )
  98             except (forms.ValidationError, serializers.ValidationError, PluginError) as e:
  99                 errors[key] = e.message
  100 
  101             if not errors.get(key):
  102                 cleaned[key] = value
  103 
  104         if not errors:
  105             try:
  106                 cleaned = plugin.validate_config(
  107                     project=project,
  108                     config=cleaned,
  109                     actor=request.user,
  110                 )
  111             except PluginError as e:
  112                 errors['__all__'] = e.message
  113 
  114         if errors:
  115             return Response(
  116                 {
  117                     'errors': errors,
  118                 }, status=400
  119             )
  120 
  121         for key, value in six.iteritems(cleaned):
  122             if value is None:
  123                 plugin.unset_option(
  124                     project=project,
  125                     key=key,
  126                 )
  127             else:
  128                 plugin.set_option(
  129                     project=project,
  130                     key=key,
  131                     value=value,
  132                 )
  133 
  134         context = serialize(plugin, request.user, PluginWithConfigSerializer(project))
  135 
  136         plugin_enabled.send(plugin=plugin, project=project, user=request.user, sender=self)
  137 
  138         return Response(context)
  139
  [end of src/sentry/api/endpoints/project_plugin_details.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/src/sentry/api/endpoints/project_plugin_details.py b/src/sentry/api/endpoints/project_plugin_details.py
  --- a/src/sentry/api/endpoints/project_plugin_details.py
  +++ b/src/sentry/api/endpoints/project_plugin_details.py
  @@ -1,11 +1,13 @@
   from __future__ import absolute_import
   
  +import logging
   import six
   
   from django import forms
   from django.core.urlresolvers import reverse
   from rest_framework import serializers
   from rest_framework.response import Response
  +from requests.exceptions import HTTPError
   
   from sentry.exceptions import PluginError, PluginIdentityRequired
   from sentry.plugins import plugins
  @@ -44,10 +46,26 @@
   
       def post(self, request, project, plugin_id):
           """
  -        Enable plugin
  +        Enable plugin or Test plugin
           """
           plugin = self._get_plugin(plugin_id)
   
  +        if request.DATA.get('test') and plugin.is_testable():
  +            try:
  +                test_results = plugin.test_configuration(project)
  +            except Exception as exc:
  +                if isinstance(exc, HTTPError):
  +                    test_results = '%s\n%s' % (exc, exc.response.text[:256])
  +                elif hasattr(exc, 'read') and callable(exc.read):
  +                    test_results = '%s\n%s' % (exc, exc.read()[:256])
  +                else:
  +                    logging.exception('Plugin(%s) raised an error during test',
  +                                      plugin_id)
  +                    test_results = 'There was an internal error with the Plugin'
  +            if not test_results:
  +                test_results = 'No errors returned'
  +            return Response({'detail': test_results}, status=200)
  +
           if not plugin.can_disable:
               return Response({'detail': ERR_ALWAYS_ENABLED}, status=400)
  
verification_info: {"golden_diff": "diff --git a/src/sentry/api/endpoints/project_plugin_details.py b/src/sentry/api/endpoints/project_plugin_details.py\n--- a/src/sentry/api/endpoints/project_plugin_details.py\n+++ b/src/sentry/api/endpoints/project_plugin_details.py\n@@ -1,11 +1,13 @@\n from __future__ import absolute_import\n \n+import logging\n import six\n \n from django import forms\n from django.core.urlresolvers import reverse\n from rest_framework import serializers\n from rest_framework.response import Response\n+from requests.exceptions import HTTPError\n \n from sentry.exceptions import PluginError, PluginIdentityRequired\n from sentry.plugins import plugins\n@@ -44,10 +46,26 @@\n \n     def post(self, request, project, plugin_id):\n         \"\"\"\n-        Enable plugin\n+        Enable plugin or Test plugin\n         \"\"\"\n         plugin = self._get_plugin(plugin_id)\n \n+        if request.DATA.get('test') and plugin.is_testable():\n+            try:\n+                test_results = plugin.test_configuration(project)\n+            except Exception as exc:\n+                if isinstance(exc, HTTPError):\n+                    test_results = '%s\\n%s' % (exc, exc.response.text[:256])\n+                elif hasattr(exc, 'read') and callable(exc.read):\n+                    test_results = '%s\\n%s' % (exc, exc.read()[:256])\n+                else:\n+                    logging.exception('Plugin(%s) raised an error during test',\n+                                      plugin_id)\n+                    test_results = 'There was an internal error with the Plugin'\n+            if not test_results:\n+                test_results = 'No errors returned'\n+            return Response({'detail': test_results}, status=200)\n+\n         if not plugin.can_disable:\n             return Response({'detail': ERR_ALWAYS_ENABLED}, status=400)\n", "issue": "[plugin] Regression: Webhook Test Button\nSomehow during transfer to the new plugin architecture, the webhook plugin's Test button has disappeared.\n", "before_files": [{"content": "from __future__ import absolute_import\n\nimport six\n\nfrom django import forms\nfrom django.core.urlresolvers import reverse\nfrom rest_framework import serializers\nfrom rest_framework.response import Response\n\nfrom sentry.exceptions import PluginError, PluginIdentityRequired\nfrom sentry.plugins import plugins\nfrom sentry.api.bases.project import ProjectEndpoint\nfrom sentry.api.exceptions import ResourceDoesNotExist\nfrom sentry.api.serializers import serialize\nfrom sentry.api.serializers.models.plugin import (\n    PluginSerializer, PluginWithConfigSerializer, serialize_field\n)\nfrom sentry.signals import plugin_enabled\n\nERR_ALWAYS_ENABLED = 'This plugin is always enabled.'\nERR_FIELD_REQUIRED = 'This field is required.'\n\nOK_UPDATED = 'Successfully updated configuration.'\n\n\nclass ProjectPluginDetailsEndpoint(ProjectEndpoint):\n    def _get_plugin(self, plugin_id):\n        try:\n            return plugins.get(plugin_id)\n        except KeyError:\n            raise ResourceDoesNotExist\n\n    def get(self, request, project, plugin_id):\n        plugin = self._get_plugin(plugin_id)\n\n        try:\n            context = serialize(plugin, request.user, PluginWithConfigSerializer(project))\n        except PluginIdentityRequired as e:\n            context = serialize(plugin, request.user, PluginSerializer(project))\n            context['config_error'] = e.message\n            context['auth_url'] = reverse('socialauth_associate', args=[plugin.slug])\n\n        return Response(context)\n\n    def post(self, request, project, plugin_id):\n        \"\"\"\n        Enable plugin\n        \"\"\"\n        plugin = self._get_plugin(plugin_id)\n\n        if not plugin.can_disable:\n            return Response({'detail': ERR_ALWAYS_ENABLED}, status=400)\n\n        plugin.enable(project)\n\n        return Response(status=201)\n\n    def delete(self, request, project, plugin_id):\n        \"\"\"\n        Disable plugin\n        \"\"\"\n        plugin = self._get_plugin(plugin_id)\n\n        if not plugin.can_disable:\n            return Response({'detail': ERR_ALWAYS_ENABLED}, status=400)\n\n        plugin.disable(project)\n\n        return Response(status=204)\n\n    def put(self, request, project, plugin_id):\n        plugin = self._get_plugin(plugin_id)\n\n        config = [\n            serialize_field(project, plugin, c)\n            for c in plugin.get_config(\n                project=project,\n                user=request.user,\n            )\n        ]\n\n        cleaned = {}\n        errors = {}\n        for field in config:\n            key = field['name']\n            value = request.DATA.get(key)\n\n            if field.get('required') and not value:\n                errors[key] = ERR_FIELD_REQUIRED\n\n            try:\n                value = plugin.validate_config_field(\n                    project=project,\n                    name=key,\n                    value=value,\n                    actor=request.user,\n                )\n            except (forms.ValidationError, serializers.ValidationError, PluginError) as e:\n                errors[key] = e.message\n\n            if not errors.get(key):\n                cleaned[key] = value\n\n        if not errors:\n            try:\n                cleaned = plugin.validate_config(\n                    project=project,\n                    config=cleaned,\n                    actor=request.user,\n                )\n            except PluginError as e:\n                errors['__all__'] = e.message\n\n        if errors:\n            return Response(\n                {\n                    'errors': errors,\n                }, status=400\n            )\n\n        for key, value in six.iteritems(cleaned):\n            if value is None:\n                plugin.unset_option(\n                    project=project,\n                    key=key,\n                )\n            else:\n                plugin.set_option(\n                    project=project,\n                    key=key,\n                    value=value,\n                )\n\n        context = serialize(plugin, request.user, PluginWithConfigSerializer(project))\n\n        plugin_enabled.send(plugin=plugin, project=project, user=request.user, sender=self)\n\n        return Response(context)\n", "path": "src/sentry/api/endpoints/project_plugin_details.py"}]}