Repository: getredash/redash

Issue:
Updating Alert Destination email field does not change the value in the database
### Issue Summary

Updating the email address list for an Alert Destination does not update the database.

### Steps to Reproduce

1. Create an email alert destination group, refresh page.
2. Change one or more emails, save. Refresh page.
3. The email group is the same as it was when created.
4. Confirm via a DB query (```select * from notification_destinations;```)

### Technical details:

* Redash Version: 1.0.3+b2850
* Browser/OS: Various, consistent behaves the same
* How did you install Redash: ```https://raw.githubusercontent.com/getredash/redash/master/bin/upgrade```

More details:

Checking the API log for the update (when I click the save button) things look ok:

```
[2017-06-23 21:17:26,311][PID:25417][INFO][metrics] method=POST path=/api/destinations/1 endpoint=destination status=200 content_type=application/json content_length=144 duration=7.94 query_count=4 query_duration=2.80
```

Enabling statement logging in PostgreSQL shows this happening (with sanitized emails):

```
2017-06-23 21:31:54.550 UTC [459] redash@redash LOG: statement: BEGIN
2017-06-23 21:31:54.551 UTC [459] redash@redash LOG: statement: SELECT organizations.updated_at AS organizations_updated_at, organizations.created_at AS organizations_created_at, organizations.id AS organizations_id, organizations.name AS organizations_name, organizations.slug AS organizations_slug, organizations.settings AS organizations_settings
    FROM organizations
    WHERE organizations.slug = 'default'
     LIMIT 1
2017-06-23 21:31:54.552 UTC [459] redash@redash LOG: statement: SELECT users.groups AS users_groups, users.updated_at AS users_updated_at, users.created_at AS users_created_at, users.id AS users_id, users.org_id AS users_org_id, users.name AS users_name, users.email AS users_email, users.password_hash AS users_password_hash, users.api_key AS users_api_key
    FROM users
    WHERE users.id = '1' AND 1 = users.org_id
2017-06-23 21:31:54.554 UTC [459] redash@redash LOG: statement: SELECT groups.id AS groups_id, groups.org_id AS groups_org_id, groups.type AS groups_type, groups.name AS groups_name, groups.permissions AS groups_permissions, groups.created_at AS groups_created_at
    FROM groups
    WHERE groups.id IN (1, 2, 3)
2017-06-23 21:31:54.555 UTC [459] redash@redash LOG: statement: SELECT notification_destinations.id AS notification_destinations_id, notification_destinations.org_id AS notification_destinations_org_id, notification_destinations.user_id AS notification_destinations_user_id, notification_destinations.name AS notification_destinations_name, notification_destinations.type AS notification_destinations_type, notification_destinations.options AS notification_destinations_options, notification_destinations.created_at AS notification_destinations_created_at
    FROM notification_destinations
    WHERE notification_destinations.id = '1' AND 1 = notification_destinations.org_id
2017-06-23 21:31:54.557 UTC [459] redash@redash LOG: statement: COMMIT
```

In other words, it does not appear to be even attempting to update the addresses. 


Discussion:

Resolving patch:
diff --git a/redash/handlers/destinations.py b/redash/handlers/destinations.py
--- a/redash/handlers/destinations.py
+++ b/redash/handlers/destinations.py
@@ -2,10 +2,11 @@
 from flask_restful import abort
 
 from redash import models
+from redash.destinations import (destinations,
+                                 get_configuration_schema_for_destination_type)
+from redash.handlers.base import BaseResource
 from redash.permissions import require_admin
-from redash.destinations import destinations, get_configuration_schema_for_destination_type
 from redash.utils.configuration import ConfigurationContainer, ValidationError
-from redash.handlers.base import BaseResource
 
 
 class DestinationTypeListResource(BaseResource):
@@ -30,6 +31,8 @@ def post(self, destination_id):
             abort(400)
 
         try:
+            destination.type = req['type']
+            destination.name = req['name']
             destination.options.set_schema(schema)
             destination.options.update(req['options'])
             models.db.session.add(destination)
@@ -37,9 +40,6 @@ def post(self, destination_id):
         except ValidationError:
             abort(400)
 
-        destination.type = req['type']
-        destination.name = req['name']
-
         return destination.to_dict(all=True)
 
     @require_admin
diff --git a/redash/models.py b/redash/models.py
--- a/redash/models.py
+++ b/redash/models.py
@@ -8,12 +8,20 @@
 import logging
 import time
 
-from funcy import project
-
 import xlsxwriter
 from flask_login import AnonymousUserMixin, UserMixin
 from flask_sqlalchemy import SQLAlchemy
+from funcy import project
 from passlib.apps import custom_app_context as pwd_context
+from sqlalchemy import or_
+from sqlalchemy.dialects import postgresql
+from sqlalchemy.event import listens_for
+from sqlalchemy.ext.mutable import Mutable
+from sqlalchemy.inspection import inspect
+from sqlalchemy.orm import backref, joinedload, object_session, subqueryload
+from sqlalchemy.orm.exc import NoResultFound  # noqa: F401
+from sqlalchemy.types import TypeDecorator
+
 from redash import redis_connection, utils
 from redash.destinations import (get_configuration_schema_for_destination_type,
                                  get_destination)
@@ -23,14 +31,6 @@
                                  get_query_runner)
 from redash.utils import generate_token, json_dumps
 from redash.utils.configuration import ConfigurationContainer
-from sqlalchemy import or_
-from sqlalchemy.dialects import postgresql
-from sqlalchemy.event import listens_for
-from sqlalchemy.ext.mutable import Mutable
-from sqlalchemy.inspection import inspect
-from sqlalchemy.orm import backref, joinedload, object_session, subqueryload
-from sqlalchemy.orm.exc import NoResultFound  # noqa: F401
-from sqlalchemy.types import TypeDecorator
 
 db = SQLAlchemy(session_options={
     'expire_on_commit': False
@@ -1481,8 +1481,9 @@ class NotificationDestination(BelongsToOrgMixin, db.Model):
     user = db.relationship(User, backref="notification_destinations")
     name = Column(db.String(255))
     type = Column(db.String(255))
-    options = Column(Configuration)
+    options = Column(ConfigurationContainer.as_mutable(Configuration))
     created_at = Column(db.DateTime(True), default=db.func.now())
+
     __tablename__ = 'notification_destinations'
     __table_args__ = (db.Index('notification_destinations_org_id_name', 'org_id',
                                'name', unique=True),)


Test patch:
diff --git a/tests/handlers/test_destinations.py b/tests/handlers/test_destinations.py
--- a/tests/handlers/test_destinations.py
+++ b/tests/handlers/test_destinations.py
@@ -1,4 +1,5 @@
 from tests import BaseTestCase
+
 from redash.models import NotificationDestination
 
 
@@ -55,10 +56,14 @@ def test_post(self):
         data = {
             'name': 'updated',
             'type': d.type,
-            'options': d.options.to_dict()
+            'options': {"url": "https://www.slack.com/updated"}
         }
-        rv = self.make_request('post', '/api/destinations/{}'.format(d.id), user=self.factory.create_admin(), data=data)
-        self.assertEqual(rv.status_code, 200)
-        self.assertEqual(NotificationDestination.query.get(d.id).name, data['name'])
 
+        with self.app.app_context():
+            rv = self.make_request('post', '/api/destinations/{}'.format(d.id), user=self.factory.create_admin(), data=data)
+
+        self.assertEqual(rv.status_code, 200)
 
+        d = NotificationDestination.query.get(d.id)
+        self.assertEqual(d.name, data['name'])
+        self.assertEqual(d.options['url'], data['options']['url'])