From a55c994cca64a97168dae3df1c91ec433d15d9fd Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Thu, 9 Jul 2015 08:24:04 +0200
Subject: [PATCH] no-jira: selenium test which test a password booking method
 on a course

---
 .../course/run/CourseRuntimeController.java   |  4 +-
 .../ui/main/AbstractMemberListController.java |  1 +
 .../ui/RepositoryEntryRuntimeController.java  |  7 +-
 .../author/AuthoringEditAccessController.java |  5 ++
 .../manager/ACFrontendManager.java            |  3 -
 .../ui/AccessConfigurationController.java     |  1 +
 .../accesscontrol/ui/OrdersController.java    |  1 -
 .../accesscontrol/ui/_content/orders.html     | 14 +--
 .../java/org/olat/selenium/CourseTest.java    | 90 +++++++++++++++++++
 .../olat/selenium/page/core/BookingPage.java  | 27 ++++++
 .../page/course/CoursePageFragment.java       | 11 +++
 .../selenium/page/course/MembersPage.java     | 24 +++++
 .../selenium/page/course/MyCoursesPage.java   | 23 +++++
 .../page/repository/RepositoryAccessPage.java | 11 +++
 .../RepositoryEditDescriptionPage.java        |  1 +
 15 files changed, 207 insertions(+), 16 deletions(-)

diff --git a/src/main/java/org/olat/course/run/CourseRuntimeController.java b/src/main/java/org/olat/course/run/CourseRuntimeController.java
index 147baf873dc..77475fc251d 100644
--- a/src/main/java/org/olat/course/run/CourseRuntimeController.java
+++ b/src/main/java/org/olat/course/run/CourseRuntimeController.java
@@ -89,7 +89,6 @@ import org.olat.course.assessment.AssessmentChangedEvent;
 import org.olat.course.assessment.AssessmentMainController;
 import org.olat.course.assessment.AssessmentModule;
 import org.olat.course.assessment.CoachingGroupAccessAssessmentCallback;
-import org.olat.course.assessment.EfficiencyStatementManager;
 import org.olat.course.assessment.FullAccessAssessmentCallback;
 import org.olat.course.assessment.ui.AssessmentModeListController;
 import org.olat.course.certificate.ui.CertificateAndEfficiencyStatementController;
@@ -191,8 +190,6 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 	private BusinessGroupService businessGroupService;
 	@Autowired
 	private AssessmentModule assessmentModule;
