]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4393 Manage favourite issue filters
authorJulien Lancelot <julien.lancelot@gmail.com>
Tue, 18 Jun 2013 14:22:09 +0000 (16:22 +0200)
committerJulien Lancelot <julien.lancelot@gmail.com>
Tue, 18 Jun 2013 14:22:21 +0000 (16:22 +0200)
24 files changed:
sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterDao.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterDto.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterFavouriteDao.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterFavouriteDto.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterFavouriteMapper.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterMapper.java
sonar-core/src/main/java/org/sonar/core/persistence/DaoUtils.java
sonar-core/src/main/java/org/sonar/core/persistence/MyBatis.java
sonar-core/src/main/resources/org/sonar/core/issue/db/IssueFilterFavouriteMapper.xml [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/core/issue/db/IssueFilterMapper.xml
sonar-core/src/test/java/org/sonar/core/issue/db/IssueFilterDaoTest.java
sonar-core/src/test/java/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest.java [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterDaoTest/should_select_by_user_with_only_favorite_filters.xml [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest/shared.xml [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest/should_delete-result.xml [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest/should_insert-result.xml [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
sonar-server/src/main/java/org/sonar/server/issue/IssueFilterService.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/issues_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/helpers/issues_helper.rb
sonar-server/src/main/webapp/WEB-INF/app/views/issues/_favourites.html.erb
sonar-server/src/main/webapp/WEB-INF/app/views/issues/manage.html.erb
sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
sonar-server/src/test/java/org/sonar/server/issue/IssueFilterServiceTest.java

index 679b02e7915ddc1991517ce3e2f804b2fac6b9b1..d74fafd02aed132c6d4267cd75fde4f19cc88ca1 100644 (file)
@@ -31,7 +31,7 @@ import javax.annotation.Nullable;
 import java.util.List;
 
 /**
- * @since 3.6
+ * @since 3.7
  */
 public class IssueFilterDao implements BatchComponent, ServerComponent {
 
@@ -56,7 +56,6 @@ public class IssueFilterDao implements BatchComponent, ServerComponent {
   public IssueFilterDto selectByNameAndUser(String name, String user, @Nullable Long existingId) {
     SqlSession session = mybatis.openSession();
     try {
-      session.getMapper(IssueFilterMapper.class);
       return getMapper(session).selectByNameAndUser(name, user, existingId);
     } finally {
       MyBatis.closeQuietly(session);
@@ -66,13 +65,21 @@ public class IssueFilterDao implements BatchComponent, ServerComponent {
   public List<IssueFilterDto> selectByUser(String user) {
     SqlSession session = mybatis.openSession();
     try {
-      session.getMapper(IssueFilterMapper.class);
       return getMapper(session).selectByUser(user);
     } finally {
       MyBatis.closeQuietly(session);
     }
   }
 
+  public List<IssueFilterDto> selectByUserWithOnlyFavoriteFilters(String user) {
+    SqlSession session = mybatis.openSession();
+    try {
+      return getMapper(session).selectByUserWithOnlyFavoriteFilters(user);
+    } finally {
+      MyBatis.closeQuietly(session);
+    }
+  }
+
   public void insert(IssueFilterDto filter) {
     SqlSession session = mybatis.openSession();
     try {
index 9fe5c1e6291f789f1155e08fa66e4fd863478cd8..ac366e66a1f0d510c08fa5964501a0efc79dddd6 100644 (file)
@@ -26,7 +26,7 @@ import javax.annotation.Nullable;
 import java.util.Date;
 
 /**
- * @since 3.6
+ * @since 3.7
  */
 public class IssueFilterDto {
 
@@ -61,7 +61,7 @@ public class IssueFilterDto {
     return userLogin;
   }
 
-  public IssueFilterDto setUserLogin(@Nullable String userLogin) {
+  public IssueFilterDto setUserLogin(String userLogin) {
     this.userLogin = userLogin;
     return this;
   }
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterFavouriteDao.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterFavouriteDao.java
new file mode 100644 (file)
index 0000000..2f692c1
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.core.issue.db;
+
+import org.apache.ibatis.session.SqlSession;
+import org.sonar.api.BatchComponent;
+import org.sonar.api.ServerComponent;
+import org.sonar.core.persistence.MyBatis;
+
+/**
+ * @since 3.7
+ */
+public class IssueFilterFavouriteDao implements BatchComponent, ServerComponent {
+
+  private final MyBatis mybatis;
+
+  public IssueFilterFavouriteDao(MyBatis mybatis) {
+    this.mybatis = mybatis;
+  }
+
+  public IssueFilterFavouriteDto selectById(Long id) {
+    SqlSession session = mybatis.openSession();
+    try {
+      return getMapper(session).selectById(id);
+    } finally {
+      MyBatis.closeQuietly(session);
+    }
+  }
+
+  public IssueFilterFavouriteDto selectByUserAndIssueFilterId(String user, Long issueFilterId) {
+    SqlSession session = mybatis.openSession();
+    try {
+      return getMapper(session).selectByIssueFilterId(user, issueFilterId);
+    } finally {
+      MyBatis.closeQuietly(session);
+    }
+  }
+
+  public void insert(IssueFilterFavouriteDto filter) {
+    SqlSession session = mybatis.openSession();
+    try {
+      getMapper(session).insert(filter);
+      session.commit();
+    } finally {
+      MyBatis.closeQuietly(session);
+    }
+  }
+
+  public void delete(Long id) {
+    SqlSession session = mybatis.openSession();
+    try {
+      getMapper(session).delete(id);
+      session.commit();
+    } finally {
+      MyBatis.closeQuietly(session);
+    }
+  }
+
+  private IssueFilterFavouriteMapper getMapper(SqlSession session) {
+    return session.getMapper(IssueFilterFavouriteMapper.class);
+  }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterFavouriteDto.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterFavouriteDto.java
new file mode 100644 (file)
index 0000000..b59dcbf
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.issue.db;
+
+import java.util.Date;
+
+/**
+ * @since 3.7
+ */
+public class IssueFilterFavouriteDto {
+
+  private Long id;
+  private String userLogin;
+  private Long issueFilterId;
+  private Date createdAt;
+
+  public Long getId() {
+    return id;
+  }
+
+  public IssueFilterFavouriteDto setId(Long id) {
+    this.id = id;
+    return this;
+  }
+
+  public String getUserLogin() {
+    return userLogin;
+  }
+
+  public IssueFilterFavouriteDto setUserLogin(String userLogin) {
+    this.userLogin = userLogin;
+    return this;
+  }
+
+  public Long getIssueFilterId() {
+    return issueFilterId;
+  }
+
+  public IssueFilterFavouriteDto setIssueFilterId(Long issueFilterId) {
+    this.issueFilterId = issueFilterId;
+    return this;
+  }
+
+  public Date getCreatedAt() {
+    return createdAt;
+  }
+
+  public IssueFilterFavouriteDto setCreatedAt(Date createdAt) {
+    this.createdAt = createdAt;
+    return this;
+  }
+
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterFavouriteMapper.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueFilterFavouriteMapper.java
new file mode 100644 (file)
index 0000000..03986f8
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.core.issue.db;
+
+import org.apache.ibatis.annotations.Param;
+
+import javax.annotation.CheckForNull;
+
+/**
+ * @since 3.7
+ */
+public interface IssueFilterFavouriteMapper {
+
+  @CheckForNull
+  IssueFilterFavouriteDto selectById(Long id);
+
+  @CheckForNull
+  IssueFilterFavouriteDto selectByIssueFilterId(@Param("userLogin") String userLogin, @Param("issueFilterId") Long issueFilterId);
+
+  void insert(IssueFilterFavouriteDto filterFavourite);
+
+  void delete(Long id);
+}
index 86f10e01c7b2673727fdf418a95573231263649a..f07316f07a14b1af6e4aaa059473f5df52014044 100644 (file)
@@ -27,7 +27,7 @@ import javax.annotation.Nullable;
 import java.util.List;
 
 /**
- * @since 3.6
+ * @since 3.7
  */
 public interface IssueFilterMapper {
 
@@ -39,6 +39,8 @@ public interface IssueFilterMapper {
 
   List<IssueFilterDto> selectByUser(String user);
 
+  List<IssueFilterDto> selectByUserWithOnlyFavoriteFilters(String user);
+
   void insert(IssueFilterDto filter);
 
   void update(IssueFilterDto filter);
index db936c44976219597c52565cc4b8c554285bd2f4..6cbb0c6e7369359720ca338419bba9a53436c31d 100644 (file)
@@ -60,6 +60,7 @@ public final class DaoUtils {
       IssueStatsDao.class,
       IssueChangeDao.class,
       IssueFilterDao.class,
+      IssueFilterFavouriteDao.class,
       LoadedTemplateDao.class,
       MeasureFilterDao.class,
       PropertiesDao.class,
index 90b83920742cfb667cd07b726a5d778d08698466..59959a445028a7865b1b5fe6817b3366f7f708b1 100644 (file)
@@ -116,6 +116,7 @@ public class MyBatis implements BatchComponent, ServerComponent {
     loadAlias(conf, "Issue", IssueDto.class);
     loadAlias(conf, "IssueChange", IssueChangeDto.class);
     loadAlias(conf, "IssueFilter", IssueFilterDto.class);
+    loadAlias(conf, "IssueFilterFavourite", IssueFilterFavouriteDto.class);
     loadAlias(conf, "SnapshotData", SnapshotDataDto.class);
     loadAlias(conf, "ActionPlanIssue", ActionPlanDto.class);
     loadAlias(conf, "ActionPlanStats", ActionPlanStatsDto.class);
@@ -127,7 +128,7 @@ public class MyBatis implements BatchComponent, ServerComponent {
 
     Class<?>[] mappers = {ActiveDashboardMapper.class, AuthorMapper.class, DashboardMapper.class,
       DependencyMapper.class, DuplicationMapper.class, GraphDtoMapper.class,
-      IssueMapper.class, IssueStatsMapper.class, IssueChangeMapper.class, IssueFilterMapper.class,
+      IssueMapper.class, IssueStatsMapper.class, IssueChangeMapper.class, IssueFilterMapper.class, IssueFilterFavouriteMapper.class,
       LoadedTemplateMapper.class, MeasureFilterMapper.class, PropertiesMapper.class, PurgeMapper.class, ResourceKeyUpdaterMapper.class, ResourceIndexerMapper.class,
       ResourceSnapshotMapper.class, RoleMapper.class, RuleMapper.class, SchemaMigrationMapper.class,
       SemaphoreMapper.class, UserMapper.class, WidgetMapper.class, WidgetPropertyMapper.class, MeasureMapper.class, SnapshotDataMapper.class,
diff --git a/sonar-core/src/main/resources/org/sonar/core/issue/db/IssueFilterFavouriteMapper.xml b/sonar-core/src/main/resources/org/sonar/core/issue/db/IssueFilterFavouriteMapper.xml
new file mode 100644 (file)
index 0000000..3c8d2db
--- /dev/null
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="org.sonar.core.issue.db.IssueFilterFavouriteMapper">
+
+  <sql id="issueFilterFavouriteColumns">
+    filter_favourites.id as id,
+    filter_favourites.user_login as userLogin,
+    filter_favourites.issue_filter_id as issueFilterId,
+    filter_favourites.created_at as createdAt
+  </sql>
+
+  <select id="selectById" parameterType="long" resultType="issueFilterFavourite">
+    select <include refid="issueFilterFavouriteColumns"/>
+    from issue_filter_favourites filter_favourites
+    <where>
+      filter_favourites.id=#{id}
+    </where>
+  </select>
+
+  <select id="selectByIssueFilterId" parameterType="long" resultType="issueFilterFavourite">
+    select <include refid="issueFilterFavouriteColumns"/>
+    from issue_filter_favourites filter_favourites
+    <where>
+      filter_favourites.issue_filter_id=#{issueFilterId}
+      and filter_favourites.user_login=#{userLogin}
+    </where>
+  </select>
+
+  <insert id="insert" parameterType="issueFilterFavourite" useGeneratedKeys="true" keyProperty="id">
+    INSERT INTO issue_filter_favourites (user_login, issue_filter_id, created_at)
+    VALUES (#{userLogin}, #{issueFilterId}, current_timestamp)
+  </insert>
+
+  <!-- Oracle -->
+  <insert id="insert" databaseId="oracle" parameterType="issueFilterFavourite" keyColumn="id" useGeneratedKeys="true" keyProperty="id">
+    <selectKey order="BEFORE" resultType="Long" keyProperty="id">
+      select issue_filter_favourites_seq.NEXTVAL from DUAL
+    </selectKey>
+    INSERT INTO issue_filter_favourites (id, user_login, issue_filter_id, created_at)
+    VALUES (#{id},#{userLogin}, #{issueFilterId}, current_timestamp)
+  </insert>
+
+  <delete id="delete" parameterType="int">
+    delete from issue_filter_favourites where id=#{id}
+  </delete>
+
+</mapper>
index 291f90db47f82b847e9e692cd289040218cb2f0e..2d62e89af3ddf4adf52297eecd3309401002f841 100644 (file)
     </where>
   </select>
 
+  <select id="selectByUserWithOnlyFavoriteFilters" parameterType="String" resultType="IssueFilter">
+    select <include refid="issueFilterColumns"/>
+    from issue_filters filters
+    inner join issue_filter_favourites fav on fav.issue_filter_id = filters.id
+    <where>
+      fav.user_login=#{user}
+    </where>
+  </select>
+
   <insert id="insert" parameterType="IssueFilter" useGeneratedKeys="true" keyProperty="id">
     INSERT INTO issue_filters (name, user_login, shared, description, data, created_at, updated_at)
     VALUES (#{name}, #{userLogin}, #{shared}, #{description}, #{data}, #{createdAt}, #{updatedAt})
index 4f6b9001a0d63975b467ca80ab29bb1447a5eb44..514a7b29d7c75e9e12402cf3d5412aa5ae8bd437 100644 (file)
@@ -70,6 +70,17 @@ public class IssueFilterDaoTest extends AbstractDaoTestCase {
     assertThat(results).hasSize(2);
   }
 
+  @Test
+  public void should_select_by_user_with_only_favorite_filters() {
+    setupData("should_select_by_user_with_only_favorite_filters");
+
+    List<IssueFilterDto> results = dao.selectByUserWithOnlyFavoriteFilters("michael");
+
+    assertThat(results).hasSize(1);
+    IssueFilterDto issueFilterDto = results.get(0);
+    assertThat(issueFilterDto.getId()).isEqualTo(2L);
+  }
+
   @Test
   public void should_insert() {
     setupData("shared");
@@ -83,8 +94,6 @@ public class IssueFilterDaoTest extends AbstractDaoTestCase {
 
     dao.insert(filterDto);
 
-    assertThat(filterDto.getId()).isNotNull();
-
     checkTables("should_insert", new String[]{"created_at", "updated_at"}, "issue_filters");
   }
 
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest.java
new file mode 100644 (file)
index 0000000..4cdaa9d
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.core.issue.db;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.core.persistence.AbstractDaoTestCase;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class IssueFilterFavouriteDaoTest extends AbstractDaoTestCase {
+
+  IssueFilterFavouriteDao dao;
+
+  @Before
+  public void createDao() {
+    dao = new IssueFilterFavouriteDao(getMyBatis());
+  }
+
+  @Test
+  public void should_select_by_id() {
+    setupData("shared");
+
+    IssueFilterFavouriteDto issueFilterFavouriteDto = dao.selectById(1L);
+    assertThat(issueFilterFavouriteDto.getId()).isEqualTo(1L);
+    assertThat(issueFilterFavouriteDto.getUserLogin()).isEqualTo("stephane");
+    assertThat(issueFilterFavouriteDto.getIssueFilterId()).isEqualTo(10L);
+    assertThat(issueFilterFavouriteDto.getCreatedAt()).isNotNull();
+
+    assertThat(dao.selectById(999L)).isNull();
+  }
+
+  @Test
+  public void should_select_by_issue_filter_id() {
+    setupData("shared");
+
+    IssueFilterFavouriteDto issueFilterFavouriteDto = dao.selectByUserAndIssueFilterId("stephane", 10L);
+    assertThat(issueFilterFavouriteDto.getId()).isEqualTo(1L);
+    assertThat(issueFilterFavouriteDto.getUserLogin()).isEqualTo("stephane");
+    assertThat(issueFilterFavouriteDto.getIssueFilterId()).isEqualTo(10L);
+    assertThat(issueFilterFavouriteDto.getCreatedAt()).isNotNull();
+
+    assertThat(dao.selectByUserAndIssueFilterId("stephane", 999L)).isNull();
+  }
+
+  @Test
+  public void should_insert() {
+    setupData("shared");
+
+    IssueFilterFavouriteDto issueFilterFavouriteDto = new IssueFilterFavouriteDto();
+    issueFilterFavouriteDto.setUserLogin("arthur");
+    issueFilterFavouriteDto.setIssueFilterId(11L);
+
+    dao.insert(issueFilterFavouriteDto);
+
+    checkTables("should_insert", new String[]{"created_at"}, "issue_filter_favourites");
+  }
+
+  @Test
+  public void should_delete() {
+    setupData("shared");
+
+    dao.delete(3l);
+
+    checkTables("should_delete", new String[]{"created_at"}, "issue_filter_favourites");
+  }
+
+}
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterDaoTest/should_select_by_user_with_only_favorite_filters.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterDaoTest/should_select_by_user_with_only_favorite_filters.xml
new file mode 100644 (file)
index 0000000..f378fb9
--- /dev/null
@@ -0,0 +1,39 @@
+<dataset>
+
+  <issue_filters
+      id="1"
+      name="Sonar Issues"
+      user_login="stephane"
+      shared="[true]"
+      description="All issues of Sonar"
+      data="componentRoots=org.codehaus.sonar"
+      created_at="2013-06-10"
+      updated_at="2013-06-10" />
+
+  <issue_filters
+      id="2"
+      name="Open issues"
+      user_login="michael"
+      shared="[false]"
+      description="All open issues"
+      data="statuses=OPEN"
+      created_at="2013-06-10"
+      updated_at="2013-06-10" />
+
+  <issue_filters
+      id="3"
+      name="Sonar Open issues"
+      user_login="michael"
+      shared="[true]"
+      description="All open issues on Sonar"
+      data="statuses=OPEN|componentRoots=org.codehaus.sonar"
+      created_at="2013-06-10"
+      updated_at="2013-06-10" />
+
+  <issue_filter_favourites
+      id="10"
+      user_login="michael"
+      issue_filter_id="2"
+      created_at="2013-06-10"/>
+
+</dataset>
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest/shared.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest/shared.xml
new file mode 100644 (file)
index 0000000..e963d10
--- /dev/null
@@ -0,0 +1,21 @@
+<dataset>
+
+  <issue_filter_favourites
+      id="1"
+      user_login="stephane"
+      issue_filter_id="10"
+      created_at="2013-06-10"/>
+
+  <issue_filter_favourites
+      id="2"
+      user_login="stephane"
+      issue_filter_id="11"
+      created_at="2013-06-10"/>
+
+  <issue_filter_favourites
+      id="3"
+      user_login="arthur"
+      issue_filter_id="10"
+      created_at="2013-06-10"/>
+
+</dataset>
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest/should_delete-result.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest/should_delete-result.xml
new file mode 100644 (file)
index 0000000..03670a1
--- /dev/null
@@ -0,0 +1,15 @@
+<dataset>
+
+  <issue_filter_favourites
+      id="1"
+      user_login="stephane"
+      issue_filter_id="10"
+      created_at="2013-06-10"/>
+
+  <issue_filter_favourites
+      id="2"
+      user_login="stephane"
+      issue_filter_id="11"
+      created_at="2013-06-10"/>
+
+</dataset>
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest/should_insert-result.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueFilterFavouriteDaoTest/should_insert-result.xml
new file mode 100644 (file)
index 0000000..7fa5aff
--- /dev/null
@@ -0,0 +1,27 @@
+<dataset>
+
+  <issue_filter_favourites
+      id="1"
+      user_login="stephane"
+      issue_filter_id="10"
+      created_at="2013-06-10"/>
+
+  <issue_filter_favourites
+      id="2"
+      user_login="stephane"
+      issue_filter_id="11"
+      created_at="2013-06-10"/>
+
+  <issue_filter_favourites
+      id="3"
+      user_login="arthur"
+      issue_filter_id="10"
+      created_at="2013-06-10"/>
+
+  <issue_filter_favourites
+      id="4"
+      user_login="arthur"
+      issue_filter_id="11"
+      created_at="2013-06-18"/>
+
+</dataset>
index 8cdbd1166e8bb472dbbdacc03dcba2a48b2c64c7..e44e0bffd6cad16017f8d2c86511e98729b93564 100644 (file)
@@ -397,7 +397,7 @@ public class InternalRubyIssueService implements ServerComponent {
   /**
    * List user issue filter
    */
-  public List<DefaultIssueFilter> findIssueFiltersForUser() {
+  public List<DefaultIssueFilter> findIssueFiltersForCurrentUser() {
     return issueFilterService.findByUser(UserSession.get());
   }
 
@@ -450,10 +450,10 @@ public class InternalRubyIssueService implements ServerComponent {
   public Result<DefaultIssueFilter> deleteIssueFilter(Long issueFilterId) {
     Result<DefaultIssueFilter> result = Result.of();
     try {
-        issueFilterService.delete(issueFilterId, UserSession.get());
-      } catch (Exception e) {
-        result.addError(e.getMessage());
-      }
+      issueFilterService.delete(issueFilterId, UserSession.get());
+    } catch (Exception e) {
+      result.addError(e.getMessage());
+    }
     return result;
   }
 
@@ -502,6 +502,20 @@ public class InternalRubyIssueService implements ServerComponent {
     return result;
   }
 
+  public List<DefaultIssueFilter> findFavouriteIssueFiltersForCurrentUser() {
+    return issueFilterService.findFavoriteFilters(UserSession.get());
+  }
+
+  public Result toggleFavouriteIssueFilter(Long issueFilterId) {
+    Result result = Result.of();
+    try {
+      issueFilterService.toggleFavouriteIssueFilter(issueFilterId, UserSession.get());
+    } catch (Exception e) {
+      result.addError(e.getMessage());
+    }
+    return result;
+  }
+
   private void checkMandatoryParameter(String value, String paramName, Result result) {
     if (Strings.isNullOrEmpty(value)) {
       result.addError(Result.Message.ofL10n("errors.cant_be_empty", paramName));
index 02c0ca17ed21c57120c8215cad3ae14347fc9589..8de1a6da8362a7230516b29e969e88a901157e8d 100644 (file)
@@ -29,6 +29,8 @@ import org.sonar.api.issue.IssueQueryResult;
 import org.sonar.core.issue.DefaultIssueFilter;
 import org.sonar.core.issue.db.IssueFilterDao;
 import org.sonar.core.issue.db.IssueFilterDto;
+import org.sonar.core.issue.db.IssueFilterFavouriteDao;
+import org.sonar.core.issue.db.IssueFilterFavouriteDto;
 import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
@@ -41,24 +43,36 @@ import static com.google.common.collect.Lists.newArrayList;
 
 public class IssueFilterService implements ServerComponent {
 
-  private IssueFilterDao issueFilterDao;
+  private final IssueFilterDao issueFilterDao;
+  private final IssueFilterFavouriteDao issueFilterFavouriteDao;
   private final IssueFinder issueFinder;
 
-  public IssueFilterService(IssueFilterDao issueFilterDao, IssueFinder issueFinder) {
+  public IssueFilterService(IssueFilterDao issueFilterDao, IssueFilterFavouriteDao issueFilterFavouriteDao, IssueFinder issueFinder) {
     this.issueFilterDao = issueFilterDao;
+    this.issueFilterFavouriteDao = issueFilterFavouriteDao;
     this.issueFinder = issueFinder;
   }
 
+  public IssueQueryResult execute(IssueQuery issueQuery) {
+    return issueFinder.find(issueQuery);
+  }
+
+  public IssueQueryResult execute(Long issueFilterId, UserSession userSession) {
+    IssueFilterDto issueFilterDto = findIssueFilter(issueFilterId, userSession);
+
+    DefaultIssueFilter issueFilter = issueFilterDto.toIssueFilter();
+    IssueQuery issueQuery = PublicRubyIssueService.toQuery(issueFilter.dataAsMap());
+    return issueFinder.find(issueQuery);
+  }
+
   @CheckForNull
   public DefaultIssueFilter findById(Long id, UserSession userSession) {
-    verifyLoggedIn(userSession);
-    IssueFilterDto issueFilterDto = findIssueFilter(id);
-    verifyCurrentUserIsOwnerOfFilter(issueFilterDto, userSession);
+    IssueFilterDto issueFilterDto = findIssueFilter(id, userSession);
     return issueFilterDto.toIssueFilter();
   }
 
   public List<DefaultIssueFilter> findByUser(UserSession userSession) {
-    if (userSession.isLoggedIn()) {
+    if (userSession.isLoggedIn() && userSession.login() != null) {
       List<IssueFilterDto> issueFilterDtoList = issueFilterDao.selectByUser(userSession.login());
       return newArrayList(Iterables.transform(issueFilterDtoList, new Function<IssueFilterDto, DefaultIssueFilter>() {
         @Override
@@ -77,13 +91,12 @@ public class IssueFilterService implements ServerComponent {
 
     IssueFilterDto issueFilterDto = IssueFilterDto.toIssueFilter(issueFilter);
     issueFilterDao.insert(issueFilterDto);
+    addFavouriteIssueFilter(issueFilterDto.getId(), userSession.login());
     return issueFilterDto.toIssueFilter();
   }
 
   public DefaultIssueFilter update(DefaultIssueFilter issueFilter, UserSession userSession) {
-    verifyLoggedIn(userSession);
-    IssueFilterDto issueFilterDto = findIssueFilter(issueFilter.id());
-    verifyCurrentUserIsOwnerOfFilter(issueFilterDto, userSession);
+    findIssueFilter(issueFilter.id(), userSession);
     verifyNameIsNotAlreadyUsed(issueFilter, userSession);
 
     issueFilterDao.update(IssueFilterDto.toIssueFilter(issueFilter));
@@ -91,9 +104,7 @@ public class IssueFilterService implements ServerComponent {
   }
 
   public DefaultIssueFilter updateData(Long issueFilterId, Map<String, Object> mapData, UserSession userSession) {
-    verifyLoggedIn(userSession);
-    IssueFilterDto issueFilterDto = findIssueFilter(issueFilterId);
-    verifyCurrentUserIsOwnerOfFilter(issueFilterDto, userSession);
+    IssueFilterDto issueFilterDto = findIssueFilter(issueFilterId, userSession);
     DefaultIssueFilter issueFilter = issueFilterDto.toIssueFilter();
     issueFilter.setData(mapData);
     issueFilterDao.update(IssueFilterDto.toIssueFilter(issueFilter));
@@ -101,15 +112,16 @@ public class IssueFilterService implements ServerComponent {
   }
 
   public void delete(Long issueFilterId, UserSession userSession) {
-    verifyLoggedIn(userSession);
-    IssueFilterDto issueFilterDto = findIssueFilter(issueFilterId);
-    verifyCurrentUserIsOwnerOfFilter(issueFilterDto, userSession);
+    findIssueFilter(issueFilterId, userSession);
+    IssueFilterFavouriteDto issueFilterFavouriteDto = findFavouriteIssueFilter(userSession.login(), issueFilterId);
+    if (issueFilterFavouriteDto != null) {
+      deleteFavouriteIssueFilter(issueFilterFavouriteDto.getId());
+    }
     issueFilterDao.delete(issueFilterId);
   }
 
   public DefaultIssueFilter copy(Long issueFilterIdToCopy, DefaultIssueFilter issueFilter, UserSession userSession) {
-    verifyLoggedIn(userSession);
-    IssueFilterDto issueFilterDtoToCopy = findIssueFilter(issueFilterIdToCopy);
+    IssueFilterDto issueFilterDtoToCopy = findIssueFilter(issueFilterIdToCopy, userSession);
     issueFilter.setUser(userSession.login());
     issueFilter.setData(issueFilterDtoToCopy.getData());
     verifyNameIsNotAlreadyUsed(issueFilter, userSession);
@@ -119,25 +131,37 @@ public class IssueFilterService implements ServerComponent {
     return issueFilterDto.toIssueFilter();
   }
 
-  public IssueQueryResult execute(IssueQuery issueQuery) {
-    return issueFinder.find(issueQuery);
+  public List<DefaultIssueFilter> findFavoriteFilters(UserSession userSession) {
+    if (userSession.isLoggedIn() && userSession.login() != null) {
+      List<IssueFilterDto> issueFilterDtoList = issueFilterDao.selectByUserWithOnlyFavoriteFilters(userSession.login());
+      return newArrayList(Iterables.transform(issueFilterDtoList, new Function<IssueFilterDto, DefaultIssueFilter>() {
+        @Override
+        public DefaultIssueFilter apply(IssueFilterDto issueFilterDto) {
+          return issueFilterDto.toIssueFilter();
+        }
+      }));
+    }
+    return Collections.emptyList();
   }
 
-  public IssueQueryResult execute(Long issueFilterId, UserSession userSession) {
-    IssueFilterDto issueFilterDto = findIssueFilter(issueFilterId);
-    verifyCurrentUserIsOwnerOfFilter(issueFilterDto, userSession);
-
-    DefaultIssueFilter issueFilter = issueFilterDto.toIssueFilter();
-    IssueQuery issueQuery = PublicRubyIssueService.toQuery(issueFilter.dataAsMap());
-    return issueFinder.find(issueQuery);
+  public void toggleFavouriteIssueFilter(Long issueFilterId, UserSession userSession) {
+    findIssueFilter(issueFilterId, userSession);
+    IssueFilterFavouriteDto issueFilterFavouriteDto = findFavouriteIssueFilter(userSession.login(), issueFilterId);
+    if (issueFilterFavouriteDto == null) {
+      addFavouriteIssueFilter(issueFilterId, userSession.login());
+    } else {
+      deleteFavouriteIssueFilter(issueFilterFavouriteDto.getId());
+    }
   }
 
-  public IssueFilterDto findIssueFilter(Long id){
+  public IssueFilterDto findIssueFilter(Long id, UserSession userSession){
+    verifyLoggedIn(userSession);
     IssueFilterDto issueFilterDto = issueFilterDao.selectById(id);
     if (issueFilterDto == null) {
       // TODO throw 404
       throw new IllegalArgumentException("Filter not found: " + id);
     }
+    verifyCurrentUserIsOwnerOfFilter(issueFilterDto, userSession);
     return issueFilterDto;
   }
 
@@ -160,4 +184,19 @@ public class IssueFilterService implements ServerComponent {
     }
   }
 
+  private IssueFilterFavouriteDto findFavouriteIssueFilter(String user, Long issueFilterId) {
+    return issueFilterFavouriteDao.selectByUserAndIssueFilterId(user, issueFilterId);
+  }
+
+  private void addFavouriteIssueFilter(Long issueFilterId, String user) {
+    IssueFilterFavouriteDto issueFilterFavouriteDto = new IssueFilterFavouriteDto()
+      .setIssueFilterId(issueFilterId)
+      .setUserLogin(user);
+    issueFilterFavouriteDao.insert(issueFilterFavouriteDto);
+  }
+
+  private void deleteFavouriteIssueFilter(Long issueFilterFavoriteId) {
+    issueFilterFavouriteDao.delete(issueFilterFavoriteId);
+  }
+
 }
index 55ed4e34a82621272f2bdbb2b0c70558a31941a0..d3d4500aed8aa49b08f89e4c72e5e59ed86d0ff6 100644 (file)
 # along with this program; if not, write to the Free Software Foundation,
 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 #
-
 class IssuesController < ApplicationController
 
   before_filter :init_options
-  before_filter :load_user_filters, :only => [:index, :search, :filter, :manage]
+  before_filter :load_filters, :only => [:index, :search, :filter, :manage, :toggle_fav]
 
   # GET /issues/index
   def index
@@ -35,8 +34,7 @@ class IssuesController < ApplicationController
     else
       @filter = Internal.issues.createFilterFromMap(criteria_params)
     end
-
-    @criteria_params = criteria_params
+    @criteria_params = Hash[@filter.dataAsMap]
     @issue_query = Internal.issues.toQuery(criteria_params)
     @issues_result = Internal.issues.execute(@issue_query)
   end
@@ -46,11 +44,8 @@ class IssuesController < ApplicationController
   def filter
     require_parameters :id
 
-    # criteria can be overridden
-    # TODO ?
-    #@filter.override_criteria(criteria_params)
-
     @filter = find_filter(params[:id].to_i)
+    @criteria_params = Hash[@filter.dataAsMap]
     @issue_query = Internal.issues.toQuery(@filter.dataAsMap)
     @issues_result = Internal.issues.execute(@issue_query)
     @unchanged = true
@@ -61,6 +56,8 @@ class IssuesController < ApplicationController
   # GET /issues/manage
   def manage
     @issue_query = Internal.issues.toQuery({})
+    @filters = Internal.issues.findIssueFiltersForCurrentUser()
+    @favourite_filter_ids = @favourite_filters.map { |filter| filter.id }
   end
 
   # GET /issues/save_as_form?[&criteria]
@@ -159,6 +156,19 @@ class IssuesController < ApplicationController
     redirect_to :action => 'manage'
   end
 
+  # POST /issues/toggle_fav/<filter id>
+  def toggle_fav
+    verify_ajax_request
+    require_parameters :id
+    result = Internal.issues.toggleFavouriteIssueFilter(params[:id].to_i)
+    if result.ok
+      render :text => '', :status => 200
+    else
+      @errors = result.errors
+      render :action => 'manage'
+    end
+  end
+
 
   private
 
@@ -167,8 +177,8 @@ class IssuesController < ApplicationController
     @options_for_resolutions = Internal.issues.listResolutions().map {|s| [message('issue.resolution.' + s), s]}
   end
 
-  def load_user_filters
-    @my_filters = Internal.issues.findIssueFiltersForUser()
+  def load_filters
+    @favourite_filters = Internal.issues.findFavouriteIssueFiltersForCurrentUser()
   end
 
   def find_filter(id)
index 5e70609414c2003eb6c4fca2b38045ec60521a8c..62abbc88488799090b4b3cf67f10c8c93f6ef269 100644 (file)
@@ -32,4 +32,16 @@ module IssuesHelper
     html
   end
 
+  def issue_filter_star(filter, is_favourite)
+    if is_favourite
+      style='fav'
+      title=message('click_to_remove_from_favourites')
+    else
+      style='notfav'
+      title=message('click_to_add_to_favourites')
+    end
+
+    "<a href='#' class='issue-filter-star #{style}' filter-id='#{filter.id.to_s}' title='#{title}'></a>"
+  end
+
 end
index 0a288c80ae2a0be62b74b49a3e1675d33b786f2e..397aecff874b9c5f4cb2de72ff442ca4d1d4137c 100644 (file)
@@ -1,6 +1,12 @@
 <div id="sidebar-favourites">
 <% if logged_in? %>
   <li class="sidebar-title"><%= message('issue_filter.favourite_filters') -%></li>
+  <% @favourite_filters.each do |filter| %>
+    <li <%= "class='active'" if @filter && filter.id==@filter.id -%>>
+      <a href="<%= ApplicationController.root_context -%>/issues/filter/<%= filter.id -%>"><%= h filter.name -%></a>
+    </li>
+  <% end %>
+
   <li><a href="<%= ApplicationController.root_context -%>/issues/manage" class="link-action"><%= message('manage') %></a></li>
   <li class="spacer"></li>
 <% end %>
index 0f95f2ae53a5fde6f3f3ea23926f0682b1c068a2..9c2baa799bce792444c9578491b3fb2b70572363 100644 (file)
@@ -1,25 +1,47 @@
+<% content_for :script do %>
+  <script>
+    $j(document).ready(function () {
+      $j(".issue-filter-star").click(function () {
+        var filterId = $j(this).attr('filter-id');
+        $j.ajax({
+          type: 'POST',
+          url: baseUrl + "/issues/toggle_fav",
+          data: {id: filterId},
+          success: function (data) {
+            window.location = baseUrl + '/issues/manage';
+          }
+        });
+      });
+    });
+  </script>
+<% end %>
 <div>
   <div class="page-split-left">
     <%= render :partial => 'issues/sidebar' -%>
   </div>
   <div class="page-split-right">
     <div id="content">
+      <%= render :partial => 'display_errors' %>
       <h1><%= message 'issue_filter.manage.my_filters' -%></h1>
       <table class="data marginbottom10" id="my-issue-filters">
         <thead>
         <tr>
+          <th class="thin"></th>
           <th><%= message('name') -%></th>
           <th class="right"><%= message('operations') -%></th>
         </tr>
         </thead>
         <tbody>
-        <% if @my_filters.empty? %>
+        <% if @filters.empty? %>
           <tr class="even">
             <td colspan="4"><%= message('issue_filter.no_filters') -%></td>
           </tr>
         <% else %>
-          <% @my_filters.each do |filter| %>
+          <% @filters.each do |filter| %>
             <tr id="my-issue-filter-<%= filter.name.parameterize -%>" class="<%= cycle('even', 'odd', :name => 'my-filters') -%>">
+              <td>
+                <%= issue_filter_star(filter, @favourite_filter_ids.include?(filter.id)) -%>
+              </td>
               <td>
                 <%= link_to h(filter.name), :action => 'filter', :id => filter.id -%>
                 <% if filter.description %>
index a8113367b0c4bbeade4646937b89c3177779e013..8f82db7347a4cc8bfb8308d29d907745399f7d44 100644 (file)
@@ -403,7 +403,7 @@ public class InternalRubyIssueServiceTest {
 
   @Test
   public void should_find_user_issue_filters() {
-    service.findIssueFiltersForUser();
+    service.findIssueFiltersForCurrentUser();
     verify(issueFilterService).findByUser(any(UserSession.class));
   }
 
@@ -414,6 +414,18 @@ public class InternalRubyIssueServiceTest {
     verify(issueFilterService).updateData(eq(10L), eq(data), any(UserSession.class));
   }
 
+  @Test
+  public void should_find_favourite_issue_filters() {
+    service.findFavouriteIssueFiltersForCurrentUser();
+    verify(issueFilterService).findFavoriteFilters(any(UserSession.class));
+  }
+
+  @Test
+  public void should_toggle_favourite_issue_filter() {
+    service.toggleFavouriteIssueFilter(10L);
+    verify(issueFilterService).toggleFavouriteIssueFilter(eq(10L), any(UserSession.class));
+  }
+
   private String createLongString(int size) {
     String result = "";
     for (int i = 0; i < size; i++) {
index 429322c1fa82c06bf785f580c6be575855b9c669..a1e1aa0e17994e6b06c27231672879c3e12b7382 100644 (file)
@@ -28,6 +28,8 @@ import org.sonar.api.issue.IssueQuery;
 import org.sonar.core.issue.DefaultIssueFilter;
 import org.sonar.core.issue.db.IssueFilterDao;
 import org.sonar.core.issue.db.IssueFilterDto;
+import org.sonar.core.issue.db.IssueFilterFavouriteDao;
+import org.sonar.core.issue.db.IssueFilterFavouriteDto;
 import org.sonar.server.user.UserSession;
 
 import java.util.List;
@@ -45,6 +47,7 @@ public class IssueFilterServiceTest {
   private IssueFilterService service;
 
   private IssueFilterDao issueFilterDao;
+  private IssueFilterFavouriteDao issueFilterFavouriteDao;
   private IssueFinder issueFinder;
 
   private UserSession userSession;
@@ -57,9 +60,10 @@ public class IssueFilterServiceTest {
     when(userSession.login()).thenReturn("john");
 
     issueFilterDao = mock(IssueFilterDao.class);
+    issueFilterFavouriteDao = mock(IssueFilterFavouriteDao.class);
     issueFinder = mock(IssueFinder.class);
 
-    service = new IssueFilterService(issueFilterDao, issueFinder);
+    service = new IssueFilterService(issueFilterDao, issueFilterFavouriteDao, issueFinder);
   }
 
   @Test
@@ -135,6 +139,15 @@ public class IssueFilterServiceTest {
     verify(issueFilterDao).insert(any(IssueFilterDto.class));
   }
 
+  @Test
+  public void should_add_favorite_on_save() {
+    DefaultIssueFilter issueFilter = new DefaultIssueFilter().setName("My Issue");
+    service.save(issueFilter, userSession);
+
+    verify(issueFilterDao).insert(any(IssueFilterDto.class));
+    verify(issueFilterFavouriteDao).insert(any(IssueFilterFavouriteDto.class));
+  }
+
   @Test
   public void should_not_save_if_not_logged() {
     when(userSession.isLoggedIn()).thenReturn(false);
@@ -220,6 +233,17 @@ public class IssueFilterServiceTest {
     verify(issueFilterDao).delete(1L);
   }
 
+  @Test
+  public void should_delete_favorite_filter_on_delete() {
+    when(issueFilterDao.selectById(1L)).thenReturn(new IssueFilterDto().setId(1L).setName("My Issues").setUserLogin("john"));
+    when(issueFilterFavouriteDao.selectByUserAndIssueFilterId("john", 1L)).thenReturn(new IssueFilterFavouriteDto().setId(10L).setUserLogin("john").setIssueFilterId(1L));
+
+    service.delete(1L, userSession);
+
+    verify(issueFilterDao).delete(1L);
+    verify(issueFilterFavouriteDao).delete(10L);
+  }
+
   @Test
   public void should_not_delete_if_filter_not_found() {
     when(issueFilterDao.selectById(1L)).thenReturn(null);
@@ -235,7 +259,7 @@ public class IssueFilterServiceTest {
 
   @Test
   public void should_copy() {
-    when(issueFilterDao.selectById(1L)).thenReturn(new IssueFilterDto().setId(1L).setName("My Issues").setUserLogin("perceval").setData("componentRoots=struts"));
+    when(issueFilterDao.selectById(1L)).thenReturn(new IssueFilterDto().setId(1L).setName("My Issues").setUserLogin("john").setData("componentRoots=struts"));
     DefaultIssueFilter issueFilter = new DefaultIssueFilter().setName("Copy Of My Issue");
 
     DefaultIssueFilter result = service.copy(1L, issueFilter, userSession);
@@ -268,4 +292,57 @@ public class IssueFilterServiceTest {
     assertThat(issueQuery.componentRoots()).contains("struts");
   }
 
+  @Test
+  public void should_find_favourite_issue_filter() {
+    when(issueFilterDao.selectByUserWithOnlyFavoriteFilters("john")).thenReturn(newArrayList(new IssueFilterDto().setId(1L).setName("My Issue").setUserLogin("john")));
+
+    List<DefaultIssueFilter> results = service.findFavoriteFilters(userSession);
+    assertThat(results).hasSize(1);
+  }
+
+  @Test
+  public void should_not_find_favourite_issue_filter_if_not_logged() {
+    when(userSession.isLoggedIn()).thenReturn(false);
+
+    List<DefaultIssueFilter> results = service.findFavoriteFilters(userSession);
+    assertThat(results).isEmpty();
+  }
+
+  @Test
+  public void should_add_favourite_issue_filter_id() {
+    when(issueFilterDao.selectById(1L)).thenReturn(new IssueFilterDto().setId(1L).setName("My Issues").setUserLogin("john").setData("componentRoots=struts"));
+    // The filter is not in the favorite list --> add to favorite
+    when(issueFilterFavouriteDao.selectByUserAndIssueFilterId("john", 1L)).thenReturn(null);
+
+    ArgumentCaptor<IssueFilterFavouriteDto> issueFilterFavouriteDtoCaptor = ArgumentCaptor.forClass(IssueFilterFavouriteDto.class);
+    service.toggleFavouriteIssueFilter(1L, userSession);
+    verify(issueFilterFavouriteDao).insert(issueFilterFavouriteDtoCaptor.capture());
+
+    IssueFilterFavouriteDto issueFilterFavouriteDto = issueFilterFavouriteDtoCaptor.getValue();
+    assertThat(issueFilterFavouriteDto.getIssueFilterId()).isEqualTo(1L);
+    assertThat(issueFilterFavouriteDto.getUserLogin()).isEqualTo("john");
+  }
+
+  @Test
+  public void should_delete_favourite_issue_filter_id() {
+    when(issueFilterDao.selectById(1L)).thenReturn(new IssueFilterDto().setId(1L).setName("My Issues").setUserLogin("john").setData("componentRoots=struts"));
+    // The filter is in the favorite list --> remove favorite
+    when(issueFilterFavouriteDao.selectByUserAndIssueFilterId("john", 1L)).thenReturn(new IssueFilterFavouriteDto().setId(10L).setUserLogin("john").setIssueFilterId(1L));
+
+    service.toggleFavouriteIssueFilter(1L, userSession);
+    verify(issueFilterFavouriteDao).delete(10L);
+  }
+
+  @Test
+  public void should_not_toggle_favourite_filter_if_filter_not_found() {
+    when(issueFilterDao.selectById(1L)).thenReturn(null);
+    try {
+      service.toggleFavouriteIssueFilter(1L, userSession);
+      fail();
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Filter not found: 1");
+    }
+    verify(issueFilterFavouriteDao, never()).delete(anyLong());
+  }
+
 }