Repository: getredash/redash

Issue:
Tags count for dashboards is wrong
![image](https://user-images.githubusercontent.com/71468/48824355-b395f400-ed6c-11e8-8fcb-7a04d6f2c2a4.png)



Discussion:
Huh!
@arikfr I'll take a look.

Resolving patch:
diff --git a/redash/models.py b/redash/models.py
--- a/redash/models.py
+++ b/redash/models.py
@@ -31,7 +31,7 @@
 from sqlalchemy.ext.hybrid import hybrid_property
 from sqlalchemy.ext.mutable import Mutable
 from sqlalchemy.inspection import inspect
-from sqlalchemy.orm import backref, contains_eager, joinedload, object_session
+from sqlalchemy.orm import backref, contains_eager, joinedload, object_session, load_only
 from sqlalchemy.orm.exc import NoResultFound  # noqa: F401
 from sqlalchemy.types import TypeDecorator
 from sqlalchemy.orm.attributes import flag_modified
@@ -990,20 +990,23 @@ def favorites(cls, user, base_query=None):
 
     @classmethod
     def all_tags(cls, user, include_drafts=False):
-        where = cls.is_archived == False
-
-        if not include_drafts:
-            where &= cls.is_draft == False
-
-        where &= DataSourceGroup.group_id.in_(user.group_ids)
+        queries = cls.all_queries(
+            group_ids=user.group_ids,
+            user_id=user.id,
+            drafts=include_drafts,
+        )
 
         tag_column = func.unnest(cls.tags).label('tag')
         usage_count = func.count(1).label('usage_count')
 
-        return db.session.query(tag_column, usage_count).join(
-            DataSourceGroup,
-            cls.data_source_id == DataSourceGroup.data_source_id
-        ).filter(where).distinct().group_by(tag_column).order_by(usage_count.desc())  # .limit(limit)
+        query = (
+            db.session
+            .query(tag_column, usage_count)
+            .group_by(tag_column)
+            .filter(Query.id.in_(queries.options(load_only('id'))))
+            .order_by(usage_count.desc())
+        )
+        return query
 
     @classmethod
     def by_user(cls, user):
@@ -1360,7 +1363,7 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
     __tablename__ = 'dashboards'
     __mapper_args__ = {
         "version_id_col": version
-        }
+    }
 
     @classmethod
     def all(cls, org, group_ids, user_id):
@@ -1390,26 +1393,19 @@ def search(cls, org, groups_ids, user_id, search_term):
 
     @classmethod
     def all_tags(cls, org, user):
+        dashboards = cls.all(org, user.group_ids, user.id)
+
         tag_column = func.unnest(cls.tags).label('tag')
         usage_count = func.count(1).label('usage_count')
 
         query = (
-            db.session.query(tag_column, usage_count)
-            .outerjoin(Widget)
-            .outerjoin(Visualization)
-            .outerjoin(Query)
-            .outerjoin(DataSourceGroup, Query.data_source_id == DataSourceGroup.data_source_id)
-            .filter(
-                Dashboard.is_archived == False,
-                (DataSourceGroup.group_id.in_(user.group_ids) |
-                 (Dashboard.user_id == user.id) |
-                 ((Widget.dashboard != None) & (Widget.visualization == None))),
-                Dashboard.org == org)
-            .group_by(tag_column))
-
-        query = query.filter(or_(Dashboard.user_id == user.id, Dashboard.is_draft == False))
-
-        return query.order_by(usage_count.desc())
+            db.session
+            .query(tag_column, usage_count)
+            .group_by(tag_column)
+            .filter(Dashboard.id.in_(dashboards.options(load_only('id'))))
+            .order_by(usage_count.desc())
+        )
+        return query
 
     @classmethod
     def favorites(cls, user, base_query=None):


Test patch:
diff --git a/tests/models/test_dashboards.py b/tests/models/test_dashboards.py
new file mode 100644
--- /dev/null
+++ b/tests/models/test_dashboards.py
@@ -0,0 +1,30 @@
+from tests import BaseTestCase
+from redash.models import db, Dashboard
+
+
+class DashboardTest(BaseTestCase):
+    def create_tagged_dashboard(self, tags):
+        dashboard = self.factory.create_dashboard(tags=tags)
+        ds = self.factory.create_data_source(group=self.factory.default_group)
+        query = self.factory.create_query(data_source=ds)
+        # We need a bunch of visualizations and widgets configured
+        # to trigger wrong counts via the left outer joins.
+        vis1 = self.factory.create_visualization(query_rel=query)
+        vis2 = self.factory.create_visualization(query_rel=query)
+        vis3 = self.factory.create_visualization(query_rel=query)
+        widget1 = self.factory.create_widget(visualization=vis1, dashboard=dashboard)
+        widget2 = self.factory.create_widget(visualization=vis2, dashboard=dashboard)
+        widget3 = self.factory.create_widget(visualization=vis3, dashboard=dashboard)
+        dashboard.layout = '[[{}, {}, {}]]'.format(widget1.id, widget2.id, widget3.id)
+        db.session.commit()
+        return dashboard
+
+    def test_all_tags(self):
+        self.create_tagged_dashboard(tags=['tag1'])
+        self.create_tagged_dashboard(tags=['tag1', 'tag2'])
+        self.create_tagged_dashboard(tags=['tag1', 'tag2', 'tag3'])
+
+        self.assertEqual(
+            list(Dashboard.all_tags(self.factory.org, self.factory.user)),
+            [('tag1', 3), ('tag2', 2), ('tag3', 1)]
+        )
diff --git a/tests/models/test_queries.py b/tests/models/test_queries.py
--- a/tests/models/test_queries.py
+++ b/tests/models/test_queries.py
@@ -15,6 +15,21 @@ def test_changing_query_text_changes_hash(self):
         db.session.flush()
         self.assertNotEquals(old_hash, q.query_hash)
 
+    def create_tagged_query(self, tags):
+        ds = self.factory.create_data_source(group=self.factory.default_group)
+        query = self.factory.create_query(data_source=ds, tags=tags)
+        return query
+
+    def test_all_tags(self):
+        self.create_tagged_query(tags=['tag1'])
+        self.create_tagged_query(tags=['tag1', 'tag2'])
+        self.create_tagged_query(tags=['tag1', 'tag2', 'tag3'])
+
+        self.assertEqual(
+            list(Query.all_tags(self.factory.user)),
+            [('tag1', 3), ('tag2', 2), ('tag3', 1)]
+        )
+
     def test_search_finds_in_name(self):
         q1 = self.factory.create_query(name=u"Testing seåřċħ")
         q2 = self.factory.create_query(name=u"Testing seåřċħing")