Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change tracking #4768

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6d4bc4c
Change tracking
kravets-levko Apr 1, 2020
dcf2a55
Change tracking
kravets-levko Apr 2, 2020
1d440fa
Change tracking: query and dashboard attributes
kravets-levko Apr 3, 2020
49feef7
Change tracking: visualization and widget attributes
kravets-levko Apr 3, 2020
b2adadc
Log changes to nested objects (visualizations, widgets) related to th…
kravets-levko Apr 3, 2020
3b4bc15
Migraton to clean changes table
kravets-levko Apr 6, 2020
15d3af3
Replace inheritance with patching to avoid SQLAlchemy issues
kravets-levko Apr 6, 2020
527b73a
Temp
kravets-levko Apr 7, 2020
69ee572
Update migration: add index, drop unused column, add downgrade script
kravets-levko Apr 13, 2020
1a8b46e
Ensure that all fields are logged when creating a new object
kravets-levko Apr 13, 2020
baac67a
Log changes to API keys; optimize a way we access to a parent object …
kravets-levko Apr 13, 2020
26b6bff
When creating/forking query - log changes to its visualzations
kravets-levko Apr 13, 2020
505e90c
Add org_id column to changes table
kravets-levko Apr 13, 2020
a36fbe4
Add tests for all models that use change tracking
kravets-levko Apr 14, 2020
8e61afe
Add some comments to tests
kravets-levko Apr 14, 2020
587e755
API endpoints to access changes (basic implementation)
kravets-levko Apr 15, 2020
b74beb0
Track changes to Alerts and Alert Destinations
kravets-levko Apr 15, 2020
3592ca5
Log changes to Alerts and Alert Destinations
kravets-levko Apr 15, 2020
3f94434
Add API endpoint for Alert Destination changes; refine API code
kravets-levko Apr 17, 2020
995fa33
Changes API: order of items
kravets-levko Apr 20, 2020
49ff502
Add some tests for API endpoints
kravets-levko Apr 20, 2020
f49826c
Merge branch 'master' into change-tracking
kravets-levko Apr 20, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions migrations/versions/4952e040e9dd_clear_changes_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""clear_changes_table

Revision ID: 4952e040e9dd
Revises: e5c7a4e2df4d
Create Date: 2020-04-06 13:22:15.256635

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '4952e040e9dd'
down_revision = 'e5c7a4e2df4d'
branch_labels = None
depends_on = None


def upgrade():
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
op.execute("DELETE FROM changes")
op.drop_column("changes", "object_version")
op.create_index(
"ix_changes_object_ref",
"changes",
["object_type", "object_id"],
unique=False,
)


def downgrade():
op.add_column("changes", sa.Column("object_version", sa.Integer(), nullable=True, default=0))
op.execute("UPDATE changes SET object_version = 0")
op.alter_column("changes", "object_version", nullable=False)
op.drop_index("ix_changes_object_ref")
19 changes: 12 additions & 7 deletions redash/handlers/dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ def post(self):
)
models.db.session.add(dashboard)
models.db.session.commit()

dashboard.record_changes(self.current_user, models.Change.Type.Created)
models.db.session.commit()
arikfr marked this conversation as resolved.
Show resolved Hide resolved

return serialize_dashboard(dashboard)


Expand Down Expand Up @@ -210,9 +214,9 @@ def post(self, dashboard_slug):

updates["changed_by"] = self.current_user

self.update_model(dashboard, updates)
models.db.session.add(dashboard)
try:
self.update_model(dashboard, updates)
dashboard.record_changes(self.current_user)
models.db.session.commit()
except StaleDataError:
abort(409)
Expand Down Expand Up @@ -240,16 +244,14 @@ def delete(self, dashboard_slug):
dashboard_slug, self.current_org
)
dashboard.is_archived = True
dashboard.record_changes(changed_by=self.current_user)
models.db.session.add(dashboard)
d = serialize_dashboard(dashboard, with_widgets=True, user=self.current_user)
dashboard.record_changes(self.current_user)
models.db.session.commit()

