Add delete for spaces, check that there is no linked projects
authorymh <ymh.work@gmail.com>
Wed, 03 Apr 2013 17:47:23 +0200
changeset 129 01c862ada33c
parent 128 93a1fbe6a848
child 130 13ed4f7543d2
Add delete for spaces, check that there is no linked projects
server/pom.xml
server/src/main/java/org/iri_research/renkan/controller/AdminController.java
server/src/main/java/org/iri_research/renkan/models/Space.java
server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryCustom.java
server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryImpl.java
server/src/main/webapp/WEB-INF/i18n/messages_en.properties
server/src/main/webapp/WEB-INF/i18n/messages_fr.properties
server/src/main/webapp/WEB-INF/spring-servlet.xml
server/src/main/webapp/WEB-INF/templates/admin/spaceDeleteConfirm.html
server/src/main/webapp/WEB-INF/templates/admin/spacesList.html
server/src/main/webapp/static/css/index.css
server/src/test/java/org/iri_research/renkan/test/controller/AdminControllerTest.java
server/src/test/java/org/iri_research/renkan/test/repositories/ProjectsRepositoryTest.java
--- a/server/pom.xml	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/pom.xml	Wed Apr 03 17:47:23 2013 +0200
@@ -398,6 +398,11 @@
             <artifactId>hibernate-validator</artifactId>
             <version>4.2.0.Final</version>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.uuid</groupId>
+            <artifactId>java-uuid-generator</artifactId>
+            <version>3.1.3</version>
+        </dependency>
     </dependencies>
     <organization>
     	<name>IRI</name>
--- a/server/src/main/java/org/iri_research/renkan/controller/AdminController.java	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/main/java/org/iri_research/renkan/controller/AdminController.java	Wed Apr 03 17:47:23 2013 +0200
@@ -1,13 +1,20 @@
 package org.iri_research.renkan.controller;
 
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Map;
+
 import javax.servlet.http.HttpServletRequest;
 import javax.validation.Valid;
 
+import org.apache.commons.codec.binary.Hex;
 import org.iri_research.renkan.Constants;
 import org.iri_research.renkan.RenkanException;
 import org.iri_research.renkan.forms.SpaceForm;
 import org.iri_research.renkan.forms.SpaceFormValidator;
 import org.iri_research.renkan.models.Space;
+import org.iri_research.renkan.repositories.ProjectsRepository;
 import org.iri_research.renkan.repositories.SpacesRepository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -26,6 +33,7 @@
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.client.HttpClientErrorException;
 
 @Controller
@@ -37,6 +45,8 @@
 	
 	@Autowired
 	private SpacesRepository spacesRepository;
+	@Autowired
+	private ProjectsRepository projectsRepository;
 	
     @InitBinder(value={"space"})
     protected void initBinder(WebDataBinder binder) {
@@ -59,6 +69,7 @@
 		
 		model.addAttribute("page", page);
 		model.addAttribute("baseUrl", Utils.buildBaseUrl(request));
+		model.addAttribute("projectsCount", this.projectsRepository.getCountBySpace());
 		
 		return "admin/spacesList";
 	}
@@ -108,7 +119,78 @@
 			throw new HttpClientErrorException(HttpStatus.NOT_FOUND, "space " + spaceForm.getId() + " not found");
 		}
 		