-	@Autowired
-	private EfficiencyStatementManager efficiencyStatementManager;
 	
 	public CourseRuntimeController(UserRequest ureq, WindowControl wControl,
 			RepositoryEntry re, RepositoryEntrySecurity reSecurity, RuntimeControllerCreator runtimeControllerCreator,
@@ -443,6 +440,7 @@ public class CourseRuntimeController extends RepositoryEntryRuntimeController im
 			
 			ordersLink = LinkFactory.createToolLink("bookings", translate("details.orders"), this, "o_sel_repo_booking");
 			ordersLink.setIconLeftCSS("o_icon o_icon-fw o_icon_booking");
+			ordersLink.setElementCssClass("o_sel_course_ac_tool");
 			boolean booking = acService.isResourceAccessControled(getRepositoryEntry().getOlatResource(), null);
 			ordersLink.setVisible(!corrupted && booking);
 			tools.addComponent(ordersLink);
diff --git a/src/main/java/org/olat/group/ui/main/AbstractMemberListController.java b/src/main/java/org/olat/group/ui/main/AbstractMemberListController.java
index e459f8a9ddc..be661066c35 100644
--- a/src/main/java/org/olat/group/ui/main/AbstractMemberListController.java
+++ b/src/main/java/org/olat/group/ui/main/AbstractMemberListController.java
@@ -217,6 +217,7 @@ public abstract class AbstractMemberListController extends FormBasicController i
 		membersTable.setAndLoadPersistedPreferences(ureq, this.getClass().getSimpleName());
 		membersTable.setSearchEnabled(true);
 		membersTable.setExportEnabled(true);
+		membersTable.setElementCssClass("o_sel_member_list");
 
 		if(!globallyManaged) {
 			editButton = uifactory.addFormLink("edit.members", formLayout, Link.BUTTON);
diff --git a/src/main/java/org/olat/repository/ui/RepositoryEntryRuntimeController.java b/src/main/java/org/olat/repository/ui/RepositoryEntryRuntimeController.java
index b8b2d68340c..c1bb068372d 100644
--- a/src/main/java/org/olat/repository/ui/RepositoryEntryRuntimeController.java
+++ b/src/main/java/org/olat/repository/ui/RepositoryEntryRuntimeController.java
@@ -61,7 +61,6 @@ import org.olat.core.util.resource.OresHelper;
 import org.olat.course.CourseModule;
 import org.olat.course.assessment.AssessmentMode;
 import org.olat.course.assessment.AssessmentModeManager;
-import org.olat.course.assessment.AssessmentModule;
 import org.olat.course.assessment.model.TransientAssessmentMode;
 import org.olat.repository.RepositoryEntry;
 import org.olat.repository.RepositoryEntryManagedFlag;
@@ -153,8 +152,6 @@ public class RepositoryEntryRuntimeController extends MainLayoutBasicController
 	@Autowired
 	protected MarkManager markManager;
 	@Autowired
-	private AssessmentModule assessmentModule;
-	@Autowired
 	protected RepositoryModule repositoryModule;
 	@Autowired
 	private RepositoryService repositoryService;
@@ -530,6 +527,10 @@ public class RepositoryEntryRuntimeController extends MainLayoutBasicController
 		} else if(accessCtrl == source) {
 			if(event == Event.CHANGED_EVENT) {
 				re = accessCtrl.getEntry();
+				if(ordersLink != null) {
+					boolean booking = acService.isResourceAccessControled(re.getOlatResource(), null);
+					ordersLink.setVisible(!corrupted && booking);
+				}
 			}
 		} else if(descriptionCtrl == source) {
 			if(event == Event.CHANGED_EVENT) {
diff --git a/src/main/java/org/olat/repository/ui/author/AuthoringEditAccessController.java b/src/main/java/org/olat/repository/ui/author/AuthoringEditAccessController.java
index 5f25c07f9ec..435f8cabb81 100644
--- a/src/main/java/org/olat/repository/ui/author/AuthoringEditAccessController.java
+++ b/src/main/java/org/olat/repository/ui/author/AuthoringEditAccessController.java
@@ -76,6 +76,7 @@ public class AuthoringEditAccessController extends BasicController {
 
 		boolean managedBookings = RepositoryEntryManagedFlag.isManaged(entry, RepositoryEntryManagedFlag.bookings);
 		acCtr = new AccessConfigurationController(ureq, getWindowControl(), entry.getOlatResource(), entry.getDisplayname(), true, !managedBookings);
+		listenTo(acCtr);
 		int access = propPupForm.getAccess();
 		int numOfBookingConfigs = acCtr.getNumOfBookingConfigurations();
 		if(access == RepositoryEntry.ACC_USERS || access == RepositoryEntry.ACC_USERS_GUESTS) {
@@ -140,6 +141,10 @@ public class AuthoringEditAccessController extends BasicController {
 				CoordinatorManager.getInstance().getCoordinator().getEventBus().fireEventToListenersOf(modifiedEvent, entry);	
 				fireEvent(ureq, Event.CHANGED_EVENT);
 			}
+		} else if(acCtr == source) {
+			if(event == Event.CHANGED_EVENT) {
+				fireEvent(ureq, Event.CHANGED_EVENT);
+			}
 		}
 	}
 }
diff --git a/src/main/java/org/olat/resource/accesscontrol/manager/ACFrontendManager.java b/src/main/java/org/olat/resource/accesscontrol/manager/ACFrontendManager.java
index d8b45c5db3c..312bdcaa271 100644
--- a/src/main/java/org/olat/resource/accesscontrol/manager/ACFrontendManager.java
+++ b/src/main/java/org/olat/resource/accesscontrol/manager/ACFrontendManager.java
@@ -31,7 +31,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import org.olat.basesecurity.BaseSecurity;
 import org.olat.basesecurity.GroupRoles;
 import org.olat.core.commons.persistence.DB;
 import org.olat.core.id.Identity;
@@ -87,8 +86,6 @@ public class ACFrontendManager implements ACService {
 	@Autowired
 	private DB dbInstance;
 	@Autowired
-	private BaseSecurity securityManager;
-	@Autowired
 	private RepositoryManager repositoryManager;
 	@Autowired
 	private RepositoryService repositoryService;
diff --git a/src/main/java/org/olat/resource/accesscontrol/ui/AccessConfigurationController.java b/src/main/java/org/olat/resource/accesscontrol/ui/AccessConfigurationController.java
index 6d0155058ea..605d53aeaf9 100644
--- a/src/main/java/org/olat/resource/accesscontrol/ui/AccessConfigurationController.java
+++ b/src/main/java/org/olat/resource/accesscontrol/ui/AccessConfigurationController.java
@@ -241,6 +241,7 @@ public class AccessConfigurationController extends FormBasicController {
 				AccessInfo infos = (AccessInfo)source.getUserObject();
 				acService.deleteOffer(infos.getLink().getOffer());
 				confControllers.remove(infos);
+				fireEvent(ureq, Event.CHANGED_EVENT);
 			} else if("edit".equals(cmd)) {
 				AccessInfo infos = (AccessInfo)source.getUserObject();
 				editMethod(ureq, infos);
diff --git a/src/main/java/org/olat/resource/accesscontrol/ui/OrdersController.java b/src/main/java/org/olat/resource/accesscontrol/ui/OrdersController.java
index 3b960dbd287..28b93708ab0 100644
--- a/src/main/java/org/olat/resource/accesscontrol/ui/OrdersController.java
+++ b/src/main/java/org/olat/resource/accesscontrol/ui/OrdersController.java
@@ -111,7 +111,6 @@ public class OrdersController extends BasicController implements Activateable2 {
 		tableCtr.addColumnDescriptor(new DefaultColumnDescriptor("order.total", Col.total.ordinal(), null, getLocale()));
 		
 		tableCtr.addColumnDescriptor(new StaticColumnDescriptor(CMD_SELECT, "table.order.details", getTranslator().translate("order.details")));
-		
 		listenTo(tableCtr);
 		
 		loadModel();
diff --git a/src/main/java/org/olat/resource/accesscontrol/ui/_content/orders.html b/src/main/java/org/olat/resource/accesscontrol/ui/_content/orders.html
index 07c5f65bf4c..f77d933b39a 100644
--- a/src/main/java/org/olat/resource/accesscontrol/ui/_content/orders.html
+++ b/src/main/java/org/olat/resource/accesscontrol/ui/_content/orders.html
@@ -1,6 +1,8 @@
-<h4><i class="o_icon o_icon_booking"> </i> $title</h4>
-<div class="o_info">$description</div>
-#if($r.available("searchForm"))
-	$r.render("searchForm")
-#end
-$r.render("orderList")
\ No newline at end of file
+<div class="o_sel_order_list">
+	<h4><i class="o_icon o_icon_booking"> </i> $title</h4>
+	<div class="o_info">$description</div>
+	#if($r.available("searchForm"))
+		$r.render("searchForm")
+	#end
+	$r.render("orderList")
+</div>
\ No newline at end of file
diff --git a/src/test/java/org/olat/selenium/CourseTest.java b/src/test/java/org/olat/selenium/CourseTest.java
index 9f32be5ff77..7bbe0b63396 100644
--- a/src/test/java/org/olat/selenium/CourseTest.java
+++ b/src/test/java/org/olat/selenium/CourseTest.java
@@ -41,6 +41,7 @@ import org.olat.selenium.page.LoginPage;
 import org.olat.selenium.page.NavigationPage;
 import org.olat.selenium.page.Participant;
 import org.olat.selenium.page.User;
+import org.olat.selenium.page.core.BookingPage;
 import org.olat.selenium.page.course.CourseEditorPageFragment;
 import org.olat.selenium.page.course.CoursePageFragment;
 import org.olat.selenium.page.course.CourseWizardPage;
@@ -50,7 +51,9 @@ import org.olat.selenium.page.course.PublisherPageFragment.Access;
 import org.olat.selenium.page.graphene.OOGraphene;
 import org.olat.selenium.page.repository.AuthoringEnvPage;
 import org.olat.selenium.page.repository.FeedPage;
+import org.olat.selenium.page.repository.RepositoryAccessPage;
 import org.olat.selenium.page.repository.AuthoringEnvPage.ResourceType;
+import org.olat.selenium.page.repository.RepositoryAccessPage.UserAccess;
 import org.olat.selenium.page.repository.RepositoryEditDescriptionPage;
 import org.olat.test.ArquillianDeployments;
 import org.olat.test.rest.UserRestClient;
@@ -820,4 +823,91 @@ public class CourseTest {
 		int numOfSurvivingMessages = infoMsgConfig.countMessages();
 		Assert.assertEquals(3, numOfSurvivingMessages);
 	}
+	
+	/**
+	 * An author creates a course, make it visible for
+	 * members and add an access control by password.
+	 * The user search for the course, books it and give
+	 * the password.<br/>
+	 * The author checks in the list of orders if the booking
+	 * of the user is there and after it checks if the user is
+	 * in the member list too.
+	 * 
+	 * @param loginPage
+	 * @param ryomouBrowser
+	 * @throws IOException
+	 * @throws URISyntaxException
+	 */
+	@Test
+	@RunAsClient
+	public void courseBooking(@InitialPage LoginPage loginPage,
+			@Drone @User WebDriver ryomouBrowser)
+	throws IOException, URISyntaxException {
+		UserVO author = new UserRestClient(deploymentUrl).createAuthor();
+		loginPage.loginAs(author.getLogin(), author.getPassword());
+		UserVO ryomou = new UserRestClient(deploymentUrl).createRandomUser("Ryomou");
+		
+		//go to authoring
+		AuthoringEnvPage authoringEnv = navBar
+			.assertOnNavigationPage()
+			.openAuthoringEnvironment();
+		
+		String title = "Create-Selen-" + UUID.randomUUID().toString();
+		//create course
+		authoringEnv
+			.openCreateDropDown()
+			.clickCreate(ResourceType.course)
+			.fillCreateForm(title)
+			.assertOnGeneralTab();
+
+		//open course editor
+		CoursePageFragment course = new CoursePageFragment(browser);
+		RepositoryAccessPage courseAccess = course
+			.openToolsMenu()
+			.edit()
+			.createNode("info")
+			.autoPublish()
+			.accessConfiguration()
+			.setUserAccess(UserAccess.registred);
+		//add booking by secret token
+		courseAccess
+			.boooking()
+			.openAddDropMenu()
+			.addTokenMethod()
+			.configureTokenMethod("secret", "The password is secret");
+		courseAccess
+			.clickToolbarBack();
+		
+		//a user search the course
+		LoginPage ryomouLoginPage = LoginPage.getLoginPage(ryomouBrowser, deploymentUrl);
+		ryomouLoginPage
+			.loginAs(ryomou.getLogin(), ryomou.getPassword())
+			.resume();
+		NavigationPage ryomouNavBar = new NavigationPage(ryomouBrowser);
+		ryomouNavBar
+			.openMyCourses()
+			.openSearch()
+			.extendedSearch(title)
+			.book(title);
+		//book the course
+		BookingPage booking = new BookingPage(ryomouBrowser);
+		booking
+			.bookToken("secret");
+		//check the course
+		CoursePageFragment bookedCourse = CoursePageFragment.getCourse(ryomouBrowser);
+		bookedCourse
+			.assertOnTitle(title);
+		
+		//Author go in the list of bookings of the course
+		BookingPage bookingList = course
+			.openToolsMenu()
+			.bookingTool();
+		bookingList
+			.assertFirstNameInListIsOk(ryomou);
+		
+		//Author go to members list
+		course
+			.members()
+			.assertFirstNameInList(ryomou);
+	}
 }
diff --git a/src/test/java/org/olat/selenium/page/core/BookingPage.java b/src/test/java/org/olat/selenium/page/core/BookingPage.java
index 19fd73e7dd4..7b15d3999b5 100644
--- a/src/test/java/org/olat/selenium/page/core/BookingPage.java
+++ b/src/test/java/org/olat/selenium/page/core/BookingPage.java
@@ -23,6 +23,7 @@ import java.util.List;
 
 import org.junit.Assert;
 import org.olat.selenium.page.graphene.OOGraphene;
+import org.olat.user.restapi.UserVO;
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.WebElement;
@@ -120,4 +121,30 @@ public class BookingPage {
 		browser.findElement(saveButtonBy).click();
 		OOGraphene.waitBusy(browser);
 	}
+	
+	/**
+	 * Check if a the booking of a user is in the list
+	 * of orders. The assert check by first name and
+	 * if the order is ok.
+	 * 
+	 * @param user
+	 * @return
+	 */
+	public BookingPage assertFirstNameInListIsOk(UserVO user) {
+		By firstNameBy = By.xpath("//td[contains(text(),'" + user.getFirstName() + "')]");
+		By okBy = By.className("o_ac_order_status_payed_icon");
+		By rowBy = By.cssSelector(".o_sel_order_list table.o_table.table tr");
+		boolean found = false;
+		List<WebElement> rows = browser.findElements(rowBy);
+		for(WebElement row:rows) {
+			List<WebElement> firstNameEl = row.findElements(firstNameBy);
+			List<WebElement> okEl = row.findElements(okBy);
+			if(firstNameEl.size() == 1 && okEl.size() == 1) {
+				found = true;
+				break;
+			}
+		}
+		Assert.assertTrue(found);
+		return this;
+	}
 }
diff --git a/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java b/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java
index c5edf45a7f5..026b30d1aa2 100644
--- a/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java
+++ b/src/test/java/org/olat/selenium/page/course/CoursePageFragment.java
@@ -26,6 +26,7 @@ import org.jboss.arquillian.drone.api.annotation.Drone;
 import org.jboss.arquillian.graphene.Graphene;
 import org.junit.Assert;
 import org.olat.restapi.support.vo.CourseVO;
+import org.olat.selenium.page.core.BookingPage;
 import org.olat.selenium.page.core.MenuTreePageFragment;
 import org.olat.selenium.page.graphene.OOGraphene;
 import org.olat.selenium.page.repository.RepositoryAccessPage;
@@ -50,6 +51,7 @@ public class CoursePageFragment {
 	
 	public static final By editCourseBy = By.className("o_sel_course_editor");
 	public static final By accessConfigBy = By.className("o_sel_course_access");
+	public static final By bookingBy = By.className("o_sel_course_ac_tool");
 	public static final By assessmentToolBy = By.className("o_sel_course_assessment_tool");
 	public static final By assessmentModeBy = By.className("o_sel_course_assessment_mode");
 	public static final By membersCourseBy = By.className("o_sel_course_members");
@@ -201,4 +203,13 @@ public class CoursePageFragment {
 		WebElement main = browser.findElement(By.id("o_main_container"));
 		return Graphene.createPageFragment(EfficiencyStatementConfigurationPage.class, main);
 	}
+	
+	public BookingPage bookingTool() {
+		if(!browser.findElement(toolsMenu).isDisplayed()) {
+			openToolsMenu();
+		}
+		browser.findElement(bookingBy).click();
+		OOGraphene.waitBusy(browser);
+		return new BookingPage(browser);
+	}
 }
diff --git a/src/test/java/org/olat/selenium/page/course/MembersPage.java b/src/test/java/org/olat/selenium/page/course/MembersPage.java
index 5d4bb065a49..e33dc2bcefe 100644
--- a/src/test/java/org/olat/selenium/page/course/MembersPage.java
+++ b/src/test/java/org/olat/selenium/page/course/MembersPage.java
@@ -19,7 +19,10 @@
  */
 package org.olat.selenium.page.course;
 
+import java.util.List;
+
 import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.jcodec.common.Assert;
 import org.olat.selenium.page.graphene.OOGraphene;
 import org.olat.selenium.page.group.GroupPage;
 import org.olat.selenium.page.group.GroupsPage;
@@ -124,6 +127,27 @@ public class MembersPage {
 			.next().next().next().finish();
 	}
 	
+	/**
+	 * Check if the user with the specified first name is in the member list.
+	 * @param user
+	 * @return
+	 */
+	public MembersPage assertFirstNameInList(UserVO user) {
+		By firstNameBy = By.xpath("//td//a[contains(text(),'" + user.getFirstName() + "')]");
+		By rowBy = By.cssSelector(".o_sel_member_list table.table tr");
+		List<WebElement> rows = browser.findElements(rowBy);
+		boolean found = false;
+		for(WebElement row:rows) {
+			List<WebElement> firstNameEl = row.findElements(firstNameBy);
+			if(firstNameEl.size() > 0) {
+				found = true;
+				break;
+			}
+		}
+		Assert.assertTrue(found);
+		return this;
+	}
+	
 	/**
 	 * Click back to the course
 	 * 
diff --git a/src/test/java/org/olat/selenium/page/course/MyCoursesPage.java b/src/test/java/org/olat/selenium/page/course/MyCoursesPage.java
index a30cfe27eb9..14d7b68e744 100644
--- a/src/test/java/org/olat/selenium/page/course/MyCoursesPage.java
+++ b/src/test/java/org/olat/selenium/page/course/MyCoursesPage.java
@@ -99,6 +99,29 @@ public class MyCoursesPage {
 		return this;
 	}
 	
+	/**
+	 * Click on the book button of the course specified
+	 * by the title in the course list.
+	 * 
+	 * @param title
+	 */
+	public void book(String title) {
+		By bookingBy = By.cssSelector("a.o_book");
+		By rowBy = By.cssSelector("div.o_table_row");
+		By titleLinkBy = By.cssSelector("h4.o_title a");
+		WebElement linkToBook = null;
+		List<WebElement> rows = browser.findElements(rowBy);
+		for(WebElement row:rows) {
+			WebElement titleLink = row.findElement(titleLinkBy);
+			if(titleLink.getText().contains(title)) {
+				linkToBook = row.findElement(bookingBy);
+			}
+		}
+		Assert.assertNotNull(linkToBook);
+		linkToBook.click();
+		OOGraphene.waitBusy(browser);
+	}
+	
 	public MyCoursesPage selectCatalogEntry(String title) {
 		By titleBy = By.cssSelector(".o_sublevel .o_meta h4.o_title a");
 		List<WebElement> titleLinks = browser.findElements(titleBy);
diff --git a/src/test/java/org/olat/selenium/page/repository/RepositoryAccessPage.java b/src/test/java/org/olat/selenium/page/repository/RepositoryAccessPage.java
index 0903145e68a..cc45565dea2 100644
--- a/src/test/java/org/olat/selenium/page/repository/RepositoryAccessPage.java
+++ b/src/test/java/org/olat/selenium/page/repository/RepositoryAccessPage.java
@@ -19,7 +19,11 @@
  */
 package org.olat.selenium.page.repository;
 
+import java.util.List;
+
 import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.junit.Assert;
+import org.olat.selenium.page.core.BookingPage;
 import org.olat.selenium.page.graphene.OOGraphene;
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebDriver;
@@ -73,6 +77,13 @@ public class RepositoryAccessPage {
 		return this;
 	}
 	
+	public BookingPage boooking() {
+		By bookingFieldsetBy = By.cssSelector("fieldset.o_ac_configuration");
+		List<WebElement> bookingFieldsetEls = browser.findElements(bookingFieldsetBy);
+		Assert.assertEquals(1, bookingFieldsetEls.size());
+		return new BookingPage(browser);
+	}
+	
 	/**
 	 * Click toolbar
 	 */
diff --git a/src/test/java/org/olat/selenium/page/repository/RepositoryEditDescriptionPage.java b/src/test/java/org/olat/selenium/page/repository/RepositoryEditDescriptionPage.java
index da9646e8c68..452c3f6e89a 100644
--- a/src/test/java/org/olat/selenium/page/repository/RepositoryEditDescriptionPage.java
+++ b/src/test/java/org/olat/selenium/page/repository/RepositoryEditDescriptionPage.java
@@ -64,4 +64,5 @@ public class RepositoryEditDescriptionPage {
 		WebElement main = browser.findElement(By.id("o_main_wrapper"));
 		Assert.assertTrue(main.isDisplayed());
 	}
+	
 }
-- 
GitLab