self.record_event(
{"action": "archive", "object_id": dashboard.id, "object_type": "dashboard"}
)

return d
return serialize_dashboard(dashboard, with_widgets=True, user=self.current_user)


class PublicDashboardResource(BaseResource):
Expand Down Expand Up @@ -283,7 +285,9 @@ def post(self, dashboard_id):
dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org)
require_admin_or_owner(dashboard.user_id)
api_key = models.ApiKey.create_for_object(dashboard, self.current_user)
models.db.session.flush()
models.db.session.commit()

api_key.record_changes(self.current_user, models.Change.Type.Created)
models.db.session.commit()

public_url = url_for(
Expand Down Expand Up @@ -316,6 +320,7 @@ def delete(self, dashboard_id):
if api_key:
api_key.active = False
models.db.session.add(api_key)
api_key.record_changes(self.current_user)
models.db.session.commit()

self.record_event(
Expand Down
15 changes: 14 additions & 1 deletion redash/handlers/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ def post(self):
models.db.session.add(query)
models.db.session.commit()

query.record_changes(self.current_user, models.Change.Type.Created)
for vis in query.visualizations:
vis.record_changes(self.current_user, models.Change.Type.Created)
models.db.session.commit()

self.record_event(
{"action": "create", "object_id": query.id, "object_type": "query"}
)
Expand Down Expand Up @@ -376,6 +381,7 @@ def post(self, query_id):

try:
self.update_model(query, query_def)
query.record_changes(self.current_user)
models.db.session.commit()
except StaleDataError:
abort(409)
Expand Down Expand Up @@ -416,7 +422,8 @@ def delete(self, query_id):
models.Query.get_by_id_and_org, query_id, self.current_org
)
require_admin_or_owner(query.user_id)
query.archive(self.current_user)
query.archive()
query.record_changes(self.current_user)
models.db.session.commit()


Expand All @@ -428,6 +435,7 @@ def post(self, query_id):
)
require_admin_or_owner(query.user_id)
query.regenerate_api_key()
query.record_changes(self.current_user)
models.db.session.commit()

self.record_event(
Expand Down Expand Up @@ -459,6 +467,11 @@ def post(self, query_id):
forked_query = query.fork(self.current_user)
models.db.session.commit()

forked_query.record_changes(self.current_user, models.Change.Type.Created)
for vis in forked_query.visualizations:
vis.record_changes(self.current_user, models.Change.Type.Created)
models.db.session.commit()

self.record_event(
{"action": "fork", "object_id": query_id, "object_type": "query"}
)
Expand Down
10 changes: 8 additions & 2 deletions redash/handlers/visualizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ def post(self):
vis = models.Visualization(**kwargs)
models.db.session.add(vis)
models.db.session.commit()

vis.record_changes(self.current_user, models.Change.Type.Created)
models.db.session.commit()

return serialize_visualization(vis, with_query=False)


Expand All @@ -42,9 +46,10 @@ def post(self, visualization_id):
kwargs.pop("query_id", None)

self.update_model(vis, kwargs)
d = serialize_visualization(vis, with_query=False)
vis.record_changes(self.current_user)
models.db.session.commit()
return d

return serialize_visualization(vis, with_query=False)

@require_permission("edit_query")
def delete(self, visualization_id):
Expand All @@ -59,5 +64,6 @@ def delete(self, visualization_id):
"object_type": "Visualization",
}
)
vis.record_changes(self.current_user, models.Change.Type.Deleted)
models.db.session.delete(vis)
models.db.session.commit()
4 changes: 4 additions & 0 deletions redash/handlers/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ def post(self):
models.db.session.add(widget)
models.db.session.commit()

widget.record_changes(self.current_user, models.Change.Type.Created)
models.db.session.commit()

return serialize_widget(widget)