-		return "redirect:";
+		return "redirect:/admin/spaces";
+	}
+	
+	//@RequestMapping(value="/spaces/confirmdelete/{spaceId}", method = RequestMethod.GET)
+	//public String askDeleteSpace(Model model, @PathVariable(value="spaceId") String spaceId) {
+		
+		
+	//}
+	
+	@RequestMapping(value="/spaces/delete/{spaceId}")
+	public String deleteSpace(
+			HttpServletRequest request,
+			Model model,
+			@PathVariable(value="spaceId") String spaceId,
+			@RequestParam(value="key", required=false) String key,
+			@RequestParam(value="salt", required=false) String salt) throws NoSuchAlgorithmException, RenkanException
+	{
+
+		if(spaceId == null || spaceId.length() == 0) {
+			throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Null or empty space id");
+		}
+		
+		RequestMethod method = RequestMethod.valueOf(request.getMethod());
+		
+		Map<String, Integer> nbProj = this.projectsRepository.getCountBySpace(Arrays.asList(spaceId));
+		if(nbProj.containsKey(spaceId) && nbProj.get(spaceId).intValue()>0) {
+			throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "This space have projects");
+		}
+		
+		if(RequestMethod.GET.equals(method)) {
+
+			Space space = this.spacesRepository.findOne(spaceId);
+			
+			if(space == null) {
+				throw new HttpClientErrorException(HttpStatus.NOT_FOUND, "space " + spaceId + " not found");
+			}
+			
+			SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
+			rand.setSeed(System.currentTimeMillis());
+			byte[] rawSalt = new byte[50];
+			rand.nextBytes(rawSalt);
+			String newSalt = Hex.encodeHexString(rawSalt);
+						
+			
+			model.addAttribute("spaceObj", space);
+			model.addAttribute("salt", newSalt);
+			model.addAttribute("key", space.getKey(newSalt));
+						
+			return "admin/spaceDeleteConfirm";
+		}
+		else if (RequestMethod.POST.equals(method) && key != null && !key.isEmpty() && salt != null && !salt.isEmpty()) {
+						
+			if(spaceId != null && spaceId.length() > 0) {
+				
+				Space space = this.spacesRepository.findOne(spaceId);
+				if(space != null) {
+					if(space.checkKey(key, salt)) {
+						this.spacesRepository.delete(spaceId);
+					}
+					else {
+						throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Key not ckecked");
+					}
+				}
+				
+			}
+			return "redirect:/admin/spaces";
+			
+		}
+		else {
+			throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Bad request method or parameters"); 
+		}
+		
 	}
 	
 }
--- a/server/src/main/java/org/iri_research/renkan/models/Space.java	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/main/java/org/iri_research/renkan/models/Space.java	Wed Apr 03 17:47:23 2013 +0200
@@ -1,7 +1,15 @@
 package org.iri_research.renkan.models;
 
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.Date;
 
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.codec.binary.Hex;
+import org.iri_research.renkan.Constants;
+import org.iri_research.renkan.RenkanException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.mongodb.core.mapping.Document;
 import org.springframework.data.mongodb.core.mapping.Field;
@@ -69,4 +77,48 @@
 		this.image = image;
 	}
 	
+	private String getRawKey(String salt) {		
+		StringBuffer key = new StringBuffer(salt!=null?salt+"|":"");
+		key.append(this.getId());
+		key.append('|');
+		key.append(this.getCreated().getTime());
+		return key.toString();
+	}
+		
+	public String getKey(String salt) throws RenkanException {
+		
+		String rawKey = this.getRawKey(salt);
+		
+		MessageDigest md;
+		try {
+			md = MessageDigest.getInstance("SHA-256");
+		} catch (NoSuchAlgorithmException e) {
+			throw new RenkanException("NoSuchAlgorithmException digest: " + e.getMessage(), e);
+		}
+		String key;
+		final SecretKeySpec secret_key = new SecretKeySpec(Constants.KEYHEX.getBytes(), "HmacSHA256");
+		md.update(secret_key.getEncoded());
+		try {
+			key = Hex.encodeHexString(md.digest(rawKey.getBytes("UTF-8")));
+		} catch (UnsupportedEncodingException e) {
+			throw new RenkanException("UnsupportedEncodingException digest: " + e.getMessage(), e);
+		}
+		
+		return key;
+	}
+	
+	public boolean checkKey(String key, String salt) throws RenkanException {
+		
+
+		if(key == null || key.isEmpty()) {
+			return false;
+		}
+				
+		String signature = key;
+		
+		String new_key = this.getKey(salt);
+		
+		return new_key.equals(signature);
+	}
+	
 }
