1. Atlassian Labs
  2. Atlassian Labs
  3. Confluence KB Survey Plugin

Commits

atlassianlabs  committed 2c35098

Imported from SVN by Bitbucket

  • Participants
  • Branches master

Comments (0)

Files changed (85)

File LICENSE.TXT

View file
+To avoid future confusion, we recommend that you include a license with your plugin.
+This file is simply a reminder.
+
+For a template license you can have a look at: http://www.opensource.org/licenses/
+
+Atlassian releases most of its modules under a BSD license: http://www.opensource.org/licenses/bsd-license.php

File pom.xml

View file
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.atlassian.confluence.plugins</groupId>
+    <artifactId>knowledgebase-survey</artifactId>
+    <version>1.3.2-SNAPSHOT</version>
+    <packaging>atlassian-plugin</packaging>
+    <scm>
+        <connection>scm:svn:https://studio.plugins.atlassian.com/svn/CKBSP/trunk</connection>
+        <developerConnection>scm:svn:https://studio.plugins.atlassian.com/svn/CKBSP/trunk</developerConnection>
+        <url>https://studio.plugins.atlassian.com/source/browse/CKBSP</url>
+    </scm>
+    <organization>
+        <name>Altassian</name>
+        <url>http://www.atlassian.com/</url>
+    </organization>
+    <name>Knowledge Base Survey Plugin</name>
+    <description>A "Was this helpful" type macro and set of reports configured for a space.</description>
+    <repositories>
+        <repository>
+            <id>atlassian-public</id>
+            <url>https://m2proxy.atlassian.com/repository/public</url>
+            <snapshots>
+                <enabled>true</enabled>
+                <updatePolicy>daily</updatePolicy>
+                <checksumPolicy>warn</checksumPolicy>
+            </snapshots>
+            <releases>
+                <enabled>true</enabled>
+                <checksumPolicy>warn</checksumPolicy>
+            </releases>
+        </repository>
+    </repositories>
+    <pluginRepositories>
+        <pluginRepository>
+            <id>atlassian-public</id>
+            <url>https://m2proxy.atlassian.com/repository/public</url>
+            <releases>
+                <enabled>true</enabled>
+                <checksumPolicy>warn</checksumPolicy>
+            </releases>
+            <snapshots>
+                <checksumPolicy>warn</checksumPolicy>
+            </snapshots>
+        </pluginRepository>
+    </pluginRepositories>
+    <dependencies>
+        <dependency>
+            <groupId>com.atlassian.activeobjects</groupId>
+            <artifactId>activeobjects-plugin</artifactId>
+            <version>${ao.version}</version>
+            <scope>provided</scope>
+            <exclusions>
+                <exclusion>
+                    <artifactId>slf4j-api</artifactId>
+                    <groupId>org.slf4j</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.bandana</groupId>
+            <artifactId>atlassian-bandana</artifactId>
+            <version>3.1</version>
+            <scope>provided</scope>
+            <exclusions>
+                <exclusion>
+                    <artifactId>slf4j-api</artifactId>
+                    <groupId>org.slf4j</groupId>
+                </exclusion>
+                <exclusion>
+                	<artifactId>xstream</artifactId>
+                    <groupId>com.thoughtworks.xstream</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.thoughtworks.xstream</groupId>
+            <artifactId>xstream</artifactId>
+            <version>1.4.4</version>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.confluence</groupId>
+            <artifactId>confluence</artifactId>
+            <version>${confluence.version}</version>
+            <scope>provided</scope>
+            <exclusions>
+                <exclusion>
+                    <artifactId>slf4j-api</artifactId>
+                    <groupId>org.slf4j</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>slf4j-log4j12</artifactId>
+                    <groupId>org.slf4j</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.atlassian.confluence.plugin</groupId>
+            <artifactId>func-test</artifactId>
+            <version>2.3</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+     		<groupId>com.atlassian.sal</groupId>
+     		<artifactId>sal-api</artifactId>
+     		<version>2.2.0</version>
+     		<scope>provided</scope>
+  		</dependency>
+        <dependency>
+            <groupId>jmock</groupId>
+            <artifactId>jmock-cglib</artifactId>
+            <version>1.2.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>net.sourceforge.jwebunit</groupId>
+            <artifactId>jwebunit-htmlunit-plugin</artifactId>
+            <version>2.2</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <artifactId>dom4j</artifactId>
+                    <groupId>dom4j</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>jdom</artifactId>
+                    <groupId>jdom</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>xalan</artifactId>
+                    <groupId>xalan</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>commons-collections</artifactId>
+                    <groupId>commons-collections</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>commons-lang</artifactId>
+                    <groupId>commons-lang</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>commons-httpclient</artifactId>
+                    <groupId>commons-httpclient</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>commons-codec</artifactId>
+                    <groupId>commons-codec</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>commons-io</artifactId>
+                    <groupId>commons-io</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>commons-logging</artifactId>
+                    <groupId>commons-logging</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>xercesImpl</artifactId>
+                    <groupId>xerces</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>commons-codec</artifactId>
+                    <groupId>commons-codec</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>nekohtml</artifactId>
+                    <groupId>nekohtml</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>servlet-api</artifactId>
+                    <groupId>javax.servlet</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>slf4j-api</artifactId>
+                    <groupId>org.slf4j</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>slf4j-log4j12</artifactId>
+                    <groupId>org.slf4j</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>net.sourceforge.nekohtml</groupId>
+            <artifactId>nekohtml</artifactId>
+            <version>1.9.12</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>jaxen</groupId>
+            <artifactId>jaxen</artifactId>
+            <version>1.1.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <version>1.5.8</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.5.8</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>com.atlassian.maven.plugins</groupId>
+                <artifactId>maven-confluence-plugin</artifactId>
+                <version>${amps.version}</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <skipManifestValidation>true</skipManifestValidation>
+                    <productVersion>${confluence.version}</productVersion>
+                    <productDataVersion>${confluence.data.version}</productDataVersion>
+                    <!-- <instructions> <Spring-Context>*;timeout:=60</Spring-Context> <Import-Package> 
+						com.atlassian.hibernate*;version="${confluence.version}", com.atlassian.bandana*;version="0.0", 
+						net.sf.hibernate*;version"0.0", org.slf4j*;version="0.0", org.springframework.orm.hibernate.support*;version"0.0" 
+						</Import-Package> </instructions> -->
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>com.atlassian.maven.plugins</groupId>
+                <artifactId>maven-refapp-plugin</artifactId>
+                <version>${amps.version}</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <pluginArtifacts>
+                        <pluginArtifact>
+                            <groupId>com.atlassian.activeobjects</groupId>
+                            <artifactId>activeobjects-plugin</artifactId>
+                            <version>${ao.version}</version>
+                        </pluginArtifact>
+                        <pluginArtifact>
+                            <groupId>com.atlassian.activeobjects</groupId>
+                            <artifactId>activeobjects-refapp-spi</artifactId>
+                            <version>${ao.version}</version>
+                        </pluginArtifact>
+                    </pluginArtifacts>
+                    <productVersion>${refapp.version}</productVersion>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.5</source>
+                    <target>1.5</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>**/Abstract*</exclude>
+                        <exclude>**/Mock*</exclude>
+                        <exclude>**/*$*</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+        <pluginManagement>
+        	<plugins>
+        		<!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+        		<plugin>
+        			<groupId>org.eclipse.m2e</groupId>
+        			<artifactId>lifecycle-mapping</artifactId>
+        			<version>1.0.0</version>
+        			<configuration>
+        				<lifecycleMappingMetadata>
+        					<pluginExecutions>
+        						<pluginExecution>
+        							<pluginExecutionFilter>
+        								<groupId>
+        									com.atlassian.maven.plugins
+        								</groupId>
+        								<artifactId>
+        									maven-refapp-plugin
+        								</artifactId>
+        								<versionRange>
+        									[4.1.2,)
+        								</versionRange>
+        								<goals>
+        									<goal>
+        										compress-resources
+        									</goal>
+        									<goal>
+        										generate-rest-docs
+        									</goal>
+        									<goal>
+        										generate-manifest
+        									</goal>
+        									<goal>
+        										copy-bundled-dependencies
+        									</goal>
+        									<goal>
+        										filter-plugin-descriptor
+        									</goal>
+        								</goals>
+        							</pluginExecutionFilter>
+        							<action>
+        								<ignore />
+        							</action>
+        						</pluginExecution>
+        						<pluginExecution>
+        							<pluginExecutionFilter>
+        								<groupId>
+        									org.apache.maven.plugins
+        								</groupId>
+        								<artifactId>
+        									maven-compiler-plugin
+        								</artifactId>
+        								<versionRange>
+        									[3.0,)
+        								</versionRange>
+        								<goals>
+        									<goal>compile</goal>
+        									<goal>testCompile</goal>
+        								</goals>
+        							</pluginExecutionFilter>
+        							<action>
+        								<ignore />
+        							</action>
+        						</pluginExecution>
+        					</pluginExecutions>
+        				</lifecycleMappingMetadata>
+        			</configuration>
+        		</plugin>
+        	</plugins>
+        </pluginManagement>
+    </build>
+    <distributionManagement>
+        <repository>
+            <id>atlassian-contrib</id>
+            <name>Atlassian Contrib Repository</name>
+            <url>davs://maven.atlassian.com/contrib</url>
+        </repository>
+        <snapshotRepository>
+            <id>atlassian-contrib-snapshot</id>
+            <name>Atlassian Contrib Snapshot Repository</name>
+            <url>davs://maven.atlassian.com/contrib-snapshot</url>
+        </snapshotRepository>
+    </distributionManagement>
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>findbugs-maven-plugin</artifactId>
+                <version>2.3.1</version>
+            </plugin>
+        </plugins>
+    </reporting>
+    <properties>
+        <atlassian.plugin.key>confluence.plugins.knowledgebase.survey</atlassian.plugin.key>
+        <atlassian.product.version>${confluence.version}</atlassian.product.version>
+        <confluence.project.spring.version>2.0.8</confluence.project.spring.version>
+        <confluence.version>5.0</confluence.version>
+        <ao.version>0.19.12</ao.version>
+        <amps.version>4.1.4</amps.version>
+        <confluence.data.version>3.5</confluence.data.version>
+    </properties>
+</project>

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/AnnotatedListener.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+import net.java.ao.Query;
+
+import org.springframework.beans.factory.DisposableBean;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import com.atlassian.activeobjects.external.ActiveObjects;
+import com.atlassian.confluence.event.events.content.page.PageRemoveEvent;
+import com.atlassian.event.api.EventListener;
+import com.atlassian.event.api.EventPublisher;
+
+public class AnnotatedListener implements DisposableBean {
+	protected EventPublisher eventPublisher;
+	private ActiveObjects ao;
+	
+    public AnnotatedListener(EventPublisher eventPublisher, ActiveObjects ao) {
+    	this.ao = checkNotNull(ao);
+        this.eventPublisher = eventPublisher;
+        eventPublisher.register(this);
+    }
+    
+    /* When deleting an space, a pageRemoveEvent is generated for every page, so no need to listen for spaceRemoveEvent
+     * until space settings are stored using Active Objects (https://studio.plugins.atlassian.com/browse/CKBSP-45)
+     */
+    @EventListener
+    public void pageRemoveEvent(PageRemoveEvent event) {
+		Response[] rowsToDelete = ao.find(Response.class, Query.select().where("PAGE_ID = ?", event.getPage().getId()));
+		ao.delete(rowsToDelete);
+    }
+    
+	public void destroy() throws Exception {
+    	eventPublisher.unregister(this);
+	}
+
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/ConfigureAction.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+
+import com.atlassian.confluence.core.ConfluenceActionSupport;
+
+public class ConfigureAction extends ConfluenceActionSupport
+{
+    private SurveySettingsManager surveySettingsManager;
+    private int yesIncrement;
+    private int noDecrement;
+    private int boostAmplifier;
+    private int boostMaximum;
+    private int boostMinimum;
+
+    @Override
+    public String execute()
+    {
+        if (yesIncrement == 0)
+        {
+            return INPUT;
+        }
+
+        surveySettingsManager.setYesIncrememt(yesIncrement);
+        surveySettingsManager.setNoDecrememt(noDecrement);
+        surveySettingsManager.setBoostAmplifier(boostAmplifier);
+        surveySettingsManager.setBoostMaximum(boostMaximum);
+        surveySettingsManager.setBoostMinimum(boostMinimum);
+
+        return SUCCESS;
+    }
+
+    public void setSurveySettingsManager(SurveySettingsManager surveySettingsManager)
+    {
+        this.surveySettingsManager = surveySettingsManager;
+    }
+
+    public void setNoDecrement(int decrement)
+    {
+        this.noDecrement = decrement;
+    }
+
+    public int getNoDecrement()
+    {
+        return surveySettingsManager.getNoDecrement();
+    }
+
+    public void setYesIncrement(int increment)
+    {
+        this.yesIncrement = increment;
+    }
+
+    public int getYesIncrement()
+    {
+        return surveySettingsManager.getYesIncrement();
+    }
+
+    public void setBoostAmplifier(int amplifier)
+    {
+        this.boostAmplifier = amplifier;
+    }
+
+    public int getBoostAmplifier()
+    {
+        return surveySettingsManager.getBoostAmplifier();
+    }
+
+    public void setBoostMaximum(int boostMaximum)
+    {
+        this.boostMaximum = boostMaximum;
+    }
+
+    public int getBoostMaximum()
+    {
+        return surveySettingsManager.getBoostMaximum();
+    }
+
+    public void setBoostMinimum(int boostMinimum)
+    {
+        this.boostMinimum = boostMinimum;
+    }
+
+    public int getBoostMinimum()
+    {
+        return surveySettingsManager.getBoostMinimum();
+    }
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/Constants.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+@SuppressWarnings("unused")
+public final class Constants
+{
+	public static final int BUILD_NUMBER = 1; // Change only when data model changes
+	public static final String PLUGIN_KEY = "confluence.plugins.knowledgebase.survey";
+	public static final String UPGRADE_DESC = "Migrates previous content to Active Objects tables";
+	public static final String GA_NULL_VALUE = "Undefined";
+	
+    public static final int DEFAULT_YES_INCREMENT = 2;
+    public static final int DEFAULT_NO_DECREMENT = -1;
+    public static final int DEFAULT_BOOST_AMPLIFIER = 3;
+    public static final int DEFAULT_BOOST_MAXIMUM = 100;
+    public static final int DEFAULT_BOOST_MINIMUM = 40;
+
+    public static final String SETTING_PREFIX = "com.atlassian.confluence.plugins.knowledgebase.";
+    public static final String GLOBAL_SPACE_KEY = "GLOBAL";
+    public static final String YES_INCREMENT_KEY = SETTING_PREFIX + "yesincrement";
+    public static final String NO_DECREMENT_KEY = SETTING_PREFIX + "nodecrement";
+    public static final String BOOST_AMPLIFIER_KEY = SETTING_PREFIX + "boostamplifier";
+    public static final String BOOST_MAXIMUM_KEY = SETTING_PREFIX + "boostmaximum";
+    public static final String BOOST_MINIMUM_KEY = SETTING_PREFIX + "boostminimum";
+
+    public static final String CONFIG = SETTING_PREFIX + "kbsurvey.config";
+    public static final String COMPOSITE_SCORE = SETTING_PREFIX + "kbsurvey.results.compositescore.";
+    public static final String TOTAL_RESULTS = SETTING_PREFIX + "kbsurvey.results.totalsurveys.";
+    public static final String RESULTS = SETTING_PREFIX + "kbsurvey.results.";
+    public static final String MARKER = SETTING_PREFIX + "kbsurvey.results.survey.placeholder";
+    public static final String BALLOT = SETTING_PREFIX + "kbsurvey.results.ballot.";
+    public static final int MAX_STORED_RESULTS = 32;
+
+    public static final int MAX_RESULTS_PER_PAGE=15;
+
+    private Constants()
+    {
+        throw new AssertionError("Do not instantiate the Constants class");
+    }
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/DefaultSurveyFeedbackDao.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+import com.atlassian.activeobjects.external.*;
+import com.atlassian.confluence.pages.Page;
+import com.atlassian.confluence.pages.PageManager;
+import com.atlassian.confluence.plugins.knowledgebase.results.SurveyGaResult;
+import com.atlassian.confluence.plugins.knowledgebase.results.SurveyResult;
+import com.atlassian.confluence.spaces.Space;
+import static com.atlassian.confluence.plugins.knowledgebase.Constants.GA_NULL_VALUE;
+import net.java.ao.Query;
+
+import java.text.NumberFormat;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+public final class DefaultSurveyFeedbackDao implements SurveyFeedbackDao
+{
+	private ActiveObjects ao;
+
+    public void setActiveObjects(final ActiveObjects ao)
+    {
+    	this.ao = ao;
+    }
+    
+    // Get total surveys (primary question answered) per space
+    public int getSpaceBallotCount(final Space space)
+    {
+    	// As QUESTION_ID contains the space key, it should not be needed to filter by SPACE_KEY but it is added in case this changes in the future.
+    	return ao.count(Response.class, Query.select().where("SPACE_KEY = ? and QUESTION_ID = ?", space.getKey(), space.getKey() + "0"));
+    }
+    // As this method has the question ID as a parameter, it should be valid for any question, not only the primary
+    // Note that question ID contains the space id
+    public String getSpacePrimaryYesNoTotal(final String questionId)
+    {
+    	int no = 0, yes = 0;
+    	yes = ao.count(Response.class, Query.select().where("QUESTION_ID = ? and ANSWER = ?", questionId, true));
+    	no = ao.count(Response.class, Query.select().where("QUESTION_ID = ? and ANSWER = ?", questionId, false));
+    	return (yes == 0 && no == 0) ? "" : NumberFormat.getPercentInstance().format((double) yes / (double) (no + yes)) + " (" + yes + "/" + (no + yes) + ")";
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<String> getPageIdsSortedByCompositeScore(final String spaceKey, final Integer max, Integer yesIncrement, Integer noDecrement)
+    {
+    	final List<String> pageIdList = new LinkedList<String>();
+    	HashMap<Long, Integer> pagescore = new HashMap<Long, Integer>();
+    	Response[] responses;
+    	
+    	// Get disticnt pageids
+    	responses = ao.find(Response.class, Query.select("PAGE_ID").distinct().where("SPACE_KEY = ?", spaceKey));
+    	for (int i = 0; i < responses.length; i++) {
+    		int no = 0, yes = 0;
+    		long pageid = responses[i].getPageId();
+    		// Calculate composite score for that page
+    		yes = ao.count(Response.class, Query.select().where("PAGE_ID = ? and QUESTION_ID = ? and ANSWER = ?", pageid, spaceKey + "0", true)) * yesIncrement;
+    		no = ao.count(Response.class, Query.select().where("PAGE_ID = ? and QUESTION_ID = ? and ANSWER = ?", pageid, spaceKey + "0", false)) * noDecrement;
+    		// Store pageid and composite score
+    		pagescore.put(pageid, yes + no);
+    		// Add the pageid in the correspondent position in the sorted list
+    		if (i == 0 ||  pagescore.get(Long.parseLong(pageIdList.get(pageIdList.size() - 1))) >= pagescore.get(pageid))
+    			// First or last item in the list
+    			pageIdList.add(String.valueOf(pageid));
+    		else {
+    			// Insert pageid in the correct position 
+    			for (int j = 0; j < pageIdList.size(); j++) {
+    				if ((pagescore.get(Long.parseLong(pageIdList.get(j))) <= pagescore.get(pageid))) {
+    					pageIdList.add(j, String.valueOf(pageid));
+    					break;
+    				}
+    			}
+    				
+    		}
+    	}
+    	
+    	return (pageIdList.size() > max)? pageIdList.subList(0, max): pageIdList;
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<Long> getUnsurveyedPages(final Space space, final PageManager pageManager)
+    {
+    	List<Page> allPages = pageManager.getPages(space, true);
+    	List<Long> unsurveyedPages = new LinkedList<Long>();
+    	List<Long> surveyedPages = new LinkedList<Long>();
+    	Response[] surveyedResponses = ao.find(Response.class, Query.select("PAGE_ID").distinct().where("SPACE_KEY = ?", space.getKey()));
+    	for (int i = 0; i < surveyedResponses.length; i++)
+    		surveyedPages.add(surveyedResponses[i].getPageId());
+    	for (int i = 0; i < allPages.size(); i++) {
+    		if (!surveyedPages.contains(allPages.get(i).getId()))
+    			unsurveyedPages.add(allPages.get(i).getId());
+    	}
+    	
+    	
+    	return unsurveyedPages;
+    }
+
+
+    public int getNumberOfSurveyedPages(final String spaceKey)
+    {
+    	return ao.find(Response.class, Query.select("PAGE_ID").distinct().where("SPACE_KEY = ?", spaceKey)).length;
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<SurveyResult> getPagedResults(final String order, final String spaceKey, final int offset, final int max, Integer yesIncrement, Integer noDecrement)
+    {
+    	List<SurveyResult> pageList = new LinkedList<SurveyResult>();
+    	Response[] responses;
+    	HashMap<Long, Integer> pagescore = new HashMap<Long, Integer>();
+    	HashMap<Long, Integer> pagesTotalSurveys = new HashMap<Long, Integer>();
+    	boolean asc = "composite_asc".equals(order) || "total_asc".equals(order);
+    	boolean total = "total_asc".equals(order) || "total_desc".equals(order);
+    	
+    	// Get disticnt pageids
+    	responses = ao.find(Response.class, Query.select("PAGE_ID").distinct().where("SPACE_KEY = ?", spaceKey));
+    	
+    	for (int i = 0; i < responses.length; i++) {
+    		SurveyResult surveyResult = new SurveyResult();
+    		int no = 0, yes = 0;
+    		long pageid = responses[i].getPageId();
+    		surveyResult.setPageId(String.valueOf(pageid));
+    		// Calculate composite score for that page
+    		yes = ao.count(Response.class, Query.select().where("PAGE_ID = ? and QUESTION_ID = ? and ANSWER = ?", pageid, spaceKey + "0", true));
+    		no = ao.count(Response.class, Query.select().where("PAGE_ID = ? and QUESTION_ID = ? and ANSWER = ?", pageid, spaceKey + "0", false));
+    		// Store pageid and composite score
+    		pagescore.put(pageid, yes  * yesIncrement + no * noDecrement);
+    		surveyResult.setCompositeScore(String.valueOf(yes * yesIncrement + no * noDecrement));
+    		// Get and store total surveys for that page
+    		pagesTotalSurveys.put(pageid, yes + no);
+    		surveyResult.setTotalSurveys(String.valueOf(yes + no));
+    		if (total) {
+    			// Sort by total surveys
+    			// Add the pageid in the correspondent position in the sorted list
+    			if (i == 0 ||  (pagesTotalSurveys.get(Long.valueOf(pageList.get(pageList.size() - 1).getPageId())) >= pagesTotalSurveys.get(pageid)))
+    				// First or last item in the list
+    				pageList.add(surveyResult);
+    			else {
+    				// Insert pageid in the right position 
+    				for (int j = 0; j < pageList.size(); j++) {
+    					if (Integer.valueOf(pageList.get(j).getTotalSurveys()) <= pagesTotalSurveys.get(pageid)) {
+    						pageList.add(j, surveyResult);
+    						break;
+    					}
+    				}
+    			}
+    		} else {
+    			// Sort by composite score
+    			// Add the pageid in the correspondent position in the sorted list
+    			if (i == 0 ||  (pagescore.get(Long.valueOf(pageList.get(pageList.size() - 1).getPageId())) >= pagescore.get(pageid)))
+    				// First or last item in the list
+    				pageList.add(surveyResult);
+    			else {
+    				// Insert pageid in the correct position 
+    				for (int j = 0; j < pageList.size(); j++) {
+    					if (Integer.valueOf(pageList.get(j).getCompositeScore()) <= pagescore.get(pageid)) {
+    						pageList.add(j, surveyResult);
+    						break;
+    					}
+    				}
+    			}
+    		}
+    		
+      	}
+    	if (asc) {
+    		//Ascendant order, revert list
+    		List<SurveyResult> reversedPageList = new LinkedList<SurveyResult>();
+    		for (int i = pageList.size() -1; i >= 0; i--)
+    			reversedPageList.add(pageList.get(i));
+    		return reversedPageList.subList(offset, (offset + max < reversedPageList.size())? offset + max : reversedPageList.size());	
+    	}
+    	return pageList.subList(offset, (offset + max < pageList.size())? offset + max : pageList.size());
+    }
+    
+    @SuppressWarnings("unchecked")
+    public List<SurveyGaResult> getPagedResults(final String gaTag, final String order, final String spaceKey, final int offset, final int max, Integer yesIncrement, Integer noDecrement)
+    {
+    	List<SurveyGaResult> gaResultList = new LinkedList<SurveyGaResult>();
+    	Response[] responses;
+    	List<String> distinctFields= new LinkedList<String>();
+    	HashMap<String, Integer> pagescore = new HashMap<String, Integer>();
+    	HashMap<String, Integer> pagesTotalSurveys = new HashMap<String, Integer>();
+    	boolean asc = "composite_asc".equals(order) || "total_asc".equals(order);
+    	boolean total = "total_asc".equals(order) || "total_desc".equals(order);
+    	
+    	// Get distinct pageids
+    	/* Note by Alex:
+    	 * This would be the most straight way to get a list of all distinct fields, but because what I believe
+    	 * a bug in Active Objects implementation, this line fails to execute successfully hence I had to find a workaround for it
+    	 *   	responses = ao.find(Response.class, gaTag, Query.select(gaTag).distinct().where("SPACE_KEY = ? and DATE is not NULL", spaceKey)); */
+    	responses = ao.find(Response.class, Query.select().distinct().where("SPACE_KEY = ? and DATE is not NULL", spaceKey));
+    	for (int i = 0; i < responses.length; i++) {
+    		String currentFieldValue = "";
+    		if (gaTag.equals("UTM_SOURCE"))
+    			currentFieldValue = responses[i].getUtmSource();
+    		if (gaTag.equals("UTM_MEDIUM"))
+    			currentFieldValue = responses[i].getUtmMedium();
+    		if (gaTag.equals("UTM_CONTENT"))
+    			currentFieldValue = responses[i].getUtmContent();
+    		if (currentFieldValue == null)
+    			currentFieldValue = GA_NULL_VALUE;
+    		if (!distinctFields.contains(currentFieldValue))
+    			distinctFields.add(currentFieldValue);
+    	}
+    	Iterator<String> it = distinctFields.iterator();
+    	while (it.hasNext()) {
+    		SurveyGaResult surveyResult = new SurveyGaResult();
+    		int no = 0, yes = 0;
+    		String gaValue = it.next();
+    		surveyResult.setGaValue(gaValue);
+    		surveyResult.setGaTag(gaTag);
+    		
+    		// Calculate composite score for that page
+    		if (gaValue.equals(GA_NULL_VALUE)) {
+    			yes = ao.count(Response.class, Query.select().where(gaTag + " is NULL and SPACE_KEY = ? and QUESTION_ID = ? and DATE is not NULL and ANSWER = ?", spaceKey, spaceKey + "0", true));
+	    		no = ao.count(Response.class, Query.select().where(gaTag + " is NULL and SPACE_KEY = ? and QUESTION_ID = ? and DATE is not NULL and ANSWER = ?", spaceKey, spaceKey + "0", false));	
+    		} else {
+	    		yes = ao.count(Response.class, Query.select().where(gaTag + " = ? and SPACE_KEY = ? and QUESTION_ID = ? and DATE is not NULL and ANSWER = ?", gaValue, spaceKey, spaceKey + "0", true));
+	    		no = ao.count(Response.class, Query.select().where(gaTag + " = ? and SPACE_KEY = ? and QUESTION_ID = ? and DATE is not NULL and ANSWER = ?", gaValue, spaceKey, spaceKey + "0", false));
+    		}
+    		// Store gaValue and composite score
+    		pagescore.put(gaValue, yes  * yesIncrement + no * noDecrement);
+    		surveyResult.setCompositeScore(String.valueOf(yes * yesIncrement + no * noDecrement));
+    		// Get and store total surveys for that page
+    		pagesTotalSurveys.put(gaValue, yes + no);
+    		surveyResult.setTotalSurveys(String.valueOf(yes + no));
+    		if (total) {
+    			// Sort by total surveys
+    			// Add the gaValue in the correspondent position in the sorted list
+    			if (gaResultList.size() == 0 ||  (pagesTotalSurveys.get(gaResultList.get(gaResultList.size() - 1).getGaValue()) >= pagesTotalSurveys.get(gaValue)))
+    				// First or last item in the list
+    				gaResultList.add(surveyResult);
+    			else {
+    				// Insert gaValue in the correct position 
+    				for (int j = 0; j < gaResultList.size(); j++) {
+    					if (Integer.valueOf(gaResultList.get(j).getTotalSurveys()) <= pagesTotalSurveys.get(gaValue)) {
+    						gaResultList.add(j, surveyResult);
+    						break;
+    					}
+    				}
+    			}
+    		} else {
+    			// Sort by composite score
+    			// Add the gaValue in the correspondent position in the sorted list
+    			if (gaResultList.size() == 0 ||  (pagescore.get(gaResultList.get(gaResultList.size() - 1).getGaValue()) >= pagescore.get(gaValue)))
+    				// First or last item in the list
+    				gaResultList.add(surveyResult);
+    			else {
+    				// Insert gaValue in the correct position 
+    				for (int j = 0; j < gaResultList.size(); j++) {
+    					if (Integer.valueOf(gaResultList.get(j).getCompositeScore()) <= pagescore.get(gaValue)) {
+    						gaResultList.add(j, surveyResult);
+    						break;
+    					}
+    				}
+    			}
+    		}
+    		
+      	}
+    	if (asc) {
+    		//Ascendant order, revert list
+    		List<SurveyGaResult> reversedPageList = new LinkedList<SurveyGaResult>();
+    		for (int i = gaResultList.size() -1; i >= 0; i--)
+    			reversedPageList.add(gaResultList.get(i));
+    		return reversedPageList.subList(offset, (offset + max < reversedPageList.size())? offset + max : reversedPageList.size());	
+    	}
+    	return gaResultList.subList(offset, (offset + max < gaResultList.size())? offset + max : gaResultList.size());
+    }
+}
+

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/DefaultSurveyFeedbackManager.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+import static com.atlassian.confluence.plugins.knowledgebase.Constants.CONFIG;
+import static com.atlassian.confluence.plugins.knowledgebase.Constants.GA_NULL_VALUE;
+
+import com.atlassian.confluence.core.ContentPropertyManager;
+import com.atlassian.confluence.pages.AbstractPage;
+import com.atlassian.confluence.pages.PageManager;
+import com.atlassian.confluence.plugins.knowledgebase.config.SurveyConfig;
+import com.atlassian.confluence.plugins.knowledgebase.config.SurveyQuestion;
+import com.atlassian.confluence.plugins.knowledgebase.results.SurveyGaResult;
+import com.atlassian.confluence.plugins.knowledgebase.results.SurveyResult;
+import com.atlassian.confluence.spaces.Space;
+import com.atlassian.confluence.spaces.SpaceDescription;
+import com.thoughtworks.xstream.XStream;
+
+
+import com.atlassian.activeobjects.external.*;
+
+import java.util.Date;
+import java.text.NumberFormat;
+import java.util.LinkedList;
+import java.util.List;
+
+import net.java.ao.DBParam;
+import net.java.ao.Query;
+
+public class DefaultSurveyFeedbackManager implements SurveyFeedbackManager {
+	private ActiveObjects ao;
+	private SurveyFeedbackDao surveyFeedbackDao;
+	private ContentPropertyManager contentPropertyManager;
+	private SurveySettingsManager surveySettingsManager;
+	private final XStream xstream = new XStream();
+	
+
+	public DefaultSurveyFeedbackManager() {
+		xstream.setClassLoader(getClass().getClassLoader());
+		xstream.alias("surveyconfig", SurveyConfig.class);
+		xstream.alias("surveyquestion", SurveyQuestion.class);
+	}
+
+	public void setActiveObjects(final ActiveObjects ao) {
+		this.ao = ao;
+	}
+
+	public void setSurveyFeedbackDao(final SurveyFeedbackDao surveyFeedbackDao) {
+		this.surveyFeedbackDao = surveyFeedbackDao;
+	}
+
+	public void setContentPropertyManager(
+			final ContentPropertyManager contentPropertyManager) {
+		this.contentPropertyManager = contentPropertyManager;
+	}
+
+	public void setSurveySettingsManager(
+			final SurveySettingsManager surveySettingsManager) {
+		this.surveySettingsManager = surveySettingsManager;
+	}
+
+	public SurveyConfig getSurveyConfig(SpaceDescription spaceDesc) {
+		String xml = contentPropertyManager.getTextProperty(spaceDesc, CONFIG);
+		if (xml == null || xml.equals(""))
+			return new SurveyConfig();
+		else
+			return (SurveyConfig) xstream.fromXML(xml);
+	}
+
+	public void setSurveyConfig(SpaceDescription spaceDesc,
+			SurveyConfig surveyConfig) {
+		contentPropertyManager.setTextProperty(spaceDesc, CONFIG,
+				xstream.toXML(surveyConfig));
+	}
+
+	public String getCompositeScore(AbstractPage page) {
+		int yes = ao.count(Response.class, Query.select().where("PAGE_ID = ? and QUESTION_ID = ? and ANSWER = ?", page.getId(), page.getSpaceKey() + "0", true)) * surveySettingsManager.getYesIncrement();
+		int no = ao.count(Response.class, Query.select().where("PAGE_ID = ? and QUESTION_ID = ? and ANSWER = ?", page.getId(), page.getSpaceKey() + "0", false)) * surveySettingsManager.getNoDecrement();
+		return yes + no == 0 ? "" : Integer.toString(yes + no);
+	}
+	
+	/* Composite score for a GA var.
+	 * gaTag is the field to check (UTM_MEDIUM, UTM_SOURCE or UTM_CONTENT)
+	 * gaValue is the content of the field specified in gaTag
+	 */
+	public String getCompositeScore(String gaTag, String gaValue, String spaceKey) {
+		int yes = ao.count(Response.class, Query.select().where(gaTag + " = ? and SPACE_KEY = ? and QUESTION_ID = ? and DATE is not NULL and ANSWER = ?", gaValue, spaceKey, spaceKey + "0", true)) * surveySettingsManager.getYesIncrement();
+		int no = ao.count(Response.class, Query.select().where(gaTag + " = ? and SPACE_KEY = ? and QUESTION_ID = ? and DATE is not NULL ANSWER = ?", gaValue, spaceKey, spaceKey + "0", false)) * surveySettingsManager.getNoDecrement();
+		return yes + no == 0 ? "" : Integer.toString(yes + no);
+	}
+
+	// Number of Surveys for a page
+	public String getNumberOfSurveys(AbstractPage page, String spaceKey) {
+		return Integer.toString(ao.count(Response.class, Query.select().where("PAGE_ID = ? and SPACE_KEY = ? and QUESTION_ID = ?", page.getId(), spaceKey, spaceKey + "0")));
+	}
+	
+	/* Number of Surveys for a GA var.
+	 * gaTag is the field to check (UTM_MEDIUM, UTM_SOURCE or UTM_CONTENT)
+	 * gaValue is the content of the field specified in gaTag
+	 */
+	public String getNumberOfSurveys(String gaTag, String gaValue, String spaceKey) {
+		return Integer.toString(ao.count(Response.class, Query.select().where(gaTag + " = ? and SPACE_KEY = ? and DATE is not NULL", gaValue, spaceKey)));
+	}
+
+	public String getYesNoPercentage(AbstractPage page, String questionId) {
+		int yes = ao.count(Response.class, Query.select().where("PAGE_ID = ? and QUESTION_ID = ? and ANSWER = ?", page.getId(), questionId, true));
+		int no = ao.count(Response.class, Query.select().where("PAGE_ID = ? and QUESTION_ID = ? and ANSWER = ?", page.getId(), questionId, false));
+
+		return yes == 0 ? NumberFormat.getPercentInstance().format(0) + " ("
+				+ "0/" + no + ")" : no == 0 ? NumberFormat.getPercentInstance()
+				.format(1) + " (" + yes + "/" + yes + ")" : NumberFormat
+				.getPercentInstance().format(
+						(double) yes / ((double) yes + (double) no))
+				+ " (" + yes + "/" + (yes + no) + ")";
+
+	}
+	
+	/* Calculate yes/no info for a GA var.
+	 * gaTag is the field to check (UTM_MEDIUM, UTM_SOURCE or UTM_CONTENT)
+	 * gaValue is the content of the field specified in gaTag
+	 */
+	public String getYesNoPercentage(String gaTag, String gaValue, String spaceKey, String questionId) {
+		int yes, no;
+		if (gaValue.equals(GA_NULL_VALUE)) {
+			yes = ao.count(Response.class, Query.select().where(gaTag + " is NULL and SPACE_KEY = ? and QUESTION_ID = ? and DATE is not NULL and ANSWER = ?", spaceKey, questionId, true));
+			no = ao.count(Response.class, Query.select().where(gaTag + " is NULL and SPACE_KEY = ? and QUESTION_ID = ? and DATE is not NULL and ANSWER = ?", spaceKey, questionId, false));
+		} else {
+			yes = ao.count(Response.class, Query.select().where(gaTag + " = ? and SPACE_KEY = ? and QUESTION_ID = ? and DATE is not NULL and ANSWER = ?", gaValue, spaceKey, questionId, true));
+			no = ao.count(Response.class, Query.select().where(gaTag + " = ? and SPACE_KEY = ? and QUESTION_ID = ? and DATE is not NULL and ANSWER = ?", gaValue, spaceKey, questionId, false));
+		}
+
+		return yes == 0 ? NumberFormat.getPercentInstance().format(0) + " ("
+				+ "0/" + no + ")" : no == 0 ? NumberFormat.getPercentInstance()
+				.format(1) + " (" + yes + "/" + yes + ")" : NumberFormat
+				.getPercentInstance().format(
+						(double) yes / ((double) yes + (double) no))
+				+ " (" + yes + "/" + (yes + no) + ")";
+
+	}
+
+	public String getSpaceBallotCount(Space space) {
+		return String.valueOf(surveyFeedbackDao.getSpaceBallotCount(space));
+	}
+
+	public String getSpacePrimaryYesNoTotal(String questionId) {
+		return surveyFeedbackDao.getSpacePrimaryYesNoTotal(questionId);
+	}
+	// Store a new answer setting the date to the current time and date
+	public void addSurveyResult(AbstractPage page, String questionId,
+			Boolean answer, String spaceKey, String utmSource, String utmMedium, String utmContent) {
+
+		// AO store new answer
+		ao.create(Response.class, new DBParam("QUESTION_ID", questionId), new DBParam("SPACE_KEY", spaceKey), new DBParam("ANSWER", answer),
+				new DBParam("PAGE_ID", page.getId()), new DBParam("UTM_SOURCE", utmSource), new DBParam("UTM_MEDIUM", utmMedium), new DBParam("UTM_CONTENT", utmContent), new DBParam("DATE", new Date()));
+	}
+	// Store a new answer specifying a date
+	public void addSurveyResult(AbstractPage page, String questionId,
+			Boolean answer, String spaceKey, String utmSource, String utmMedium, String utmContent, Date date) {
+
+		// AO store new answer
+		ao.create(Response.class, new DBParam("QUESTION_ID", questionId), new DBParam("SPACE_KEY", spaceKey), new DBParam("ANSWER", answer),
+				new DBParam("PAGE_ID", page.getId()), new DBParam("UTM_SOURCE", utmSource), new DBParam("UTM_MEDIUM", utmMedium), new DBParam("UTM_CONTENT", utmContent), new DBParam("DATE", date));
+	}
+
+	public List<String> getPageIdsSortedByCompositeScore(String spaceKey,
+			Integer max) {
+		return surveyFeedbackDao
+				.getPageIdsSortedByCompositeScore(spaceKey, max, surveySettingsManager.getYesIncrement(), surveySettingsManager.getNoDecrement());
+	}
+
+	public List<Long> getUnsurveyedPageIdList(Space space, PageManager pageManager) {
+		return surveyFeedbackDao.getUnsurveyedPages(space, pageManager);
+	}
+
+	public int getNumberOfSurveyedPages(String spaceKey) {
+		return surveyFeedbackDao.getNumberOfSurveyedPages(spaceKey);
+	}
+	
+	public int getNumberOfGaValues(String gaTag, String spaceKey) {
+		/* Buggy AO don't let me do
+		 * 		return ao.find(Response.class, gaTag, Query.select(gaTag).distinct().where("SPACE_KEY = ?", spaceKey)).length;
+		 * nor
+		 * 		return ao.count(Response.class, Query.select(gaTag).distinct().where("SPACE_KEY = ?", spaceKey));
+		 * 
+		 * Check:
+		 * 		https://ecosystem.atlassian.net/browse/AO-398
+		 * 		https://ecosystem.atlassian.net/browse/AO-399
+		 * 
+		 * For more information.
+		 * 
+		 */
+		List<String> distinctFields= new LinkedList<String>();
+		Response [] responses = ao.find(Response.class, Query.select().distinct().where("SPACE_KEY = ? and DATE is not NULL", spaceKey));
+    	for (int i = 0; i < responses.length; i++) {
+    		String currentFieldValue = "";
+    		if (gaTag.equals("UTM_SOURCE"))
+    			currentFieldValue = responses[i].getUtmSource();
+    		if (gaTag.equals("UTM_MEDIUM"))
+    			currentFieldValue = responses[i].getUtmMedium();
+    		if (gaTag.equals("UTM_CONTENT"))
+    			currentFieldValue = responses[i].getUtmContent();
+    		if (!distinctFields.contains(currentFieldValue))
+    			distinctFields.add(currentFieldValue);
+    	}
+    	return distinctFields.size();
+	}
+
+	public List<SurveyResult> getPagedResults(String order, String spaceKey,
+			int offset, int max) {
+		return surveyFeedbackDao.getPagedResults(order, spaceKey, offset, max, surveySettingsManager.getYesIncrement(), surveySettingsManager.getNoDecrement());
+	}
+	
+	public List<SurveyGaResult> getPagedResults(String gaTag, String order, String spaceKey, int offset, int max) {
+		return surveyFeedbackDao.getPagedResults(gaTag, order, spaceKey, offset, max, surveySettingsManager.getYesIncrement(), surveySettingsManager.getNoDecrement());
+	}
+
+	public void evictSurveyResults(AbstractPage page) {
+		Response[] rowsToDelete = ao.find(Response.class, Query.select().where("PAGE_ID = ?", page.getId()));
+		for (int i = 0; i < rowsToDelete.length; i++)
+			ao.delete(rowsToDelete[i]);
+	}
+
+	public boolean hasPageResult(AbstractPage page, String spaceKey) {
+		return (ao.find(Response.class, Query.select().where("PAGE_ID = ?", page.getId())).length > 0);
+	}
+	
+	public boolean hasGaResult(String gaTag, String spaceKey) {
+		return (ao.find(Response.class, Query.select(gaTag).distinct().where("SPACE_KEY = ?", spaceKey)).length > 0);
+	}
+
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/DefaultSurveySettingsManager.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+import static com.atlassian.confluence.plugins.knowledgebase.Constants.*;
+import net.java.ao.DBParam;
+import net.java.ao.Query;
+
+import org.apache.log4j.Logger;
+import com.atlassian.activeobjects.external.*;
+
+public class DefaultSurveySettingsManager implements SurveySettingsManager
+{
+    private static final Logger log = Logger.getLogger(SurveySettingsManager.class);
+    private ActiveObjects ao;
+    
+    public DefaultSurveySettingsManager()
+    {
+    	// Do nothing
+    }
+    
+    public void setActiveObjects(final ActiveObjects ao)
+    {
+    	this.ao = ao;
+    }
+
+    public Integer getYesIncrement()
+    {
+        Integer result = getSetting(YES_INCREMENT_KEY);
+        return result != null ? result : defaultYesIncrement();
+    }
+
+    private int defaultYesIncrement()
+    {
+        setYesIncrememt(DEFAULT_YES_INCREMENT);
+        return DEFAULT_YES_INCREMENT;
+    }
+
+    public void setYesIncrememt(Integer percentage)
+    {
+        store(YES_INCREMENT_KEY, percentage);
+    }
+
+    public Integer getNoDecrement()
+    {
+        Integer result = getSetting(NO_DECREMENT_KEY);
+        return result != null ? result : defaultNoDecrement();
+    }
+
+    private int defaultNoDecrement()
+    {
+        setNoDecrememt(DEFAULT_NO_DECREMENT);
+        return DEFAULT_NO_DECREMENT;
+    }
+
+    public void setNoDecrememt(int percentage)
+    {
+        store(NO_DECREMENT_KEY, percentage);
+    }
+
+    public Integer getBoostAmplifier()
+    {
+        Integer result = getSetting(BOOST_AMPLIFIER_KEY);
+        return result != null ? result : defaultBoostAmplifier();
+    }
+
+    private int defaultBoostAmplifier()
+    {
+        setBoostAmplifier(DEFAULT_BOOST_AMPLIFIER);
+        return DEFAULT_BOOST_AMPLIFIER;
+    }
+
+    public void setBoostAmplifier(int percentage)
+    {
+        store(BOOST_AMPLIFIER_KEY, percentage);
+    }
+
+    public Integer getBoostMaximum()
+    {
+        Integer result = getSetting(BOOST_MAXIMUM_KEY);
+        return result != null ? result : defaultBoostMaximum();
+    }
+
+    private int defaultBoostMaximum()
+    {
+        setBoostMaximum(DEFAULT_BOOST_MAXIMUM);
+        return DEFAULT_BOOST_MAXIMUM;
+    }
+
+    public void setBoostMaximum(int percentage)
+    {
+        store(BOOST_MAXIMUM_KEY, percentage);
+    }
+
+    public Integer getBoostMinimum()
+    {
+        Integer result = getSetting(BOOST_MINIMUM_KEY);
+        return result != null ? result : defaultBoostMinimum();
+    }
+
+    private Integer defaultBoostMinimum()
+    {
+        setBoostMinimum(DEFAULT_BOOST_MINIMUM);
+        return DEFAULT_BOOST_MINIMUM;
+    }
+
+    public void setBoostMinimum(int percentage)
+    {
+        store(BOOST_MINIMUM_KEY, percentage);
+    }
+
+    private Integer getSetting(String key)
+    {
+    	Settings[] ao_current_settings = ao.find(Settings.class, Query.select().where("KEY = ? and SPACE = ?", key, GLOBAL_SPACE_KEY));
+        if (ao_current_settings.length > 0)
+        	// There should be only one result
+        	return ao_current_settings[0].getValue();
+        else
+        	return null;
+    }
+
+    private void store(String key, Integer value)
+    {
+    	// AO_Settings ao_settings = ao.get(AO_Settings.class, id);
+    	Settings[] ao_current_settings = ao.find(Settings.class, Query.select().where("KEY = ? and SPACE = ?", key, GLOBAL_SPACE_KEY));
+    	Settings ao_settings = (ao_current_settings.length > 0)? ao_current_settings[0] : null;
+    	if (ao_settings == null)
+    		// Setting does not exist in DB. Create a new entry.
+    		ao.create(Settings.class, new DBParam("SPACE", GLOBAL_SPACE_KEY), new DBParam("KEY", key), new DBParam("VALUE", value));
+    	else {
+    		// Setting already exist. Update current entry.
+    		ao_settings.setKey(key);
+    		ao_settings.setSpace(GLOBAL_SPACE_KEY);
+    		ao_settings.setValue(value);  
+    		ao_settings.save();
+    	}
+    }
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/Response.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+import java.util.Date;
+
+import net.java.ao.Entity;
+import net.java.ao.schema.*;
+
+public interface Response extends Entity {
+	@AutoIncrement
+    @NotNull
+    @PrimaryKey("ID")
+	public int getID();
+
+	@NotNull
+	public long getPageId();
+	public void setPageId(long pageId);
+	
+	@NotNull
+	public String getSpaceKey();
+	public void setSpaceKey(String spaceKey);
+	
+	@NotNull
+	public String getQuestionId();
+	public void setQuestionId(String questionId);
+	
+	@NotNull
+	public boolean getAnswer();
+	public void setAnswer(boolean answer);
+	
+	public String getUtmSource();
+	public void setUtmSource(String utmSource);
+	
+	public String getUtmMedium();
+	public void setUtmMedium(String utmMedium);
+	
+	public String getUtmContent();
+	public void setUtmContent(String utmContent);
+	
+	public Date getDate();
+	public void setDate(Date date);
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/Settings.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+import net.java.ao.schema.*;
+import net.java.ao.Entity;
+
+public interface Settings extends Entity {
+	@PrimaryKey
+	@AutoIncrement
+	public Integer getId();
+	
+	@NotNull
+	public String getSpace();
+	public void setSpace(String key);
+	
+	@NotNull
+	public String getKey();
+	public void setKey(String key);
+
+	@NotNull
+	public Integer getValue();
+	public void setValue(Integer value);
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/SurveyCondition.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+import com.atlassian.confluence.plugin.descriptor.web.WebInterfaceContext;
+import com.atlassian.confluence.plugin.descriptor.web.conditions.BaseConfluenceCondition;
+import com.atlassian.confluence.security.SpacePermissionManager;
+import com.atlassian.confluence.security.SpacePermission;
+import java.util.Collections;
+
+public class SurveyCondition extends BaseConfluenceCondition
+{
+    
+    private SpacePermissionManager spacePermissionManager;
+
+    public boolean shouldDisplay(WebInterfaceContext context)
+    {
+        return spacePermissionManager.hasPermission(Collections.singletonList(SpacePermission.ADMINISTER_SPACE_PERMISSION),
+                context.getSpace(), context.getUser());
+    }
+
+    public void setSpacePermissionManager(SpacePermissionManager spacePermissionManager)
+    {
+        this.spacePermissionManager = spacePermissionManager;
+    }
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/SurveyFeedbackDao.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+import com.atlassian.confluence.pages.PageManager;
+import com.atlassian.confluence.plugins.knowledgebase.results.SurveyGaResult;
+import com.atlassian.confluence.plugins.knowledgebase.results.SurveyResult;
+import com.atlassian.confluence.spaces.Space;
+
+import java.util.List;
+
+public interface SurveyFeedbackDao {
+
+    public int getSpaceBallotCount(Space space);
+
+    public String getSpacePrimaryYesNoTotal(String questionId);
+
+    public List<String> getPageIdsSortedByCompositeScore(String spaceKey, Integer max, Integer yesIncrement, Integer noDecrement);
+
+    public List<Long> getUnsurveyedPages(Space space, PageManager pageManager);
+
+    public int getNumberOfSurveyedPages(String spaceKey);
+
+    public List<SurveyResult> getPagedResults(String order, String spaceKey, int offset, int max, Integer yesIncrement, Integer noDecrement);
+    public List<SurveyGaResult> getPagedResults(String gaTag, String order, String spaceKey, int offset, int max, Integer yesIncrement, Integer noDecrement);
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/SurveyFeedbackManager.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+import com.atlassian.confluence.plugins.knowledgebase.config.SurveyConfig;
+import com.atlassian.confluence.plugins.knowledgebase.results.SurveyGaResult;
+import com.atlassian.confluence.plugins.knowledgebase.results.SurveyResult;
+import com.atlassian.confluence.spaces.SpaceDescription;
+import com.atlassian.confluence.spaces.Space;
+import com.atlassian.confluence.pages.AbstractPage;
+import com.atlassian.confluence.pages.PageManager;
+
+import java.util.List;
+import java.util.Date;
+
+
+public interface SurveyFeedbackManager {
+    public SurveyConfig getSurveyConfig(SpaceDescription spaceDesc);
+
+    public void setSurveyConfig(SpaceDescription spaceDesc, SurveyConfig surveyConfig);
+
+    public String getCompositeScore(AbstractPage page);
+    public String getCompositeScore(String gaTag, String gaValue, String spaceKey);
+
+    public String getNumberOfSurveys(AbstractPage page, String spaceKey);
+    public String getNumberOfSurveys(String gaTag, String gaValue, String spaceKey);
+
+    public String getYesNoPercentage(AbstractPage page, String questionId);
+    public String getYesNoPercentage(String gaTag, String gaValue, String spaceKey, String questionId);
+
+    public String getSpaceBallotCount(Space space);
+
+    public String getSpacePrimaryYesNoTotal(String questionId);
+
+    public void addSurveyResult(AbstractPage page, String questionId, Boolean answer, String spaceKey, String utmSource, String utmMedium, String utmContent);
+    public void addSurveyResult(AbstractPage page, String questionId, Boolean answer, String spaceKey, String utmSource, String utmMedium, String utmContent, Date date);
+
+    public List<String> getPageIdsSortedByCompositeScore(String spaceKey, Integer max);
+
+    public List<Long> getUnsurveyedPageIdList(Space space, PageManager pageManager);
+
+    public int getNumberOfSurveyedPages(String spaceKey);
+    public int getNumberOfGaValues(String gaTag, String spaceKey);
+
+    public List<SurveyResult> getPagedResults(String order, String spaceKey, int offset, int max);
+    public List<SurveyGaResult> getPagedResults(String gaTag, String order, String spaceKey, int offset, int max);
+
+    public void evictSurveyResults(AbstractPage page);
+
+    public boolean hasPageResult(AbstractPage page, String spaceKey);
+    public boolean hasGaResult(String gaTag, String spaceKey);
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/SurveySettingsManager.java

View file
+package com.atlassian.confluence.plugins.knowledgebase;
+
+public interface SurveySettingsManager
+{
+    public Integer getYesIncrement();
+    public void setYesIncrememt(Integer percentage);
+
+    public Integer getNoDecrement();
+    public void setNoDecrememt(int percentage);
+
+    public Integer getBoostAmplifier();
+    public void setBoostAmplifier(int percentage);
+
+    public Integer getBoostMaximum();
+    public void setBoostMaximum(int percentage);
+
+    public Integer getBoostMinimum();
+    public void setBoostMinimum(int percentage);
+
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/config/AddFeedbackAction.java

View file
+package com.atlassian.confluence.plugins.knowledgebase.config;
+
+import com.atlassian.confluence.plugins.knowledgebase.SurveyFeedbackManager;
+import com.atlassian.confluence.spaces.SpaceDescription;
+import com.atlassian.confluence.spaces.actions.AbstractSpaceAdminAction;
+import com.atlassian.confluence.spaces.actions.SpaceAware;
+import com.atlassian.sal.api.websudo.WebSudoRequired;
+import com.opensymphony.webwork.interceptor.ServletRequestAware;
+import org.apache.commons.lang.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+@WebSudoRequired
+public class AddFeedbackAction extends AbstractSpaceAdminAction implements ServletRequestAware,SpaceAware
+{
+
+    private SurveyConfig surveyConfig;
+    private String newQuestion;
+    private String primaryQuestion;
+    private String response;
+    private String responseUser;
+    private HttpServletRequest request;
+    private SurveyFeedbackManager surveyFeedbackManager;
+
+    public String execute() throws Exception
+    {
+        SpaceDescription spaceDesc = this.getSpace().getDescription();
+        surveyConfig =  surveyFeedbackManager.getSurveyConfig(spaceDesc);
+        if (StringUtils.isNotBlank(primaryQuestion))
+        {
+            surveyConfig.setPrimaryQuestionId(this.getSpace().getKey()+"0");
+            surveyConfig.setPrimaryQuestion(primaryQuestion);
+        }
+        if (StringUtils.isNotBlank(newQuestion))
+            surveyConfig.addQuestion(new SurveyQuestion(newQuestion,this.getSpace().getKey() + (surveyConfig.getQuestionMap().size()+1)));
+
+        surveyConfig.setResponse(response);
+        surveyConfig.setResponseUser(responseUser);
+
+
+        //Edit names - only for active questions!
+        for (SurveyQuestion question : surveyConfig.getActiveQuestionList())
+        {
+            String newName = request.getParameter(question.getId());
+            if (StringUtils.isNotBlank(newName))
+            {
+              question.setQuestion(newName);
+                
+              surveyConfig.getQuestionMap().put(question.getId(),question);
+            }
+        }
+        surveyFeedbackManager.setSurveyConfig(spaceDesc,surveyConfig);
+        return SUCCESS;
+
+    }
+
+    public void setResponse(String r)
+    {
+       response = r;
+    }
+
+    public String getResponse()
+    {
+        return response;
+    }
+
+    public String getResponseUser()
+    {
+        return responseUser;
+    }
+
+    public void setResponseUser(String responseUser)
+    {
+        this.responseUser = responseUser;
+    }
+
+    public void setAdditionalQuestion(String q)
+    {
+        newQuestion = q;
+    }
+
+    public void setPrimaryQuestion(String pq)
+    {
+        primaryQuestion = pq;
+    }
+
+
+    public SurveyConfig getSurveyConfig()
+    {
+        return surveyConfig;
+    }
+
+    public void setServletRequest(HttpServletRequest httpServletRequest)
+    {
+        request = httpServletRequest;
+    }
+
+    public void setSurveyFeedbackManager(SurveyFeedbackManager surveyFeedbackManager)
+    {
+        this.surveyFeedbackManager = surveyFeedbackManager;
+    }
+
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/config/ConfigureSurveyAction.java

View file
+package com.atlassian.confluence.plugins.knowledgebase.config;
+
+import com.atlassian.confluence.spaces.actions.SpaceAware;
+import com.atlassian.confluence.spaces.actions.AbstractSpaceAdminAction;
+import com.atlassian.confluence.spaces.SpaceDescription;
+import com.atlassian.confluence.plugins.knowledgebase.SurveyFeedbackManager;
+import com.atlassian.sal.api.websudo.WebSudoRequired;
+
+/**
+ * Configure the feedback on  space level
+ */
+@WebSudoRequired
+public class ConfigureSurveyAction extends AbstractSpaceAdminAction implements SpaceAware
+{
+    private SurveyConfig surveyConfig;
+    private SurveyFeedbackManager surveyFeedbackManager;
+
+    public void setSurveyFeedbackManager(SurveyFeedbackManager surveyFeedbackManager)
+    {
+        this.surveyFeedbackManager = surveyFeedbackManager;
+    }
+
+    @com.atlassian.xwork.RequireSecurityToken(true)
+    public String execute() throws Exception
+    {
+        SpaceDescription spaceDesc = this.getSpace().getDescription();
+        surveyConfig = surveyFeedbackManager.getSurveyConfig(spaceDesc);
+        surveyFeedbackManager.setSurveyConfig(spaceDesc,surveyConfig);
+        return super.doDefault();
+    }
+
+    public SurveyConfig getSurveyConfig()
+    {
+        return surveyConfig;
+    }
+}
+
+

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/config/DeactivateFeedbackQuestionAction.java

View file
+package com.atlassian.confluence.plugins.knowledgebase.config;
+
+import com.atlassian.confluence.spaces.actions.AbstractSpaceAdminAction;
+import com.atlassian.confluence.spaces.actions.SpaceAware;
+import com.atlassian.confluence.spaces.SpaceDescription;
+import com.atlassian.confluence.plugins.knowledgebase.SurveyFeedbackManager;
+import com.atlassian.sal.api.websudo.WebSudoRequired;
+
+@WebSudoRequired
+public class DeactivateFeedbackQuestionAction extends AbstractSpaceAdminAction implements SpaceAware
+{
+    private String selectedQuestion;
+    private boolean activate;
+    private SurveyFeedbackManager surveyFeedbackManager;
+
+    public String execute() throws Exception
+    {
+        SpaceDescription spaceDesc = this.getSpace().getDescription();
+        SurveyConfig surveyConfig = surveyFeedbackManager.getSurveyConfig(spaceDesc);
+
+        if (activate)
+            surveyConfig.activateQuestion(selectedQuestion);
+        else
+            surveyConfig.deactivateQuestion(selectedQuestion);
+        surveyFeedbackManager.setSurveyConfig(spaceDesc,surveyConfig);
+
+        return super.doDefault();
+    }
+
+    public void setSelectedQuestion(String questionId)
+    {
+        selectedQuestion = questionId;
+    }
+
+    public void setActivate(String activateString)
+    {
+        activate = activateString.equals("true");
+    }
+
+    public void setSurveyFeedbackManager(SurveyFeedbackManager surveyFeedbackManager)
+    {
+        this.surveyFeedbackManager = surveyFeedbackManager;
+    }
+ 
+}

File src/main/java/com/atlassian/confluence/plugins/knowledgebase/config/SurveyConfig.java

View file
+package com.atlassian.confluence.plugins.knowledgebase.config;
+
+import com.atlassian.confluence.user.AuthenticatedUserThreadLocal;
+import com.atlassian.confluence.velocity.htmlsafe.HtmlSafe;
+
+import javax.servlet.http.HttpSession;
+import java.util.*;
+
+public class SurveyConfig
+{
+    private String response;
+    private String responseUser;
+    private Map<String, SurveyQuestion> questionMap;
+    private String primaryQuestionId;
+    private String primaryQuestion;
+
+    public SurveyConfig()
+    {
+        questionMap = new HashMap<String,SurveyQuestion>();
+    }
+
+    public void setPrimaryQuestionId(String primaryQuestionId)
+    {
+        this.primaryQuestionId = primaryQuestionId;
+    }
+
+    public String getPrimaryQuestionId()
+    {
+        return primaryQuestionId;
+    }
+
+    public void setPrimaryQuestion(String primaryQuestion)
+    {
+        this.primaryQuestion = primaryQuestion;
+    }
+
+    public String getPrimaryQuestion()
+    {
+        return primaryQuestion;
+    }
+
+    public void setResponse(String r)
+    {
+        response = r;
+    }
+
+    public String getResponse(HttpSession session)
+    {
+       if (AuthenticatedUserThreadLocal.getUser() != null)
+       {
+           return getResponseUser();
+       }
+        else
+           return getResponse();
+    }
+