Expand All @@ -70,6 +72,7 @@ def post(self, widget_id):
widget_properties = request.get_json(force=True)
widget.text = widget_properties["text"]
widget.options = json_dumps(widget_properties["options"])
widget.record_changes(self.current_user)
models.db.session.commit()
return serialize_widget(widget)

Expand All @@ -85,5 +88,6 @@ def delete(self, widget_id):
self.record_event(
{"action": "delete", "object_id": widget_id, "object_type": "widget"}
)
widget.record_changes(self.current_user, models.Change.Type.Deleted)
models.db.session.delete(widget)
models.db.session.commit()
40 changes: 32 additions & 8 deletions redash/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from redash.models.parameterized_query import ParameterizedQuery

from .base import db, gfk_type, Column, GFKBase, SearchBaseQuery
from .changes import ChangeTrackingMixin, Change # noqa
from .changes import Change, track_changes # noqa
from .mixins import BelongsToOrgMixin, TimestampMixin
from .organizations import Organization
from .types import (
Expand Down Expand Up @@ -451,7 +451,20 @@ def should_schedule_next(
"schedule",
"schedule_failures",
)
class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
@track_changes(
attributes=[
"name",
"description",
"query_text",
"is_archived",
"is_draft",
"api_key",
"options",
"schedule",
"tags",
]
)
class Query(TimestampMixin, BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
version = Column(db.Integer, default=1)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
Expand Down Expand Up @@ -500,7 +513,7 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
def __str__(self):
return str(self.id)

def archive(self, user=None):
def archive(self):
db.session.add(self)
self.is_archived = True
self.schedule = None
Expand All @@ -512,8 +525,6 @@ def archive(self, user=None):
for a in self.alerts:
db.session.delete(a)

if user:
self.record_changes(user)

def regenerate_api_key(self):
self.api_key = generate_token(40)
Expand Down Expand Up @@ -1055,7 +1066,8 @@ def generate_slug(ctx):
@generic_repr(
"id", "name", "slug", "user_id", "org_id", "version", "is_archived", "is_draft"
)
class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
@track_changes(attributes=["name", "is_archived", "is_draft", "tags", "dashboard_filters_enabled"])
class Dashboard(TimestampMixin, BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
version = Column(db.Integer)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
Expand Down Expand Up @@ -1161,6 +1173,10 @@ def lowercase_name(cls):


@generic_repr("id", "name", "type", "query_id")
@track_changes(
parent=(Query, "query_id"),
attributes=["type", "name", "description", "options"]
)
class Visualization(TimestampMixin, BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
type = Column(db.String(100))
Expand All @@ -1178,7 +1194,7 @@ def __str__(self):

@classmethod
def get_by_id_and_org(cls, object_id, org):
return super(Visualization, cls).get_by_id_and_org(object_id, org, Query)
return super().get_by_id_and_org(object_id, org, Query)

def copy(self):
return {
Expand All @@ -1190,6 +1206,10 @@ def copy(self):


@generic_repr("id", "visualization_id", "dashboard_id")
@track_changes(
parent=(Dashboard, "dashboard_id"),
attributes=["text", "visualization_id", "options"]
)
class Widget(TimestampMixin, BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
visualization_id = Column(
Expand All @@ -1210,7 +1230,7 @@ def __str__(self):

@classmethod
def get_by_id_and_org(cls, object_id, org):
return super(Widget, cls).get_by_id_and_org(object_id, org, Dashboard)
return super().get_by_id_and_org(object_id, org, Dashboard)


@generic_repr(
Expand Down Expand Up @@ -1275,6 +1295,10 @@ def record(cls, event):


@generic_repr("id", "created_by_id", "org_id", "active")
@track_changes(
parent=("object_type", "object_id"),
attributes=["api_key", "created_by_id", "active"]
)
class ApiKey(TimestampMixin, GFKBase, db.Model):
id = Column(db.Integer, primary_key=True)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
Expand Down
Loading