\ No newline at end of file
--- a/server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryCustom.java	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryCustom.java	Wed Apr 03 17:47:23 2013 +0200
@@ -1,5 +1,6 @@
 package org.iri_research.renkan.repositories;
 
+import java.util.Collection;
 import java.util.Map;
 
 import org.iri_research.renkan.models.Project;
@@ -8,6 +9,7 @@
 	
 	public int getRevCounter(String projectId);
 	public Map<String, Integer> getCountBySpace();
+	public Map<String, Integer> getCountBySpace(Collection<String> spaceIds);
 	
 	public void deleteRecursive(String projectId);
 	public void deleteRecursive(Project project);
--- a/server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryImpl.java	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryImpl.java	Wed Apr 03 17:47:23 2013 +0200
@@ -1,6 +1,7 @@
 package org.iri_research.renkan.repositories;
 
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -14,6 +15,7 @@
 
 import org.springframework.data.mongodb.core.mapreduce.GroupBy;
 import org.springframework.data.mongodb.core.mapreduce.GroupByResults;
+import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.Update;
 
 @Component
@@ -49,11 +51,19 @@
 		}
 		return p.getRevCounter();
 	}
+	
 
 	@Override
-	public Map<String, Integer> getCountBySpace() {
+	public Map<String, Integer> getCountBySpace(Collection<String> spaceIds) {
+
+		Criteria filter = null;
+		
+		if(spaceIds != null) {
+			filter = Criteria.where("space_id").in(spaceIds);
+		}
 		
 		GroupByResults<GroupResult> groupResult = this.mongoTemplate.group(
+				filter,
 				this.mongoTemplate.getCollectionName(Project.class),
 				GroupBy.key("space_id").initialDocument("{ count: 0 }").reduceFunction("function(doc, prev) { prev.count += 1; }"),
 				GroupResult.class);
@@ -66,6 +76,12 @@
 		return res;
 		
 	}
+	
+	@Override
+	public Map<String, Integer> getCountBySpace() {
+		return this.getCountBySpace(null);
+	}
+
 
 	@Override
 	public Project copy(Project p, String newTitle) {
@@ -107,5 +123,4 @@
 			this.projectsRepository.delete(p);
 		}
 	}
-
 }
--- a/server/src/main/webapp/WEB-INF/i18n/messages_en.properties	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/main/webapp/WEB-INF/i18n/messages_en.properties	Wed Apr 03 17:47:23 2013 +0200
@@ -1,5 +1,7 @@
 
 date.format = yyyy/MM/dd HH:mm
+question.yes = yes
+question.no = no
 
 renkanIndex.renkan_exp = Create a Renkan
 renkanIndex.project_list = Renkan list
@@ -40,6 +42,8 @@
 
 renkanAdmin.space_add = Add space
 renkanAdmin.space_edit = Edit space
+renkanAdmin.space_delete = Delete space
+renkanAdmin.space_confirm_delete = Do you want to delete the space entitled "{0}" ?
 
 renkanAdmin.object_name = Name
 renkanAdmin.object_edit = Edit
--- a/server/src/main/webapp/WEB-INF/i18n/messages_fr.properties	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/main/webapp/WEB-INF/i18n/messages_fr.properties	Wed Apr 03 17:47:23 2013 +0200
@@ -1,5 +1,7 @@
 
 date.format = dd/MM/yyyy HH:mm
+question.yes = oui
+question.no = non
 
 renkanIndex.renkan_exp = Créer un Renkan
 
@@ -38,6 +40,8 @@
 renkanAdmin.space_objects_name = Espaces
 renkanAdmin.space_add = Nouvel espace
 renkanAdmin.space_edit = Edition espaces
+renkanAdmin.space_delete = Supression espace
+renkanAdmin.space_confirm_delete = Confirmez-vous l'effacement de l'espace intitulé "{0}" ?
 
 renkanAdmin.object_name = Nom
 renkanAdmin.object_edit = Modif.
--- a/server/src/main/webapp/WEB-INF/spring-servlet.xml	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/main/webapp/WEB-INF/spring-servlet.xml	Wed Apr 03 17:47:23 2013 +0200
@@ -40,6 +40,7 @@
         <property name="prefix" value="/WEB-INF/jsp/" />
         <property name="suffix" value=".jsp" />
         <property name="order" value="2" />
+        <property name="redirectHttp10Compatible" value="false" />
     </bean>
     
     <bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
@@ -59,6 +60,7 @@
         <property name="order" value="1" />
         <!--property name="viewNames" value="*.html,*.xhtml" /-->
         <property name="characterEncoding" value="UTF-8"/>
+        <property name="redirectHttp10Compatible" value="false" />
     </bean>
     
     <bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="messageSource">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/main/webapp/WEB-INF/templates/admin/spaceDeleteConfirm.html	Wed Apr 03 17:47:23 2013 +0200
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" >
+  <head>
+    <title>Renkan Admin - edit space</title>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
+
+    <link rel="shortcut icon" href="../../../static/img/favicon.ico" th:href="@{/static/img/favicon.ico}" />
+
+    <link href="../../../static/css/style.css" rel="stylesheet" th:href="@{/static/css/style.css}"/>
+    <link href="../../../static/css/index.css" rel="stylesheet" th:href="@{/static/css/index.css}"/>
+        
+    <script th:remove="all" type="text/javascript" src="../../../static/lib/jquery.min.js" th:src="@{/static/lib/jquery.min.js}" ></script>
+    <script th:remove="all" type="text/javascript" src="../../../static/js/thymol.js"></script>
+  </head>
+  <body>
+    <div id="container">
+      <div id="wrapper">
+        <h1><a href="renkanIndex.html" th:href="@{/admin}" th:text="#{renkanAdmin.renkan_admin}" id="home-link">Renkan administration</a></h1>
+        <h2><a href="spacesList.html" th:href="@{/admin/spaces}" th:text="#{renkanAdmin.object_list(#{renkanAdmin.space_objects_name})}">Spaces List</a>&nbsp;/&nbsp;<span th:text="#{renkanAdmin.space_delete}">Delete space</span></h2>
+        <div id="space-delete-container">
+            <div id="space-delete-question" th:text="#{renkanAdmin.space_confirm_delete(${spaceObj.title})}">Do you want to delete space with title</div>
+            <div id="space-delete-confirm-buttons"><form action="spacesList.html" th:action="@{'/admin/spaces/delete/'+${spaceObj.id}(key=${key},salt=${salt})}" method="post" id="yes-form"><input type="submit" name="ok" value="yes" th:value="#{question.yes}" id="yes-button"/></form><form action="spacesList.html" method="get" th:action="@{/admin/spaces}" id="no-form" onsubmit="return false"><input type="submit" name="ok" value="no" th:value="#{question.no}" id="no-button" onclick="window.location.href='spacesList.html'" th:onclick="'window.location.href=\''+@{/admin/spaces}+'\''"/></form></div>
+        </div>
+      </div>
+      <footer id="footer" th:substituteby="fragment/pageFragment::footerFragment">
+        <div id="version">© <span class="version-date">2013</span> <a href="http://www.iri.centrepompidou.fr" target="_blanck">IRI</a> - Version <span class="version-version">0.0</span></div>
+      </footer>      
+    </div>
+  </body>
+</html>
\ No newline at end of file
--- a/server/src/main/webapp/WEB-INF/templates/admin/spacesList.html	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/main/webapp/WEB-INF/templates/admin/spacesList.html	Wed Apr 03 17:47:23 2013 +0200
@@ -43,16 +43,18 @@
               <tr>
                 <th th:text="#{renkanAdmin.object_name}" class="spaces-table-title">Name</th>
                 <th th:text="#{renkanAdmin.object_name}" class="spaces-table-created">Created</th>
+                <th th:text="#{renkanIndex.space_proj_count}">Project count</th>
                 <th th:text="#{renkanAdmin.object_edit}" class="spaces-table-actions">Edit</th>
                 <th th:text="#{renkanAdmin.object_delete}" class="spaces-table-actions">Delete</th>
               </tr>
             </thead>
             <tbody>
-              <tr th:each="object: ${page.content}">
+              <tr th:each="object: ${page.content}" th:with="spaceProjCount=${#maps.containsKey(projectsCount, object.id)}? ${projectsCount[object.id]} : 0">
                 <td th:text="${object.title}" class="spaces-table-title" >title</td>
                 <td th:text="${object.created}?${#dates.format(object.created, #messages.msg('date.format'))}:'n/a'" class="spaces-table-created">created</td>
+                <td th:text="${spaceProjCount}">nb. proj</td>
                 <td><a href="spaceEdit.html" th:href="@{'/admin/spaces/edit/'+${object.id}}" th:text="#{renkanAdmin.object_edit_link}" class="spaces-table-actions">Edit</a></td>
-                <td><a href="#" th:href="@{'/s/'+${object.id}}" th:text="#{renkanAdmin.object_delete_link}" class="spaces-table-actions">Delete</a></td>
+                <td><a href="#" th:if="${spaceProjCount==0}" th:href="@{'/admin/spaces/delete/'+${object.id}}" th:text="#{renkanAdmin.object_delete_link}" class="spaces-table-actions">Delete</a><span th:if="${spaceProjCount>0}" class="spaces-table-actions spaces-table-actions-disabled" th:text="#{renkanAdmin.object_delete_link}">Delete</span></td>
               </tr>
             </tbody>
           </table>
--- a/server/src/main/webapp/static/css/index.css	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/main/webapp/static/css/index.css	Wed Apr 03 17:47:23 2013 +0200
@@ -212,6 +212,12 @@
     width: 40px;
 }
 
+.spaces-table-actions-disabled, .spaces-table-actions-disabled:link, .spaces-table-actions-disabled:visited, .spaces-table-actions-disabled:hover, .spaces-table-actions-disabled:active, .spaces-table-actions-disabled:focus {
+	color: gray;
+	text-decoration: none;
+	cursor: default;
+}
+
 td.spaces-table-created {
 	text-align: center;
 }
@@ -247,6 +253,27 @@
     width: 650px;
     height: 150px;
 }
+
 #binConfigDiv div {
     margin-bottom: 0; 
 }
+
+#space-delete-container {
+	margin-left: 12px;
+	margin-top: 1em;
+}
+
+
+#space-delete-confirm-buttons {
+	margin-top: 1em;
+}
+
+#space-delete-confirm-buttons form {
+	margin: 0;
+	padding: 0;
+	display: inline;
+}
+
+#space-delete-confirm-buttons input[type=submit] {
+	margin-right: 12px;
+}
--- a/server/src/test/java/org/iri_research/renkan/test/controller/AdminControllerTest.java	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/test/java/org/iri_research/renkan/test/controller/AdminControllerTest.java	Wed Apr 03 17:47:23 2013 +0200
@@ -1,5 +1,6 @@
 package org.iri_research.renkan.test.controller;
 
+import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
@@ -7,7 +8,10 @@
 import java.util.Map;
 import java.util.UUID;
 
+import org.apache.commons.codec.binary.Hex;
+import org.iri_research.renkan.models.Project;
 import org.iri_research.renkan.models.Space;
+import org.iri_research.renkan.repositories.ProjectsRepository;
 import org.iri_research.renkan.repositories.SpacesRepository;
 import org.junit.After;
 import org.junit.Assert;
@@ -17,6 +21,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.test.context.web.WebAppConfiguration;
@@ -26,7 +31,9 @@
 import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
 import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.util.NestedServletException;
 
 @RunWith(SpringJUnit4ClassRunner.class)
 @WebAppConfiguration
@@ -39,10 +46,14 @@
 	
 	@Autowired
 	private SpacesRepository spacesRepository;
+	@Autowired
+	private ProjectsRepository projectsRepository;
 	
 	private Map<String, Space> spacesList = new HashMap<String, Space>(SPACE_NB);
 	private List<String> spacesUuids = new ArrayList<>(SPACE_NB);
 	
+	private ArrayList<Project> testProjects = new ArrayList<Project>();
+	
 	@Autowired
 	private WebApplicationContext context;
 	private MockMvc mvc;
@@ -53,6 +64,9 @@
 		
 		logger.debug("Setup");
 		spacesRepository.deleteAll();
+		projectsRepository.deleteAll();
+		
+		ArrayList<Project> pl = new ArrayList<Project>();
 		for(int i=0;i<SPACE_NB;i++) {
 			Date creationDate = new Date();
 			String uuid = UUID.randomUUID().toString();
@@ -60,6 +74,9 @@
 			Space testSpace = new Space(uuid, "test " + i, "Test space " + 1, "{}", "http://ldt.iri.centrepompidou.fr", "#ababab", "test_user", "http://ldt.iri.centrepompidou.fr", creationDate);
 			testSpace = spacesRepository.save(testSpace);
 			this.spacesList.put(uuid, testSpace);
+			for(int j=0; j<SPACE_NB-1-i; j++) {
+				pl.add(new Project(testSpace.getId(), UUID.randomUUID().toString(), "test"+((SPACE_NB-1)*i+j+1), "desc"+((SPACE_NB-1)*i+j+1), "http://localhost:8080/rest/projects/id"+((SPACE_NB-1)*i+j+1), creationDate));
+			}
 			try {
 				Thread.sleep(1);
 			} catch (InterruptedException e) {
@@ -67,9 +84,20 @@
 			}
 		}
 		
+		for(Project p: projectsRepository.save(pl)) {
+			this.testProjects.add(p);
+		}
+		
 		this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build();
 	}
 	
+	@After
+	public void teardown() {
+		spacesRepository.deleteAll();
+		projectsRepository.deleteAll();
+	}
+
+	
 	@Test
 	public void testContext() throws Exception {
 		MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/");
@@ -93,8 +121,8 @@
 		post = post.param("binConfig", "{}");
 		
 		this.mvc.perform(post)
-				.andExpect(MockMvcResultMatchers.status().isMovedTemporarily())
-				.andExpect(MockMvcResultMatchers.redirectedUrl(""));
+				.andExpect(MockMvcResultMatchers.status().isSeeOther())
+				.andExpect(MockMvcResultMatchers.redirectedUrl("/admin/spaces"));
 		
 		Space sp = this.spacesRepository.findOne(this.spacesUuids.get(0));
 		
@@ -118,8 +146,8 @@
 		post = post.param("binConfig", "{}");
 		
 		this.mvc.perform(post)
-				.andExpect(MockMvcResultMatchers.status().isMovedTemporarily())
-				.andExpect(MockMvcResultMatchers.redirectedUrl(""));
+				.andExpect(MockMvcResultMatchers.status().isSeeOther())
+				.andExpect(MockMvcResultMatchers.redirectedUrl("/admin/spaces"));
 		
 		
 		Assert.assertEquals("Must have one more space", SPACE_NB + 1, this.spacesRepository.count());
@@ -288,10 +316,164 @@
 		
 	}
 	
+	@Test
+	public void testDeleteSpace() throws Exception {
+		
+		MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/admin/spaces/delete/"+this.spacesUuids.get(SPACE_NB-1));
+		
+		MvcResult res = this.mvc.perform(get)
+			.andExpect(MockMvcResultMatchers.status().isOk())			
+			.andExpect(MockMvcResultMatchers.view().name("admin/spaceDeleteConfirm"))
+			.andExpect(MockMvcResultMatchers.model().attributeExists("spaceObj", "key", "salt"))
+			.andReturn();
+		
+		Map<String, Object> model = res.getModelAndView().getModel();
+		
+		Space space = (Space)model.get("spaceObj");
+		Assert.assertNotNull("Space is not null", space);
+		Assert.assertEquals("Must be first space id", this.spacesUuids.get(SPACE_NB-1), space.getId());
+		
+		String key = (String)model.get("key");
+		Assert.assertNotNull("key is not null", key);
+
+		String salt = (String)model.get("salt");
+		Assert.assertNotNull("salt is not null", salt);
+		
+		Assert.assertTrue("Key must be checked", space.checkKey(key, salt));
+		
+	}
+
+	@Test
+	public void testDeleteFakeSpace() throws Exception {
+		
+		MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/admin/spaces/delete/" + UUID.randomUUID().toString());
+		
+		try {
+			this.mvc.perform(get)
+				.andExpect(MockMvcResultMatchers.status().isNotFound());
+		}
+		catch(NestedServletException e) {
+			Assert.assertNotNull("Nested exception must not be null", e.getCause());
+			Assert.assertEquals("Inner exception must be a HttpClientErrorException", HttpClientErrorException.class, e.getCause().getClass());
+			Assert.assertEquals("Exception error status must be not found", HttpStatus.NOT_FOUND, ((HttpClientErrorException)e.getCause()).getStatusCode());
+		}
+		
+	}
 	
-	@After
-	public void teardown() {
-		spacesRepository.deleteAll();
+	@Test
+	public void testDeleteSpaceProject() throws Exception {
+		
+		MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/admin/spaces/delete/" + this.spacesUuids.get(0));
+		
+		try {
+			this.mvc.perform(get)
+				.andExpect(MockMvcResultMatchers.status().isBadRequest());
+		}
+		catch(NestedServletException e) {
+			Assert.assertNotNull("Nested exception must not be null", e.getCause());
+			Assert.assertEquals("Inner exception must be a HttpClientErrorException", HttpClientErrorException.class, e.getCause().getClass());
+			Assert.assertEquals("Exception error status must be not found", HttpStatus.BAD_REQUEST, ((HttpClientErrorException)e.getCause()).getStatusCode());
+		}
+		
+	}
+	
+	@Test
+	public void testDoDeleteSpaceNoKey() throws Exception {
+		MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post("/admin/spaces/delete/"+this.spacesUuids.get(SPACE_NB-1));
+		
+		try {
+			this.mvc.perform(post)
+				.andExpect(MockMvcResultMatchers.status().isBadRequest());
+		}
+		catch(NestedServletException e) {
+			Assert.assertNotNull("Nested exception must not be null", e.getCause());
+			Assert.assertEquals("Inner exception must be a HttpClientErrorException", HttpClientErrorException.class, e.getCause().getClass());
+			Assert.assertEquals("Exception error status must be not found", HttpStatus.BAD_REQUEST, ((HttpClientErrorException)e.getCause()).getStatusCode());
+		}
+
+		
+		Assert.assertEquals("Must have same nb of space", SPACE_NB, this.spacesRepository.count());
+		
+	}
+	
+	@Test
+	public void testDoDeleteSpace() throws Exception {
+		
+		Space space = this.spacesList.get(this.spacesUuids.get(SPACE_NB-1));
+		
+		SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
+		rand.setSeed(System.currentTimeMillis());
+		byte[] rawSalt = new byte[50];
+		rand.nextBytes(rawSalt);
+		String salt = Hex.encodeHexString(rawSalt);
+		String key = space.getKey(salt);
+		
+		
+		MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post(String.format("/admin/spaces/delete/%s?key=%s&salt=%s",this.spacesUuids.get(SPACE_NB-1), key, salt));
+		
+		this.mvc.perform(post)
+			.andExpect(MockMvcResultMatchers.status().isSeeOther())
+			.andExpect(MockMvcResultMatchers.redirectedUrl("/admin/spaces"));
+		
+		Assert.assertEquals("Must have one less space", SPACE_NB-1, this.spacesRepository.count());
+		
+		space = this.spacesRepository.findOne(this.spacesUuids.get(SPACE_NB-1));
+		
+		Assert.assertNull("Space " + this.spacesUuids.get(SPACE_NB-1) + " deleted", space);
+		
+	}
+
+	
+	@Test
+	public void testDoDeleteSpaceFake() throws Exception {
+		
+		Space space = this.spacesList.get(this.spacesUuids.get(SPACE_NB-1));
+		
+		SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
+		rand.setSeed(System.currentTimeMillis());
+		byte[] rawSalt = new byte[50];
+		rand.nextBytes(rawSalt);
+		String salt = Hex.encodeHexString(rawSalt);
+		String key = space.getKey(salt);
+		
+		
+		MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post(String.format("/admin/spaces/delete/%s?key=%s&salt=%s",UUID.randomUUID(), key, salt));
+		
+		this.mvc.perform(post)
+			.andExpect(MockMvcResultMatchers.status().isSeeOther())
+			.andExpect(MockMvcResultMatchers.redirectedUrl("/admin/spaces"));			
+		
+		Assert.assertEquals("Must have the same nb of space", SPACE_NB, this.spacesRepository.count());
+				
+	}
+
+	@Test
+	public void testDoDeleteSpaceProject() throws Exception {
+		
+		Space space = this.spacesList.get(this.spacesUuids.get(0));
+		
+		SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
+		rand.setSeed(System.currentTimeMillis());
+		byte[] rawSalt = new byte[50];
+		rand.nextBytes(rawSalt);
+		String salt = Hex.encodeHexString(rawSalt);
+		String key = space.getKey(salt);
+		
+		
+		MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post(String.format("/admin/spaces/delete/%s?key=%s&salt=%s",this.spacesUuids.get(0), key, salt));
+		
+		try {
+			this.mvc.perform(post)
+				.andExpect(MockMvcResultMatchers.status().isBadRequest());
+		}
+		catch(NestedServletException e) {
+			Assert.assertNotNull("Nested exception must not be null", e.getCause());
+			Assert.assertEquals("Inner exception must be a HttpClientErrorException", HttpClientErrorException.class, e.getCause().getClass());
+			Assert.assertEquals("Exception error status must be not found", HttpStatus.BAD_REQUEST, ((HttpClientErrorException)e.getCause()).getStatusCode());
+		}
+		
+		Assert.assertEquals("Must have the same nb of space", SPACE_NB, this.spacesRepository.count());
+				
 	}
 
 }
--- a/server/src/test/java/org/iri_research/renkan/test/repositories/ProjectsRepositoryTest.java	Tue Apr 02 14:05:56 2013 +0200
+++ b/server/src/test/java/org/iri_research/renkan/test/repositories/ProjectsRepositoryTest.java	Wed Apr 03 17:47:23 2013 +0200
@@ -2,6 +2,7 @@
 
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -206,6 +207,25 @@
 	}
 	
 	@Test
+	public void testGetCountBySpaceFilter() {
+		
+		List<String> spacesIdsFilter = Arrays.asList(this.spaceIds.get(0));
+		
+		Map<String, Integer> groupRes = projectsRepository.getCountBySpace(spacesIdsFilter);
+		
+		Assert.assertNotNull("GroupRes not null", groupRes);
+		Assert.assertEquals("Group res size", 1, groupRes.size());
+		
+		Integer count = groupRes.get(this.spaceIds.get(0));
+		Assert.assertNotNull("count not null", count);
+		Assert.assertEquals("Nb of project/space", 2, count.intValue());
+		
+		for(int i=1; i<SPACE_NB; i++) {
+			Assert.assertNull("other space id has no project i.e count is null", groupRes.get(this.spaceIds.get(i)));
+		}
+	}
+	
+	@Test
 	public void testCopyProjectCreation() {
 		DBCollection coll = mongoTemplate.getCollection(mongoTemplate.getCollectionName(Project.class));