From 4709bca28108f3cc37ca9c66b14313d6fe2b0e4a Mon Sep 17 00:00:00 2001 From: Praburaj Date: Wed, 2 Apr 2014 18:40:25 -0700 Subject: [PATCH] A few changes with this checkin: 1. Removing copyright text from all files per a code review feedback 2. Adding a cookie based session to the application to retrieve the cart Id. 3. Enabled the Accounts controller as we now have the necessary Http abstractions for User, SignIn, SignOut etc. 4. Changing self hosting scripts to run on coreclr by default. 5. Enabling the layout page on all required views. 6. Adding a favico.ico 7. Renaming the Cart property in the EF context to CartItems as it returns a CartItems list. 8. Uncommenting some code which was previously accessing unavailable APIs. They are available now (but yet to validate them). --- Clean.cmd | 2 - README.md | 9 +- .../Controllers/AccountController.cs | 897 +++++++++--------- .../Controllers/CheckoutController.cs | 36 +- src/MusicStore/Controllers/HomeController.cs | 4 +- .../Controllers/ShoppingCartController.cs | 6 +- src/MusicStore/Controllers/StoreController.cs | 4 +- .../Controllers/StoreManagerController.cs | 49 +- src/MusicStore/CustomHost.cmd | 3 + src/MusicStore/Helios.cmd | 3 + src/MusicStore/LKG.json | 53 +- src/MusicStore/Models/AccountViewModels.cs | 4 +- src/MusicStore/Models/Album.cs | 4 +- src/MusicStore/Models/Artist.cs | 4 +- src/MusicStore/Models/CartItem.cs | 5 +- src/MusicStore/Models/Genre.cs | 4 +- src/MusicStore/Models/IdentityModels.cs | 4 +- src/MusicStore/Models/MusicStoreContext.cs | 6 +- src/MusicStore/Models/Order.cs | 4 +- src/MusicStore/Models/OrderDetail.cs | 4 +- src/MusicStore/Models/SampleData.cs | 4 +- src/MusicStore/Models/ShoppingCart.cs | 70 +- src/MusicStore/Run.cmd | 1 - src/MusicStore/SelfHost.cmd | 3 + src/MusicStore/Startup.cs | 7 +- .../ViewModels/ShoppingCartRemoveViewModel.cs | 4 +- .../ViewModels/ShoppingCartViewModel.cs | 4 +- src/MusicStore/Views/Home/Index.cshtml | 5 +- src/MusicStore/Views/Shared/_Layout.cshtml | 5 +- .../Views/ShoppingCart/CartSummary.cshtml | 7 +- .../Views/ShoppingCart/Index.cshtml | 22 +- src/MusicStore/Views/Store/Browse.cshtml | 2 + src/MusicStore/Views/Store/Details.cshtml | 2 + src/MusicStore/Views/Store/Index.cshtml | 2 + .../Views/StoreManager/Create.cshtml | 2 + .../Views/StoreManager/Delete.cshtml | 2 + .../Views/StoreManager/Details.cshtml | 2 + src/MusicStore/Views/StoreManager/Edit.cshtml | 2 + .../Views/StoreManager/Index.cshtml | 2 + src/MusicStore/favicon.ico | Bin 0 -> 32038 bytes src/MusicStore/project.json | 1 + 41 files changed, 595 insertions(+), 659 deletions(-) delete mode 100644 src/MusicStore/Run.cmd create mode 100644 src/MusicStore/favicon.ico diff --git a/Clean.cmd b/Clean.cmd index 9cd29c4b19..d36f61ad18 100644 --- a/Clean.cmd +++ b/Clean.cmd @@ -11,5 +11,3 @@ rmdir /S /Q %APP_PATH%\Properties del %APP_PATH%\*.csproj del %APP_PATH%\*.v12.suo del %APP_PATH%\*.csproj.user -del src\PlaceHolder\*.csproj -rmdir /S /Q src\PlaceHolder\bin \ No newline at end of file diff --git a/README.md b/README.md index f9314c5e7c..95f1aa8c41 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,9 @@ ### Run the application: 1. Run build.cmd to restore all the necessary packages and generate project files -2. Open a command prompt and cd \src\MusicStore\ +2. Open a command prompt and cd \src\\ 3. [Helios]: - 4. Execute CopyAspNetLoader.cmd to copy the AspNet.Loader.dll to the bin directory - 5. Helios.cmd to launch the app on IISExpress. + 4. Helios.cmd to launch the app on IISExpress. 4. [SelfHost]: 5. Run Selfhost.cmd (This runs k web) (Note: If your changes to C# files are not picked up in successive builds - try deleting the bin and obj folder) 5. [CustomHost]: @@ -24,5 +23,5 @@ 2. This is a captured snapshot of build numbers which worked for this application. This LKG will be captured once in a while. ### Note: -1. By default this script starts the application at http://localhost:5001/. Modify Run.cmd if you would like to change the url -2. Use Visual studio only for editing & intellisense. Don't try to build or run the app from Visual studio. +1. By default the scripts will start the application at http://localhost:5001/. Modify the scripts to change the url. +2. Use Visual studio only for editing & intellisense. Don't try to build or run the app from Visual studio. \ No newline at end of file diff --git a/src/MusicStore/Controllers/AccountController.cs b/src/MusicStore/Controllers/AccountController.cs index 24131b31b2..d214df821d 100644 --- a/src/MusicStore/Controllers/AccountController.cs +++ b/src/MusicStore/Controllers/AccountController.cs @@ -1,505 +1,484 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.Abstractions.Security; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.InMemory; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; +using MusicStore.Models; +using System.Security.Principal; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; -//using Microsoft.AspNet.Identity; -//using Microsoft.AspNet.Identity.InMemory; -//using Microsoft.AspNet.Mvc; -//using Microsoft.AspNet.Mvc.ModelBinding; -//using MusicStore.Models; -//using System; -//using System.Collections.Generic; -//using System.Linq; -//using System.Threading.Tasks; +namespace MusicStore.Controllers +{ + //[Authorize] + public class AccountController : Controller + { + public AccountController() + //Bug: No EF yet - using an in memory store + //: this(new UserManager(new UserStore(new ApplicationDbContext()))) + : this(new UserManager(new InMemoryUserStore())) + { + } -//namespace MusicStore.Controllers -//{ -// //[Authorize] -// public class AccountController : Controller -// { -// public AccountController() -// //Bug: No EF yet - using an in memory store -// //: this(new UserManager(new UserStore(new ApplicationDbContext()))) -// : this(new UserManager(new InMemoryUserStore())) -// { -// } + public AccountController(UserManager userManager) + { + UserManager = userManager; + } -// public AccountController(UserManager userManager) -// { -// UserManager = userManager; -// } + public UserManager UserManager { get; private set; } -// public UserManager UserManager { get; private set; } + // + // GET: /Account/Login + [AllowAnonymous] + public IActionResult Login(string returnUrl) + { + ViewBag.ReturnUrl = returnUrl; + return View(); + } -// private void MigrateShoppingCart(string UserName) -// { -// //Bug: No EF -// //var storeDb = new MusicStoreEntities(); -// var storeDb = MusicStoreEntities.Instance; + // + // POST: /Account/Login + //Bug: HTTP verb attribs not available + //[HttpPost] + [AllowAnonymous] + //[ValidateAntiForgeryToken] + public async Task Login(LoginViewModel model, string returnUrl) + { + if (ModelState.IsValid == true) + { + var user = await UserManager.Find(model.UserName, model.Password); + if (user != null) + { + await SignIn(user, model.RememberMe); + return RedirectToLocal(returnUrl); + } + else + { + ModelState.AddModelError("", "Invalid username or password."); + } + } -// // Associate shopping cart items with logged-in user -// var cart = ShoppingCart.GetCart(storeDb, this.Context); -// cart.MigrateCart(UserName); -// storeDb.SaveChanges(); + // If we got this far, something failed, redisplay form + return View(model); + } -// //Bug: TODO -// //Session[ShoppingCart.CartSessionKey] = UserName; -// } + // + // GET: /Account/Register + [AllowAnonymous] + public IActionResult Register() + { + return View(); + } -// // -// // GET: /Account/Login -// [AllowAnonymous] -// public IActionResult Login(string returnUrl) -// { -// //ViewBag.ReturnUrl = returnUrl; -// return View(); -// } + // + // POST: /Account/Register + //Bug: Missing verb attributes + //[HttpPost] + [AllowAnonymous] + //[ValidateAntiForgeryToken] + public async Task Register(RegisterViewModel model) + { + if (ModelState.IsValid == true) + { + var user = new ApplicationUser() { UserName = model.UserName }; + var result = await UserManager.Create(user, model.Password); + if (result.Succeeded) + { + await SignIn(user, isPersistent: false); + //Bug: No helper methods + //return RedirectToAction("Index", "Home"); + } + else + { + AddErrors(result); + } + } -// // -// // POST: /Account/Login -// //Bug: HTTP verb attribs not available -// //[HttpPost] -// [AllowAnonymous] -// //[ValidateAntiForgeryToken] -// public async Task Login(LoginViewModel model, string returnUrl) -// { -// //Bug: How to validate the model state? -// //if (ModelState.IsValid) -// { -// var user = await UserManager.Find(model.UserName, model.Password); -// if (user != null) -// { -// await SignIn(user, model.RememberMe); -// return RedirectToLocal(returnUrl); -// } -// else -// { -// //Bug: Model state error -// //ModelState.AddModelError("", "Invalid username or password."); -// } -// } + // If we got this far, something failed, redisplay form + return View(model); + } -// // If we got this far, something failed, redisplay form -// return View(model); -// } + // + // POST: /Account/Disassociate + //Bug: HTTP verbs + //[HttpPost] + //[ValidateAntiForgeryToken] + public async Task Disassociate(string loginProvider, string providerKey) + { -// // -// // GET: /Account/Register -// [AllowAnonymous] -// public IActionResult Register() -// { -// return View(); -// } + ManageMessageId? message = null; + IdentityResult result = await UserManager.RemoveLogin(this.Context.User.Identity.GetUserId(), new UserLoginInfo(loginProvider, providerKey)); + if (result.Succeeded) + { + message = ManageMessageId.RemoveLoginSuccess; + } + else + { + message = ManageMessageId.Error; + } + //Bug: No helpers available + //return RedirectToAction("Manage", new { Message = message }); + return View(); + } -// // -// // POST: /Account/Register -// //Bug: Missing verb attributes -// //[HttpPost] -// [AllowAnonymous] -// //[ValidateAntiForgeryToken] -// public async Task Register(RegisterViewModel model) -// { -// //Bug: How to validate the model state? -// //if (ModelState.IsValid) -// { -// //Bug: Replacing it with InmemoryUser -// var user = new ApplicationUser() { UserName = model.UserName }; -// var result = await UserManager.Create(user, model.Password); -// if (result.Succeeded) -// { -// await SignIn(user, isPersistent: false); -// //Bug: No helper methods -// //return RedirectToAction("Index", "Home"); -// } -// else -// { -// AddErrors(result); -// } -// } + // + // GET: /Account/Manage + public IActionResult Manage(ManageMessageId? message) + { + ViewBag.StatusMessage = + message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." + : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." + : message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." + : message == ManageMessageId.Error ? "An error has occurred." + : ""; + ViewBag.HasLocalPassword = HasPassword(); + //Bug: No Action method with single parameter + //ViewBag.ReturnUrl = Url.Action("Manage"); + //ViewBag.ReturnUrl = Url.Action("Manage", "Account", null); + return View(); + } -// // If we got this far, something failed, redisplay form -// return View(model); -// } + // + // POST: /Account/Manage + //Bug: No verb attributes + //[HttpPost] + //[ValidateAntiForgeryToken] + public async Task Manage(ManageUserViewModel model) + { + bool hasPassword = await HasPassword(); + ViewBag.HasLocalPassword = hasPassword; + //Bug: No Action method with single parameter + //ViewBag.ReturnUrl = Url.Action("Manage"); + //ViewBag.ReturnUrl = Url.Action("Manage", "Account", null); + if (hasPassword) + { + if (ModelState.IsValid == true) + { + IdentityResult result = await UserManager.ChangePassword(this.Context.User.Identity.GetUserId(), model.OldPassword, model.NewPassword); + if (result.Succeeded) + { + //Bug: No helper method + //return RedirectToAction("Manage", new { Message = ManageMessageId.ChangePasswordSuccess }); + return View(); + } + else + { + AddErrors(result); + } + } + } + else + { + // User does not have a password so remove any validation errors caused by a missing OldPassword field + ModelState state = null; + ModelState.TryGetValue("OldPassword", out state); -// // -// // POST: /Account/Disassociate -// //Bug: HTTP verbs -// //[HttpPost] -// //[ValidateAntiForgeryToken] -// public async Task Disassociate(string loginProvider, string providerKey) -// { -// ManageMessageId? message = null; -// IdentityResult result = await UserManager.RemoveLogin(User.Identity.GetUserId(), new UserLoginInfo(loginProvider, providerKey)); -// if (result.Succeeded) -// { -// message = ManageMessageId.RemoveLoginSuccess; -// } -// else -// { -// message = ManageMessageId.Error; -// } -// //Bug: No helpers available -// //return RedirectToAction("Manage", new { Message = message }); -// return View(); -// } + if (state != null) + { + state.Errors.Clear(); + } -// // -// // GET: /Account/Manage -// public IActionResult Manage(ManageMessageId? message) -// { -// //ViewBag.StatusMessage = -// // message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." -// // : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." -// // : message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." -// // : message == ManageMessageId.Error ? "An error has occurred." -// // : ""; -// //ViewBag.HasLocalPassword = HasPassword(); -// //Bug: No Action method with single parameter -// //ViewBag.ReturnUrl = Url.Action("Manage"); -// //ViewBag.ReturnUrl = Url.Action("Manage", "Account", null); -// return View(); -// } + if (ModelState.IsValid == true) + { + IdentityResult result = await UserManager.AddPassword(this.Context.User.Identity.GetUserId(), model.NewPassword); + if (result.Succeeded) + { + //Bug: No helper method + //return RedirectToAction("Manage", new { Message = ManageMessageId.SetPasswordSuccess }); + } + else + { + AddErrors(result); + } + } + } -// // -// // POST: /Account/Manage -// //Bug: No verb attributes -// //[HttpPost] -// //[ValidateAntiForgeryToken] -// public async Task Manage(ManageUserViewModel model) -// { -// bool hasPassword = await HasPassword(); -// //ViewBag.HasLocalPassword = hasPassword; -// //Bug: No Action method with single parameter -// //ViewBag.ReturnUrl = Url.Action("Manage"); -// //ViewBag.ReturnUrl = Url.Action("Manage", "Account", null); -// if (hasPassword) -// { -// //if (ModelState.IsValid) -// { -// IdentityResult result = await UserManager.ChangePassword(User.Identity.GetUserId(), model.OldPassword, model.NewPassword); -// if (result.Succeeded) -// { -// //Bug: No helper method -// //return RedirectToAction("Manage", new { Message = ManageMessageId.ChangePasswordSuccess }); -// return View(); -// } -// else -// { -// AddErrors(result); -// } -// } -// } -// else -// { -// // User does not have a password so remove any validation errors caused by a missing OldPassword field -// //Bug: Still controller does not have a ModelState property -// //ModelState state = ModelState["OldPassword"]; -// ModelState state = null; + // If we got this far, something failed, redisplay form + return View(model); + } -// if (state != null) -// { -// state.Errors.Clear(); -// } + // + // POST: /Account/ExternalLogin + //Bug: No verb attributes + //[HttpPost] + [AllowAnonymous] + //[ValidateAntiForgeryToken] + public IActionResult ExternalLogin(string provider, string returnUrl) + { + // Request a redirect to the external login provider + return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl })); + } -// //Bug: No model state validation -// //if (ModelState.IsValid) -// { -// IdentityResult result = await UserManager.AddPassword(User.Identity.GetUserId(), model.NewPassword); -// if (result.Succeeded) -// { -// //Bug: No helper method -// //return RedirectToAction("Manage", new { Message = ManageMessageId.SetPasswordSuccess }); -// } -// else -// { -// AddErrors(result); -// } -// } -// } + // + // GET: /Account/ExternalLoginCallback + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl) + { + var loginInfo = await this.Context.Response.GetExternalLoginInfo(); + if (loginInfo == null) + { + //Bug: No helper + //return RedirectToAction("Login"); + return View(); + } -// // If we got this far, something failed, redisplay form -// return View(model); -// } + // Sign in the user with this external login provider if the user already has a login + var user = await UserManager.Find(loginInfo.Login); + if (user != null) + { + await SignIn(user, isPersistent: false); + return RedirectToLocal(returnUrl); + } + else + { + // If the user does not have an account, then prompt the user to create an account + ViewBag.ReturnUrl = returnUrl; + ViewBag.LoginProvider = loginInfo.Login.LoginProvider; + return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { UserName = loginInfo.DefaultUserName }); + } + } -// // -// // POST: /Account/ExternalLogin -// //Bug: No verb attributes -// //[HttpPost] -// [AllowAnonymous] -// //[ValidateAntiForgeryToken] -// public IActionResult ExternalLogin(string provider, string returnUrl) -// { -// // Request a redirect to the external login provider -// return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl })); -// } + // + // POST: /Account/LinkLogin + //Bug: No HTTP verbs + //[HttpPost] + //[ValidateAntiForgeryToken] + public IActionResult LinkLogin(string provider) + { + // Request a redirect to the external login provider to link a login for the current user + return new ChallengeResult(provider, Url.Action("LinkLoginCallback", "Account", null), this.Context.User.Identity.GetUserId()); + } -// // -// // GET: /Account/ExternalLoginCallback -// [AllowAnonymous] -// public async Task ExternalLoginCallback(string returnUrl) -// { -// var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(); -// if (loginInfo == null) -// { -// //Bug: No helper -// //return RedirectToAction("Login"); -// return View(); -// } + // + // GET: /Account/LinkLoginCallback + public async Task LinkLoginCallback() + { + var loginInfo = await this.Context.Response.GetExternalLoginInfo(XsrfKey, this.Context.User.Identity.GetUserId()); + if (loginInfo == null) + { + //Bug: No helper method + //return RedirectToAction("Manage", new { Message = ManageMessageId.Error }); + return View(); + } + var result = await UserManager.AddLogin(this.Context.User.Identity.GetUserId(), loginInfo.Login); + if (result.Succeeded) + { + //Bug: No helper method + //return RedirectToAction("Manage"); + return View(); + } + //Bug: No helper method + //return RedirectToAction("Manage", new { Message = ManageMessageId.Error }); + return View(); + } -// // Sign in the user with this external login provider if the user already has a login -// var user = await UserManager.Find(loginInfo.Login); -// if (user != null) -// { -// await SignIn(user, isPersistent: false); -// return RedirectToLocal(returnUrl); -// } -// else -// { -// // If the user does not have an account, then prompt the user to create an account -// //ViewBag.ReturnUrl = returnUrl; -// //ViewBag.LoginProvider = loginInfo.Login.LoginProvider; -// return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { UserName = loginInfo.DefaultUserName }); -// } -// } + // + // POST: /Account/ExternalLoginConfirmation + //Bug: No HTTP verbs + //[HttpPost] + [AllowAnonymous] + //[ValidateAntiForgeryToken] + public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl) + { + if (this.Context.User.Identity.IsAuthenticated) + { + //Bug: No helper yet + //return RedirectToAction("Manage"); + return View(); + } -// // -// // POST: /Account/LinkLogin -// //Bug: No HTTP verbs -// //[HttpPost] -// //[ValidateAntiForgeryToken] -// public IActionResult LinkLogin(string provider) -// { -// // Request a redirect to the external login provider to link a login for the current user -// return new ChallengeResult(provider, Url.Action("LinkLoginCallback", "Account", null), User.Identity.GetUserId()); -// } + if (ModelState.IsValid == true) + { + // Get the information about the user from the external login provider + var info = await this.Context.Response.GetExternalLoginInfo(); + if (info == null) + { + return View("ExternalLoginFailure"); + } -// // -// // GET: /Account/LinkLoginCallback -// public async Task LinkLoginCallback() -// { -// var loginInfo = null;// await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId()); -// if (loginInfo == null) -// { -// //Bug: No helper method -// //return RedirectToAction("Manage", new { Message = ManageMessageId.Error }); -// return View(); -// } -// var result = await UserManager.AddLogin(User.Identity.GetUserId(), loginInfo.Login); -// if (result.Succeeded) -// { -// //Bug: No helper method -// //return RedirectToAction("Manage"); -// return View(); -// } -// //Bug: No helper method -// //return RedirectToAction("Manage", new { Message = ManageMessageId.Error }); -// return View(); -// } + var user = new ApplicationUser() { UserName = model.UserName }; + var result = await UserManager.Create(user); + if (result.Succeeded) + { + result = await UserManager.AddLogin(user.Id, info.Login); + if (result.Succeeded) + { + await SignIn(user, isPersistent: false); + return RedirectToLocal(returnUrl); + } + } + AddErrors(result); + } -// // -// // POST: /Account/ExternalLoginConfirmation -// //Bug: No HTTP verbs -// //[HttpPost] -// [AllowAnonymous] -// //[ValidateAntiForgeryToken] -// public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl) -// { -// if (User.Identity.IsAuthenticated) -// { -// //Bug: No helper yet -// //return RedirectToAction("Manage"); -// return View(); -// } + ViewBag.ReturnUrl = returnUrl; + return View(model); + } -// //Bug: No model state validation -// //if (ModelState.IsValid) -// { -// // Get the information about the user from the external login provider -// var info = await AuthenticationManager.GetExternalLoginInfoAsync(); -// if (info == null) -// { -// return View("ExternalLoginFailure"); -// } -// //Using InMemory user -// var user = new ApplicationUser() { UserName = model.UserName }; -// var result = await UserManager.Create(user); -// if (result.Succeeded) -// { -// result = await UserManager.AddLogin(user.Id, info.Login); -// if (result.Succeeded) -// { -// await SignIn(user, isPersistent: false); -// return RedirectToLocal(returnUrl); -// } -// } -// AddErrors(result); -// } + // + // POST: /Account/LogOff + //Bug: No HTTP verbs + //[HttpPost] + //[ValidateAntiForgeryToken] + public IActionResult LogOff() + { + this.Context.Response.SignOut(); + //Bug: No helper + //return RedirectToAction("Index", "Home"); + return View(); + } -// //ViewBag.ReturnUrl = returnUrl; -// return View(model); -// } + // + // GET: /Account/ExternalLoginFailure + [AllowAnonymous] + public IActionResult ExternalLoginFailure() + { + return View(); + } -// // -// // POST: /Account/LogOff -// //Bug: No HTTP verbs -// //[HttpPost] -// //[ValidateAntiForgeryToken] -// public IActionResult LogOff() -// { -// AuthenticationManager.SignOut(); -// //return RedirectToAction("Index", "Home"); -// return View(); -// } + //Bug: Need this attribute + //[ChildActionOnly] + public async Task RemoveAccountList() + { + var linkedAccounts = await UserManager.GetLogins(this.Context.User.Identity.GetUserId()); + ViewBag.ShowRemoveButton = await HasPassword() || linkedAccounts.Count > 1; + //Bug: We dont have partial views yet + //return (IActionResult)PartialView("_RemoveAccountPartial", linkedAccounts); + return View(); + } -// // -// // GET: /Account/ExternalLoginFailure -// [AllowAnonymous] -// public IActionResult ExternalLoginFailure() -// { -// return View(); -// } + //Bug: Controllers need to be disposable? + protected void Dispose(bool disposing) + { + if (disposing && UserManager != null) + { + UserManager.Dispose(); + UserManager = null; + } -// //Bug: Need this attribute -// //[ChildActionOnly] -// public async Task RemoveAccountList() -// { -// var linkedAccounts = await UserManager.GetLogins(User.Identity.GetUserId()); -// //ViewBag.ShowRemoveButton = await HasPassword() || linkedAccounts.Count > 1; -// //Bug: We dont have partial views yet -// //return (IActionResult)PartialView("_RemoveAccountPartial", linkedAccounts); -// return View(); -// } + //base.Dispose(disposing); + } -// //Bug: Controllers need to be disposable? -// protected void Dispose(bool disposing) -// { -// if (disposing && UserManager != null) -// { -// UserManager.Dispose(); -// UserManager = null; -// } - -// //base.Dispose(disposing); -// } + #region Helpers + // Used for XSRF protection when adding external logins + private const string XsrfKey = "XsrfId"; -// #region Helpers -// // Used for XSRF protection when adding external logins -// private const string XsrfKey = "XsrfId"; + private async Task SignIn(ApplicationUser user, bool isPersistent) + { + this.Context.Response.SignOut(DefaultAuthenticationTypes.ExternalCookie); + var identity = await UserManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie); + this.Context.Response.SignIn(identity, new AuthenticationProperties() { IsPersistent = isPersistent }); + } -// //private IAuthenticationManager AuthenticationManager -// //{ -// // get -// // { -// // //Will change to Context.Authentication -// // return new IAuthenticationManager(); -// // } -// //} + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError("", error); + } + } -// private async Task SignIn(ApplicationUser user, bool isPersistent) -// { -// //Bug: No cookies middleware now. -// //AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie); -// //var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); -// //AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity); + private async Task HasPassword() + { + var user = await UserManager.FindById(this.Context.User.Identity.GetUserId()); + if (user != null) + { + return user.PasswordHash != null; + } + return false; + } -// // Migrate the user's shopping cart -// MigrateShoppingCart(user.UserName); -// } + public enum ManageMessageId + { + ChangePasswordSuccess, + SetPasswordSuccess, + RemoveLoginSuccess, + Error + } -// private void AddErrors(IdentityResult result) -// { -// foreach (var error in result.Errors) -// { -// //ModelState.AddModelError("", error); -// } -// } + private IActionResult RedirectToLocal(string returnUrl) + { + //Bug: No helpers available + //if (Url.IsLocalUrl(returnUrl)) + //{ + // return Redirect(returnUrl); + //} + //else + //{ + // return RedirectToAction("Index", "Home"); + //} + return View(); + } -// private async Task HasPassword() -// { -// //Bug: Need to get the User object somehow: TODO -// //var user = await UserManager.FindById(User.Identity.GetUserId()); -// var user = await UserManager.FindById("TODO"); -// if (user != null) -// { -// return user.PasswordHash != null; -// } -// return false; -// } + private class ChallengeResult : HttpStatusCodeResult + { + public ChallengeResult(string provider, string redirectUri) + : this(provider, redirectUri, null) + { + } -// public enum ManageMessageId -// { -// ChangePasswordSuccess, -// SetPasswordSuccess, -// RemoveLoginSuccess, -// Error -// } + public ChallengeResult(string provider, string redirectUri, string userId) + : base(401) + { + LoginProvider = provider; + RedirectUri = redirectUri; + UserId = userId; + } -// private IActionResult RedirectToLocal(string returnUrl) -// { -// //Bug: No helpers available -// //if (Url.IsLocalUrl(returnUrl)) -// //{ -// // return Redirect(returnUrl); -// //} -// //else -// //{ -// // return RedirectToAction("Index", "Home"); -// //} -// return View(); -// } + public string LoginProvider { get; set; } + public string RedirectUri { get; set; } + public string UserId { get; set; } -// private class ChallengeResult : HttpStatusCodeResult -// { -// public ChallengeResult(string provider, string redirectUri) -// : this(provider, redirectUri, null) -// { -// } + new public void ExecuteResultAsync(ActionContext context) + { + var properties = new AuthenticationProperties() { RedirectUri = RedirectUri }; + if (UserId != null) + { + properties.Dictionary[XsrfKey] = UserId; + } -// public ChallengeResult(string provider, string redirectUri, string userId) -// : base(401) -// { -// LoginProvider = provider; -// RedirectUri = redirectUri; -// UserId = userId; -// } + context.HttpContext.Response.Challenge(LoginProvider, properties); + } + } + #endregion + } -// public string LoginProvider { get; set; } -// public string RedirectUri { get; set; } -// public string UserId { get; set; } + /// + /// TODO: Temporary APIs to unblock build. Need to remove this once we have these APIs available. + /// + public static class Extensions + { + public static string GetUserId(this IIdentity user) + { + return string.Empty; + } -// new public void ExecuteResultAsync(ActionContext context) -// { -// //Bug: No security package yet -// //var properties = new AuthenticationProperties() { RedirectUri = RedirectUri }; -// //if (UserId != null) -// //{ -// // properties.Dictionary[XsrfKey] = UserId; -// //} -// //context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider); -// } -// } -// #endregion -// } + public static Task GetExternalLoginInfo(this HttpResponse response) + { + return Task.FromResult(new ExternalLoginInfo()); + } -// //Bug: To remove this. Until we have ClaimsPrincipal available -// internal class User -// { -// public static IdentityInstance Identity { get; set; } + public static Task GetExternalLoginInfo(this HttpResponse response, string xsrfKey, string expectedValue) + { + return Task.FromResult(new ExternalLoginInfo()); + } + } -// public User() -// { -// if (Identity == null) -// { -// Identity = new IdentityInstance(); -// } -// } + /// + /// TODO: Temporary APIs to unblock build. Need to remove this once we have these APIs available. + /// + public class ExternalLoginInfo + { + public string DefaultUserName { get; set; } + public UserLoginInfo Login { get; set; } + } -// internal class IdentityInstance -// { -// public string GetUserId() -// { -// return string.Empty; -// } - -// public bool IsAuthenticated { get; set; } -// } -// } -//} \ No newline at end of file + /// + /// TODO: Temporary APIs to unblock build. Need to remove this once we have these APIs available. + /// + public static class DefaultAuthenticationTypes + { + public const string ApplicationCookie = "Application"; + public const string ExternalCookie = "External"; + } +} \ No newline at end of file diff --git a/src/MusicStore/Controllers/CheckoutController.cs b/src/MusicStore/Controllers/CheckoutController.cs index 1b7f37840b..bd0f97c4ac 100644 --- a/src/MusicStore/Controllers/CheckoutController.cs +++ b/src/MusicStore/Controllers/CheckoutController.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc; using MusicStore.Models; using System; using System.Linq; @@ -26,20 +24,16 @@ namespace MusicStore.Controllers // // POST: /Checkout/AddressAndPayment - //Bug: Http verbs not available. Also binding to FormCollection is not available. + //Bug: Http verbs not available. + //Bug: Using direct model binding until we have TryUpdateModel available. //[HttpPost] //public IActionResult AddressAndPayment(FormCollection values) - public async Task AddressAndPayment(int workaroundId) + public async Task AddressAndPayment(Order order) { var formCollection = await Context.Request.GetFormAsync(); - var order = new Order(); - //TryUpdateModel(order); - try { - //if (string.Equals(values["PromoCode"], PromoCode, - // StringComparison.OrdinalIgnoreCase) == false) if (string.Equals(formCollection.GetValues("PromoCode").FirstOrDefault(), PromoCode, StringComparison.OrdinalIgnoreCase) == false) { @@ -52,20 +46,9 @@ namespace MusicStore.Controllers ? db.Orders.Max(o => o.OrderId) + 1 : 1; - //Bug: Object values should come from page (putting in hard coded values as EF can't work with nulls against SQL Server yet) - //Bug: Identity not available - order.Username = "unknown"; //User.Identity.Name; order.OrderId = nextId; + order.Username = this.Context.User.Identity.Name; order.OrderDate = DateTime.Now; - order.FirstName = "John"; - order.LastName = "Doe"; - order.Address = "One Microsoft Way"; - order.City = "Redmond"; - order.State = "WA"; - order.Country = "USA"; - order.Email = "john.doe@example.com"; - order.Phone = "555-555-5555"; - order.PostalCode = "98052"; //Add the Order db.Orders.Add(order); @@ -82,7 +65,6 @@ namespace MusicStore.Controllers // new { id = order.OrderId }); return View(); } - } catch { @@ -97,13 +79,9 @@ namespace MusicStore.Controllers public IActionResult Complete(int id) { // Validate customer owns this order - //Bug: Identity not available - //bool isValid = storeDB.Orders.Any( - // o => o.OrderId == id && - // o.Username == User.Identity.Name); - bool isValid = db.Orders.Any( - o => o.OrderId == id); + o => o.OrderId == id && + o.Username == this.Context.User.Identity.Name); if (isValid) { diff --git a/src/MusicStore/Controllers/HomeController.cs b/src/MusicStore/Controllers/HomeController.cs index b0cbe8f3bf..af032c7c8a 100644 --- a/src/MusicStore/Controllers/HomeController.cs +++ b/src/MusicStore/Controllers/HomeController.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc; using MusicStore.Models; using System.Collections.Generic; using System.Linq; diff --git a/src/MusicStore/Controllers/ShoppingCartController.cs b/src/MusicStore/Controllers/ShoppingCartController.cs index 68226d165d..c08394d149 100644 --- a/src/MusicStore/Controllers/ShoppingCartController.cs +++ b/src/MusicStore/Controllers/ShoppingCartController.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc; using MusicStore.Models; using MusicStore.ViewModels; using System.Linq; @@ -64,7 +62,7 @@ namespace MusicStore.Controllers // Get the name of the album to display confirmation // TODO [EF] Turn into one query once query of related data is enabled - int albumId = db.Carts.Single(item => item.CartItemId == id).AlbumId; + int albumId = db.CartItems.Single(item => item.CartItemId == id).AlbumId; string albumName = db.Albums.Single(a => a.AlbumId == albumId).Title; // Remove from cart diff --git a/src/MusicStore/Controllers/StoreController.cs b/src/MusicStore/Controllers/StoreController.cs index 454de6ae0f..623199c133 100644 --- a/src/MusicStore/Controllers/StoreController.cs +++ b/src/MusicStore/Controllers/StoreController.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc; using MusicStore.Models; using System.Linq; diff --git a/src/MusicStore/Controllers/StoreManagerController.cs b/src/MusicStore/Controllers/StoreManagerController.cs index 5496caf9b7..8a0fb87164 100644 --- a/src/MusicStore/Controllers/StoreManagerController.cs +++ b/src/MusicStore/Controllers/StoreManagerController.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.Data.Entity; using MusicStore.Models; @@ -49,32 +47,31 @@ namespace MusicStore.Controllers // // GET: /StoreManager/Create - //public IActionResult Create() - //{ - // ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name"); - // ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name"); - // return View(); - //} + public IActionResult Create() + { + //ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name"); + //ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name"); + return View(); + } - //Bug: ModelState.IsValid not available - //Bug: RedirectToAction() not available //Bug: SelectList not available // POST: /StoreManager/Create - //[HttpPost] - //public IActionResult Create(Album album) - //{ - // if (ModelState.IsValid) - // { - // db.Albums.Add(album); - // db.SaveChanges(); - // return RedirectToAction("Index"); - // } + public IActionResult Create(Album album) + { + if (ModelState.IsValid == true) + { + db.Albums.Add(album); + db.SaveChanges(); + //Bug: RedirectToAction() not available + //return RedirectToAction("Index"); + return View(); + } - // ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId); - // ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId); - // return View(album); - //} + //ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId); + //ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId); + return View(album); + } // // GET: /StoreManager/Edit/5 @@ -100,13 +97,13 @@ namespace MusicStore.Controllers //[HttpPost] public IActionResult Edit(Album album) { - //Bug: ModelState.IsValid missing - //if (ModelState.IsValid) + if (ModelState.IsValid == true) { db.ChangeTracker.Entry(album).State = EntityState.Modified; db.SaveChanges(); //Bug: Missing RedirectToAction helper //return RedirectToAction("Index"); + return View(); } //ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId); //ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId); diff --git a/src/MusicStore/CustomHost.cmd b/src/MusicStore/CustomHost.cmd index dc89e0482a..c53fa95574 100644 --- a/src/MusicStore/CustomHost.cmd +++ b/src/MusicStore/CustomHost.cmd @@ -1,3 +1,6 @@ +REM run on k10 by default. Assign it to an empty value to run on desktop CLR. +SET TARGET_FRAMEWORK=k10 + REM Selfhost does not need this bin folder rmdir /S /Q bin diff --git a/src/MusicStore/Helios.cmd b/src/MusicStore/Helios.cmd index 9fa1a25ea7..d593c946d1 100644 --- a/src/MusicStore/Helios.cmd +++ b/src/MusicStore/Helios.cmd @@ -1 +1,4 @@ +REM copy the AspNet.Loader.dll to the bin folder +call CopyAspNetLoader.cmd + "%ProgramFiles(x86)%\iis Express\iisexpress.exe" /port:5001 /path:"%cd%" \ No newline at end of file diff --git a/src/MusicStore/LKG.json b/src/MusicStore/LKG.json index 36efaf1a2c..a742464bf1 100644 --- a/src/MusicStore/LKG.json +++ b/src/MusicStore/LKG.json @@ -1,31 +1,32 @@ { "version": "0.1-alpha-*", "dependencies": { - "Helios": "0.1-alpha-107", - "Microsoft.AspNet.Abstractions": "0.1-alpha-162", - "Microsoft.AspNet.Mvc": "0.1-alpha-324", - "Microsoft.AspNet.Razor": "0.1-alpha-144", - "Microsoft.AspNet.ConfigurationModel": "0.1-alpha-117", - "Microsoft.AspNet.DependencyInjection": "0.1-alpha-169", - "Microsoft.AspNet.RequestContainer": "0.1-alpha-133", - "Microsoft.AspNet.Routing": "0.1-alpha-115", - "Microsoft.AspNet.Mvc.ModelBinding": "0.1-alpha-324", - "Microsoft.AspNet.Mvc.Core": "0.1-alpha-324", - "Microsoft.AspNet.Mvc.Razor": "0.1-alpha-324", - "Microsoft.AspNet.Mvc.Rendering": "0.1-alpha-324", - "Microsoft.AspNet.StaticFiles": "0.1-alpha-090", - "System.Security.Claims": "0.1-alpha-052", - "Microsoft.AspNet.Security.DataProtection": "0.1-alpha-100", - "Microsoft.AspNet.Identity": "0.1-alpha-164", - "Microsoft.AspNet.Identity.Entity": "0.1-alpha-164", - "Microsoft.AspNet.Identity.InMemory": "0.1-alpha-164", - "Microsoft.Data.Entity": "0.1-alpha-284", - "Microsoft.Data.Relational": "0.1-alpha-284", - "Microsoft.Data.SqlServer": "0.1-pre-284", - "Microsoft.Data.InMemory": "0.1-alpha-284", - "Microsoft.AspNet.Diagnostics": "0.1-alpha-025", - "Microsoft.AspNet.Hosting": "0.1-alpha-133", - "Microsoft.AspNet.Server.WebListener": "0.1-alpha-039" + "Helios": "0.1-alpha-168", + "Microsoft.AspNet.Abstractions": "0.1-alpha-210", + "Microsoft.AspNet.Mvc": "0.1-alpha-449", + "Microsoft.AspNet.Razor": "0.1-alpha-181", + "Microsoft.AspNet.ConfigurationModel": "0.1-alpha-167", + "Microsoft.AspNet.DependencyInjection": "0.1-alpha-232", + "Microsoft.AspNet.RequestContainer": "0.1-alpha-201", + "Microsoft.AspNet.Routing": "0.1-alpha-182", + "Microsoft.AspNet.Mvc.ModelBinding": "0.1-alpha-449", + "Microsoft.AspNet.Mvc.Core": "0.1-alpha-449", + "Microsoft.AspNet.Mvc.Razor": "0.1-alpha-449", + "Microsoft.AspNet.Mvc.Rendering": "0.1-alpha-449", + "Microsoft.AspNet.StaticFiles": "0.1-alpha-146", + "System.Security.Claims": "0.1-alpha-094", + "System.Security.Principal": "4.0.0.0", + "Microsoft.AspNet.Security.DataProtection": "0.1-alpha-141", + "Microsoft.AspNet.Identity": "0.1-alpha-238", + "Microsoft.AspNet.Identity.Entity": "0.1-alpha-238", + "Microsoft.AspNet.Identity.InMemory": "0.1-alpha-238", + "Microsoft.Data.Entity": "0.1-alpha-380", + "Microsoft.Data.Relational": "0.1-alpha-380", + "Microsoft.Data.SqlServer": "0.1-pre-380", + "Microsoft.Data.InMemory": "0.1-alpha-380", + "Microsoft.AspNet.Diagnostics": "0.1-alpha-081", + "Microsoft.AspNet.Hosting": "0.1-alpha-201", + "Microsoft.AspNet.Server.WebListener": "0.1-alpha-105" }, "configurations": { "net45": { @@ -41,7 +42,7 @@ "System.Linq": "4.0.0.0", "System.Runtime": "4.0.20.0", "System.Dynamic.Runtime": "4.0.0.0", - "System.Threading.Tasks": "4.0.0.0", + "System.Threading.Tasks": "4.0.10.0", "System.ComponentModel": "4.0.0.0", "System.Console": "4.0.0.0", "System.Diagnostics.Debug": "4.0.10.0", diff --git a/src/MusicStore/Models/AccountViewModels.cs b/src/MusicStore/Models/AccountViewModels.cs index c6efee0211..657a4f011d 100644 --- a/src/MusicStore/Models/AccountViewModels.cs +++ b/src/MusicStore/Models/AccountViewModels.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace MusicStore.Models { diff --git a/src/MusicStore/Models/Album.cs b/src/MusicStore/Models/Album.cs index d9b355b123..7ccca05d15 100644 --- a/src/MusicStore/Models/Album.cs +++ b/src/MusicStore/Models/Album.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace MusicStore.Models diff --git a/src/MusicStore/Models/Artist.cs b/src/MusicStore/Models/Artist.cs index c398d51590..43d677c437 100644 --- a/src/MusicStore/Models/Artist.cs +++ b/src/MusicStore/Models/Artist.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace MusicStore.Models { diff --git a/src/MusicStore/Models/CartItem.cs b/src/MusicStore/Models/CartItem.cs index a986c2e4dd..64550abf9a 100644 --- a/src/MusicStore/Models/CartItem.cs +++ b/src/MusicStore/Models/CartItem.cs @@ -1,12 +1,11 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; +using System; using System.ComponentModel.DataAnnotations; namespace MusicStore.Models { public class CartItem { + [Key] public int CartItemId { get; set; } [Required] diff --git a/src/MusicStore/Models/Genre.cs b/src/MusicStore/Models/Genre.cs index 1680d712c4..53b1e9c6f0 100644 --- a/src/MusicStore/Models/Genre.cs +++ b/src/MusicStore/Models/Genre.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace MusicStore.Models diff --git a/src/MusicStore/Models/IdentityModels.cs b/src/MusicStore/Models/IdentityModels.cs index 03ae9b330a..1fdd9494fc 100644 --- a/src/MusicStore/Models/IdentityModels.cs +++ b/src/MusicStore/Models/IdentityModels.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Identity.InMemory; +using Microsoft.AspNet.Identity.InMemory; namespace MusicStore.Models { diff --git a/src/MusicStore/Models/MusicStoreContext.cs b/src/MusicStore/Models/MusicStoreContext.cs index 625d69543e..be38494c46 100644 --- a/src/MusicStore/Models/MusicStoreContext.cs +++ b/src/MusicStore/Models/MusicStoreContext.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.Data.Entity; +using Microsoft.Data.Entity; using Microsoft.Data.Entity.Metadata; using Microsoft.Data.InMemory; using Microsoft.Data.SqlServer; @@ -13,7 +11,7 @@ namespace MusicStore.Models public EntitySet Artists { get; set; } public EntitySet Orders { get; set; } public EntitySet Genres { get; set; } - public EntitySet Carts { get; set; } + public EntitySet CartItems { get; set; } public EntitySet OrderDetails { get; set; } protected override void OnConfiguring(EntityConfigurationBuilder builder) diff --git a/src/MusicStore/Models/Order.cs b/src/MusicStore/Models/Order.cs index eca87275ee..05a1a98ee0 100644 --- a/src/MusicStore/Models/Order.cs +++ b/src/MusicStore/Models/Order.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace MusicStore.Models diff --git a/src/MusicStore/Models/OrderDetail.cs b/src/MusicStore/Models/OrderDetail.cs index a27c971aa1..29f87988df 100644 --- a/src/MusicStore/Models/OrderDetail.cs +++ b/src/MusicStore/Models/OrderDetail.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace MusicStore.Models +namespace MusicStore.Models { public class OrderDetail { diff --git a/src/MusicStore/Models/SampleData.cs b/src/MusicStore/Models/SampleData.cs index f432297346..b2c65f1e11 100644 --- a/src/MusicStore/Models/SampleData.cs +++ b/src/MusicStore/Models/SampleData.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.Data.Entity; +using Microsoft.Data.Entity; using Microsoft.Data.SqlServer; using MusicStore.Models; using System; diff --git a/src/MusicStore/Models/ShoppingCart.cs b/src/MusicStore/Models/ShoppingCart.cs index cbc482ecd1..158cfcf614 100644 --- a/src/MusicStore/Models/ShoppingCart.cs +++ b/src/MusicStore/Models/ShoppingCart.cs @@ -1,7 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Abstractions; -using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Abstractions; using Microsoft.Data.Entity; using System; using System.Collections.Generic; @@ -19,8 +16,6 @@ namespace MusicStore.Models _db = db; } - public const string CartSessionKey = "CartId"; - public static ShoppingCart GetCart(MusicStoreContext db, HttpContext context) { var cart = new ShoppingCart(db); @@ -38,15 +33,15 @@ namespace MusicStore.Models public void AddToCart(Album album) { // Get the matching cart and album instances - var cartItem = _db.Carts.SingleOrDefault( + var cartItem = _db.CartItems.SingleOrDefault( c => c.CartId == ShoppingCartId && c.AlbumId == album.AlbumId); if (cartItem == null) { // TODO [EF] Swap to store generated key once we support identity pattern - var nextCartItemId = _db.Carts.Any() - ? _db.Carts.Max(c => c.CartItemId) + 1 + var nextCartItemId = _db.CartItems.Any() + ? _db.CartItems.Max(c => c.CartItemId) + 1 : 1; // Create a new cart item if no cart item exists @@ -59,7 +54,7 @@ namespace MusicStore.Models DateCreated = DateTime.Now }; - _db.Carts.Add(cartItem); + _db.CartItems.Add(cartItem); } else { @@ -67,14 +62,14 @@ namespace MusicStore.Models cartItem.Count++; // TODO [EF] Remove this line once change detection is available - _db.ChangeTracker.Entry(cartItem).State = Microsoft.Data.Entity.EntityState.Modified; + _db.ChangeTracker.Entry(cartItem).State = EntityState.Modified; } } public int RemoveFromCart(int id) { // Get the cart - var cartItem = _db.Carts.Single( + var cartItem = _db.CartItems.Single( cart => cart.CartId == ShoppingCartId && cart.CartItemId == id); @@ -93,7 +88,7 @@ namespace MusicStore.Models } else { - _db.Carts.Remove(cartItem); + _db.CartItems.Remove(cartItem); } } @@ -103,7 +98,7 @@ namespace MusicStore.Models public void EmptyCart() { - var cartItems = _db.Carts.Where(cart => cart.CartId == ShoppingCartId); + var cartItems = _db.CartItems.Where(cart => cart.CartId == ShoppingCartId); foreach (var cartItem in cartItems) { @@ -115,13 +110,13 @@ namespace MusicStore.Models public List GetCartItems() { - return _db.Carts.Where(cart => cart.CartId == ShoppingCartId).ToList(); + return _db.CartItems.Where(cart => cart.CartId == ShoppingCartId).ToList(); } public int GetCount() { // Get the count of each item in the cart and sum them up - int? count = (from cartItems in _db.Carts + int? count = (from cartItems in _db.CartItems where cartItems.CartId == ShoppingCartId select (int?)cartItems.Count).Sum(); @@ -137,7 +132,7 @@ namespace MusicStore.Models // TODO Collapse to a single query once EF supports querying related data decimal total = 0; - foreach (var item in _db.Carts.Where(c => c.CartId == ShoppingCartId)) + foreach (var item in _db.CartItems.Where(c => c.CartId == ShoppingCartId)) { var album = _db.Albums.Single(a => a.AlbumId == item.AlbumId); total += item.Count * album.Price; @@ -194,38 +189,23 @@ namespace MusicStore.Models // We're using HttpContextBase to allow access to cookies. public string GetCartId(HttpContext context) { - //Bug: Session not in scope. But we should substitute this with cookies when available. - //if (context.Session[CartSessionKey] == null) - //{ - // if (!string.IsNullOrWhiteSpace(context.User.Identity.Name)) - // { - // context.Session[CartSessionKey] = context.User.Identity.Name; - // } - // else - // { - // // Generate a new random GUID using System.Guid class - // Guid tempCartId = Guid.NewGuid(); + var sessionCookie = context.Request.Cookies.Get("Session"); + string cartId = null; - // // Send tempCartId back to client as a cookie - // context.Session[CartSessionKey] = tempCartId.ToString(); - // } - //} - - //return context.Session[CartSessionKey].ToString(); - return string.Empty; - } - - // When a user has logged in, migrate their shopping cart to - // be associated with their username - public void MigrateCart(string userName) - { - var shoppingCart = _db.Carts.Where(c => c.CartId == ShoppingCartId); - - foreach (CartItem item in shoppingCart) + if (string.IsNullOrWhiteSpace(sessionCookie)) { - item.CartId = userName; + //A GUID to hold the cartId. + cartId = Guid.NewGuid().ToString(); + + // Send cart Id as a cookie to the client. + context.Response.Cookies.Append("Session", cartId); + } + else + { + cartId = sessionCookie; } + return cartId; } } } \ No newline at end of file diff --git a/src/MusicStore/Run.cmd b/src/MusicStore/Run.cmd deleted file mode 100644 index 9fa1a25ea7..0000000000 --- a/src/MusicStore/Run.cmd +++ /dev/null @@ -1 +0,0 @@ -"%ProgramFiles(x86)%\iis Express\iisexpress.exe" /port:5001 /path:"%cd%" \ No newline at end of file diff --git a/src/MusicStore/SelfHost.cmd b/src/MusicStore/SelfHost.cmd index d79b405b6c..b752619491 100644 --- a/src/MusicStore/SelfHost.cmd +++ b/src/MusicStore/SelfHost.cmd @@ -1,3 +1,6 @@ +REM run on k10 by default. Assign it to an empty value to run on desktop CLR. +SET TARGET_FRAMEWORK=k10 + REM Selfhost does not need this bin folder rmdir /S /Q bin diff --git a/src/MusicStore/Startup.cs b/src/MusicStore/Startup.cs index a3637341e8..b83d58b467 100644 --- a/src/MusicStore/Startup.cs +++ b/src/MusicStore/Startup.cs @@ -1,11 +1,10 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet; +using Microsoft.AspNet; using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.ConfigurationModel; using Microsoft.AspNet.DependencyInjection; using Microsoft.AspNet.DependencyInjection.Fallback; using Microsoft.AspNet.DependencyInjection.NestedProviders; +using Microsoft.AspNet.RequestContainer; using Microsoft.AspNet.Diagnostics; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.InMemory; @@ -22,11 +21,13 @@ public class Startup { CreateAdminUser(); + //ErrorPageOptions.ShowAll to be used only at development time. Not recommended for production. app.UseErrorPage(ErrorPageOptions.ShowAll); app.UseFileServer(); var serviceProvider = MvcServices.GetDefaultServices().BuildServiceProvider(app.ServiceProvider); + app.UseContainer(serviceProvider); var routes = new RouteCollection() { diff --git a/src/MusicStore/ViewModels/ShoppingCartRemoveViewModel.cs b/src/MusicStore/ViewModels/ShoppingCartRemoveViewModel.cs index 86f690b86e..5743b27d8d 100644 --- a/src/MusicStore/ViewModels/ShoppingCartRemoveViewModel.cs +++ b/src/MusicStore/ViewModels/ShoppingCartRemoveViewModel.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace MusicStore.ViewModels +namespace MusicStore.ViewModels { public class ShoppingCartRemoveViewModel { diff --git a/src/MusicStore/ViewModels/ShoppingCartViewModel.cs b/src/MusicStore/ViewModels/ShoppingCartViewModel.cs index 3948885965..2aa641b8d5 100644 --- a/src/MusicStore/ViewModels/ShoppingCartViewModel.cs +++ b/src/MusicStore/ViewModels/ShoppingCartViewModel.cs @@ -1,6 +1,4 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; +using System.Collections.Generic; using MusicStore.Models; namespace MusicStore.ViewModels diff --git a/src/MusicStore/Views/Home/Index.cshtml b/src/MusicStore/Views/Home/Index.cshtml index b5118ca45c..341f3af947 100644 --- a/src/MusicStore/Views/Home/Index.cshtml +++ b/src/MusicStore/Views/Home/Index.cshtml @@ -1,6 +1,5 @@ -@*Bug: Having a ViewBag.Title throws compilation error: https://github.com/aspnet/WebFx/issues/67*@ -@{ - //Layout = "/Views/Shared/_Layout.cshtml"; +@{ + Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Home Page"; } diff --git a/src/MusicStore/Views/Shared/_Layout.cshtml b/src/MusicStore/Views/Shared/_Layout.cshtml index 4d13260b71..3518a78ae2 100644 --- a/src/MusicStore/Views/Shared/_Layout.cshtml +++ b/src/MusicStore/Views/Shared/_Layout.cshtml @@ -41,7 +41,8 @@

mvcmusicstore.codeplex.com

- @Html.ActionLink("admin", "Index", "StoreManager") + @*Bug: No Html helpers yet*@ + @*@Html.ActionLink("admin", "Index", "StoreManager")*@
@@ -54,4 +55,4 @@ - + \ No newline at end of file diff --git a/src/MusicStore/Views/ShoppingCart/CartSummary.cshtml b/src/MusicStore/Views/ShoppingCart/CartSummary.cshtml index 50d6dbd769..5dc45459f2 100644 --- a/src/MusicStore/Views/ShoppingCart/CartSummary.cshtml +++ b/src/MusicStore/Views/ShoppingCart/CartSummary.cshtml @@ -1,4 +1,9 @@ -@if (ViewBag.CartCount > 0) +@{ + //Bug: Need to have a way to specify an application level layout page + Layout = "/Views/Shared/_Layout.cshtml"; +} + +@if (ViewBag.CartCount > 0) {
  • diff --git a/src/MusicStore/Views/ShoppingCart/Index.cshtml b/src/MusicStore/Views/ShoppingCart/Index.cshtml index e6d1f1679f..09c82ce791 100644 --- a/src/MusicStore/Views/ShoppingCart/Index.cshtml +++ b/src/MusicStore/Views/ShoppingCart/Index.cshtml @@ -1,9 +1,11 @@ -@model MvcMusicStore.ViewModels.ShoppingCartViewModel +@model MusicStore.ViewModels.ShoppingCartViewModel @{ + //Bug: Need to have a way to specify an application level layout page + Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Shopping Cart"; } -@section Scripts { +@*//@section Scripts {*@ -} +@*}*@

    Review your cart:

    - @Html.ActionLink("Checkout >>", "AddressAndPayment", "Checkout") + @*@Html.ActionLink("Checkout >>", "AddressAndPayment", "Checkout")*@

    @@ -58,22 +60,22 @@ @foreach (var item in Model.CartItems) { - + - @Html.ActionLink(item.Album.Title, - "Details", "Store", new { id = item.AlbumId }, null) + @*@Html.ActionLink(item.Album.Title, + "Details", "Store", new { id = item.AlbumId }, null)*@ @item.Album.Price - + @item.Count -
    Remove from cart - + *@ } diff --git a/src/MusicStore/Views/Store/Browse.cshtml b/src/MusicStore/Views/Store/Browse.cshtml index 8789ce33d0..c12b81d257 100644 --- a/src/MusicStore/Views/Store/Browse.cshtml +++ b/src/MusicStore/Views/Store/Browse.cshtml @@ -1,5 +1,7 @@ @model MusicStore.Models.Genre @{ + //Bug: Need to have a way to specify an application level layout page + Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Browse Albums"; }
    diff --git a/src/MusicStore/Views/Store/Details.cshtml b/src/MusicStore/Views/Store/Details.cshtml index 1cff113581..11c13d6f35 100644 --- a/src/MusicStore/Views/Store/Details.cshtml +++ b/src/MusicStore/Views/Store/Details.cshtml @@ -1,6 +1,8 @@ @model MusicStore.Models.Album @{ + //Bug: Need to have a way to specify an application level layout page + Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Album - " + Model.Title; } diff --git a/src/MusicStore/Views/Store/Index.cshtml b/src/MusicStore/Views/Store/Index.cshtml index 9520e3d01b..f3f72f3f51 100644 --- a/src/MusicStore/Views/Store/Index.cshtml +++ b/src/MusicStore/Views/Store/Index.cshtml @@ -1,5 +1,7 @@ @model IEnumerable @{ + //Bug: Need to have a way to specify an application level layout page + Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Store"; }

    Browse Genres

    diff --git a/src/MusicStore/Views/StoreManager/Create.cshtml b/src/MusicStore/Views/StoreManager/Create.cshtml index 062a16cd4f..aee77d06be 100644 --- a/src/MusicStore/Views/StoreManager/Create.cshtml +++ b/src/MusicStore/Views/StoreManager/Create.cshtml @@ -2,6 +2,8 @@ @model MvcMusicStore.Models.Album @{ + //Bug: Need to have a way to specify an application level layout page + Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Create"; } diff --git a/src/MusicStore/Views/StoreManager/Delete.cshtml b/src/MusicStore/Views/StoreManager/Delete.cshtml index 3ab95a31b8..6609a9d0f9 100644 --- a/src/MusicStore/Views/StoreManager/Delete.cshtml +++ b/src/MusicStore/Views/StoreManager/Delete.cshtml @@ -2,6 +2,8 @@ @model MvcMusicStore.Models.Album @{ + //Bug: Need to have a way to specify an application level layout page + Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Delete"; } diff --git a/src/MusicStore/Views/StoreManager/Details.cshtml b/src/MusicStore/Views/StoreManager/Details.cshtml index d649766730..07e973cf07 100644 --- a/src/MusicStore/Views/StoreManager/Details.cshtml +++ b/src/MusicStore/Views/StoreManager/Details.cshtml @@ -2,6 +2,8 @@ @model MvcMusicStore.Models.Album @{ + //Bug: Need to have a way to specify an application level layout page + Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Details"; } diff --git a/src/MusicStore/Views/StoreManager/Edit.cshtml b/src/MusicStore/Views/StoreManager/Edit.cshtml index ab7e395c11..24cc9b77c6 100644 --- a/src/MusicStore/Views/StoreManager/Edit.cshtml +++ b/src/MusicStore/Views/StoreManager/Edit.cshtml @@ -2,6 +2,8 @@ @model MvcMusicStore.Models.Album @{ + //Bug: Need to have a way to specify an application level layout page + Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Edit"; } diff --git a/src/MusicStore/Views/StoreManager/Index.cshtml b/src/MusicStore/Views/StoreManager/Index.cshtml index 669203eb92..cd5ba7ffc5 100644 --- a/src/MusicStore/Views/StoreManager/Index.cshtml +++ b/src/MusicStore/Views/StoreManager/Index.cshtml @@ -14,6 +14,8 @@ } @{ + //Bug: Need to have a way to specify an application level layout page + Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Index"; } diff --git a/src/MusicStore/favicon.ico b/src/MusicStore/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a3a799985c43bc7309d701b2cad129023377dc71 GIT binary patch literal 32038 zcmeHwX>eTEbtY7aYbrGrkNjgie?1jXjZ#zP%3n{}GObKv$BxI7Sl;Bwl5E+Qtj&t8 z*p|m4DO#HoJC-FyvNnp8NP<{Na0LMnTtO21(rBP}?EAiNjWgeO?z`{3ZoURUQlV2d zY1Pqv{m|X_oO91|?^z!6@@~od!@OH>&BN;>c@O+yUfy5w>LccTKJJ&`-k<%M^Zvi( z<$dKp=jCnNX5Qa+M_%6g|IEv~4R84q9|7E=|Ho(Wz3f-0wPjaRL;W*N^>q%^KGRr7 zxbjSORb_c&eO;oV_DZ7ua!sPH=0c+W;`vzJ#j~-x3uj};50#vqo*0w4!LUqs*UCh9 zvy2S%$#8$K4EOa&e@~aBS65_hc~Mpu=454VT2^KzWqEpBA=ME|O;1cn?8p<+{MKJf zbK#@1wzL44m$k(?85=Obido7=C|xWKe%66$z)NrzRwR>?hK?_bbwT z@Da?lBrBL}Zemo1@!9pYRau&!ld17h{f+UV0sY(R{ET$PBB|-=Nr@l-nY6w8HEAw* zRMIQU`24Jl_IFEPcS=_HdrOP5yf81z_?@M>83Vv65$QFr9nPg(wr`Ke8 zaY4ogdnMA*F7a4Q1_uXadTLUpCk;$ZPRRJ^sMOch;rlbvUGc1R9=u;dr9YANbQ<4Z z#P|Cp9BP$FXNPolgyr1XGt$^lFPF}rmBF5rj1Kh5%dforrP8W}_qJL$2qMBS-#%-|s#BPZBSETsn_EBYcr(W5dq( z@f%}C|iN7)YN`^)h7R?Cg}Do*w-!zwZb9=BMp%Wsh@nb22hA zA{`wa8Q;yz6S)zfo%sl08^GF`9csI9BlGnEy#0^Y3b);M+n<(}6jziM7nhe57a1rj zC@(2ISYBL^UtWChKzVWgf%4LW2Tqg_^7jMw`C$KvU+mcakFjV(BGAW9g%CzSyM;Df z143=mq0oxaK-H;o>F3~zJ<(3-j&?|QBn)WJfP#JR zRuA;`N?L83wQt78QIA$(Z)lGQY9r^SFal;LB^qi`8%8@y+mwcGsf~nv)bBy2S7z~9 z=;X@Gglk)^jpbNz?1;`!J3QUfAOp4U$Uxm5>92iT`mek#$>s`)M>;e4{#%HAAcb^8_Ax%ersk|}# z0bd;ZPu|2}18KtvmIo8`1@H~@2ejwo(5rFS`Z4&O{$$+ch2hC0=06Jh`@p+p8LZzY z&2M~8T6X^*X?yQ$3N5EzRv$(FtSxhW>>ABUyp!{484f8(%C1_y)3D%Qgfl_!sz`LTXOjR&L!zPA0qH_iNS!tY{!^2WfD%uT}P zI<~&?@&))5&hPPHVRl9);TPO>@UI2d!^ksb!$9T96V(F){puTsn(}qt_WXNw4VvHj zf;6A_XCvE`Z@}E-IOaG0rs>K>^=Sr&OgT_p;F@v0VCN0Y$r|Lw1?Wjt`AKK~RT*kJ z2>QPuVgLNcF+XKno;WBv$yj@d_WFJbl*#*V_Cwzo@%3n5%z4g21G*PVZ)wM5$A{klYozmGlB zT@u2+s}=f}25%IA!yNcXUr!!1)z(Nqbhojg0lv@7@0UlvUMT)*r;M$d0-t)Z?B1@qQk()o!4fqvfr_I0r7 zy1(NdkHEj#Yu{K>T#We#b#FD=c1XhS{hdTh9+8gy-vkcdkk*QS@y(xxEMb1w6z<^~ zYcETGfB#ibR#ql0EiD;PR$L&Vrh2uRv5t_$;NxC;>7_S5_OXxsi8udY3BUUdi55Sk zcyKM+PQ9YMA%D1kH1q48OFG(Gbl=FmV;yk8o>k%0$rJ8%-IYsHclnYuTskkaiCGkUlkMY~mx&K}XRlKIW;odWIeuKjtbc^8bBOTqK zjj(ot`_j?A6y_h%vxE9o*ntx#PGrnK7AljD_r58ylE*oy@{IY%+mA^!|2vW_`>`aC{#3`#3;D_$^S^cM zRcF+uTO2sICledvFgNMU@A%M)%8JbSLq{dD|2|2Sg8vvh_uV6*Q?F&rKaV{v_qz&y z`f;stIb?Cb2!Cg7CG91Bhu@D@RaIrq-+o+T2fwFu#|j>lD6ZS9-t^5cx>p|?flqUA z;Cgs#V)O#`Aw4$Kr)L5?|7f4izl!;n0jux}tEW$&&YBXz9o{+~HhoiYDJ`w5BVTl&ARya=M7zdy$FEe}iGBur8XE>rhLj&_yDk5D4n2GJZ07u7%zyAfNtOLn;)M?h*Py-Xtql5aJOtL4U8e|!t? z((sc6&OJXrPdVef^wZV&x=Z&~uA7^ix8rly^rEj?#d&~pQ{HN8Yq|fZ#*bXn-26P^ z5!)xRzYO9{u6vx5@q_{FE4#7BipS#{&J7*>y}lTyV94}dfE%Yk>@@pDe&F7J09(-0|wuI|$of-MRfK51#t@t2+U|*s=W; z!Y&t{dS%!4VEEi$efA!#<<7&04?kB}Soprd8*jYv;-Qj~h~4v>{XX~kjF+@Z7<t?^|i z#>_ag2i-CRAM8Ret^rZt*^K?`G|o>1o(mLkewxyA)38k93`<~4VFI?5VB!kBh%NNU zxb8K(^-MU1ImWQxG~nFB-Un;6n{lQz_FfsW9^H$Xcn{;+W^ZcG$0qLM#eNV=vGE@# z1~k&!h4@T|IiI<47@pS|i?Qcl=XZJL#$JKve;booMqDUYY{(xcdj6STDE=n?;fsS1 ze`h~Q{CT$K{+{t+#*I1=&&-UU8M&}AwAxD-rMa=e!{0gQXP@6azBq9(ji11uJF%@5 zCvV`#*?;ZguQ7o|nH%bm*s&jLej#@B35gy32ZAE0`Pz@#j6R&kN5w{O4~1rhDoU zEBdU)%Nl?8zi|DR((u|gg~r$aLYmGMyK%FO*qLvwxK5+cn*`;O`16c!&&XT{$j~5k zXb^fbh1GT-CI*Nj{-?r7HNg=e3E{6rxuluPXY z5Nm8ktc$o4-^SO0|Es_sp!A$8GVwOX+%)cH<;=u#R#nz;7QsHl;J@a{5NUAmAHq4D zIU5@jT!h?kUp|g~iN*!>jM6K!W5ar0v~fWrSHK@})@6Lh#h)C6F6@)&-+C3(zO! z8+kV|B7LctM3DpI*~EYo>vCj>_?x&H;>y0*vKwE0?vi$CLt zfSJB##P|M2dEUDBPKW=9cY-F;L;h3Fs4E2ERdN#NSL7ctAC z?-}_a{*L@GA7JHJudxtDVA{K5Yh*k(%#x4W7w+^ zcb-+ofbT5ieG+@QG2lx&7!MyE2JWDP@$k`M;0`*d+oQmJ2A^de!3c53HFcfW_Wtv< zKghQ;*FifmI}kE4dc@1y-u;@qs|V75Z^|Q0l0?teobTE8tGl@EB?k#q_wUjypJ*R zyEI=DJ^Z+d*&}B_xoWvs27LtH7972qqMxVFcX9}c&JbeNCXUZM0`nQIkf&C}&skSt z^9fw@b^Hb)!^hE2IJq~~GktG#ZWwWG<`@V&ckVR&r=JAO4YniJewVcG`HF;59}=bf zLyz0uxf6MhuSyH#-^!ZbHxYl^mmBVrx) zyrb8sQ*qBd_WXm9c~Of$&ZP$b^)<~0%nt#7y$1Jg$e}WCK>TeUB{P>|b1FAB?%K7>;XiOfd}JQ`|IP#Vf%kVy zXa4;XFZ+>n;F>uX&3|4zqWK2u3c<>q;tzjsb1;d{u;L$-hq3qe@82(ob<3qom#%`+ z;vzYAs7TIMl_O75BXu|r`Qhc4UT*vN$3Oo0kAC!{f2#HexDy|qUpgTF;k{o6|L>7l z=?`=*LXaow1o;oNNLXsGTrvC)$R&{m=94Tf+2iTT3Y_Or z-!;^0a{kyWtO4vksG_3cyc7HQ0~detf0+2+qxq(e1NS251N}w5iTSrM)`0p8rem!j zZ56hGD=pHI*B+dd)2B`%|9f0goozCSeXPw3 z+58k~sI02Yz#lOneJzYcG)EB0|F+ggC6D|B`6}d0khAK-gz7U3EGT|M_9$ZINqZjwf>P zJCZ=ogSoE`=yV5YXrcTQZx@Un(64*AlLiyxWnCJ9I<5Nc*eK6eV1Mk}ci0*NrJ=t| zCXuJG`#7GBbPceFtFEpl{(lTm`LX=B_!H+& z>$*Hf}}y zkt@nLXFG9%v**s{z&{H4e?aqp%&l#oU8lxUxk2o%K+?aAe6jLojA& z_|J0<-%u^<;NT*%4)n2-OdqfctSl6iCHE?W_Q2zpJken#_xUJlidzs249H=b#g z?}L4-Tnp6)t_5X?_$v)vz`s9@^BME2X@w<>sKZ3=B{%*B$T5Nj%6!-Hr;I!Scj`lH z&2dHFlOISwWJ&S2vf~@I4i~(0*T%OFiuX|eD*nd2utS4$1_JM?zmp>a#CsVy6Er^z zeNNZZDE?R3pM?>~e?H_N`C`hy%m4jb;6L#8=a7l>3eJS2LGgEUxsau-Yh9l~o7=Yh z2mYg3`m5*3Ik|lKQf~euzZlCWzaN&=vHuHtOwK!2@W6)hqq$Zm|7`Nmu%9^F6UH?+ z@2ii+=iJ;ZzhiUKu$QB()nKk3FooI>Jr_IjzY6=qxYy;&mvi7BlQ?t4kRjIhb|2q? zd^K~{-^cxjVSj?!Xs=Da5IHmFzRj!Kzh~b!?`P7c&T9s77VLYB?8_?F zauM^)p;qFG!9PHLfIsnt43UnmV?Wn?Ki7aXSosgq;f?MYUuSIYwOn(5vWhb{f%$pn z4ySN-z}_%7|B);A@PA5k*7kkdr4xZ@s{e9j+9w;*RFm;XPDQwx%~;8iBzSKTIGKO z{53ZZU*OLr@S5=k;?CM^i#zkxs3Sj%z0U`L%q`qM+tP zX$aL;*^g$7UyM2Go+_4A+f)IQcy^G$h2E zb?nT$XlgTEFJI8GN6NQf%-eVn9mPilRqUbT$pN-|;FEjq@Ao&TxpZg=mEgBHB zU@grU;&sfmqlO=6|G3sU;7t8rbK$?X0y_v9$^{X`m4jZ_BR|B|@?ZCLSPPEzz`w1n zP5nA;4(kQFKm%$enjkkBxM%Y}2si&d|62L)U(dCzCGn56HN+i#6|nV-TGIo0;W;`( zW-y=1KF4dp$$mC_|6}pbb>IHoKQeZajXQB>jVR?u`R>%l1o54?6NnS*arpVopdEF; zeC5J3*M0p`*8lif;!irrcjC?(uExejsi~>4wKYwstGY^N@KY}TujLx`S=Cu+T=!dx zKWlPm->I**E{A*q-Z^FFT5$G%7Ij0_*Mo4-y6~RmyTzUB&lfae(WZfO>um}mnsDXPEbau-!13!!xd!qh*{C)6&bz0j1I{>y$D-S)b*)JMCPk!=~KL&6Ngin0p6MCOxF2L_R9t8N!$2Wpced<#`y!F;w zKTi5V_kX&X09wAIJ#anfg9Dhn0s7(C6Nj3S-mVn(i|C6ZAVq0$hE)874co};g z^hR7pe4lU$P;*ggYc4o&UTQC%liCXooIfkI3TNaBV%t~FRr}yHu7kjQ2J*3;e%;iW zvDVCh8=G80KAeyhCuY2LjrC!Od1rvF7h}zszxGV)&!)6ChP5WAjv-zQAMNJIG!JHS zwl?pLxC-V5II#(hQ`l)ZAp&M0xd4%cxmco*MIk?{BD=BK`1vpc}D39|XlV z{c&0oGdDa~TL2FT4lh=~1NL5O-P~0?V2#ie`v^CnANfGUM!b4F=JkCwd7Q`c8Na2q zJGQQk^?6w}Vg9-{|2047((lAV84uN%sK!N2?V(!_1{{v6rdgZl56f0zDMQ+q)jKzzu^ztsVken;=DjAh6G`Cw`Q4G+BjS+n*=KI~^K{W=%t zbD-rN)O4|*Q~@<#@1Vx$E!0W9`B~IZeFn87sHMXD>$M%|Bh93rdGf1lKoX3K651t&nhsl= zXxG|%@8}Bbrlp_u#t*DZX<}_0Yb{A9*1Pd_)LtqNwy6xT4pZrOY{s?N4)pPwT(i#y zT%`lRi8U#Ken4fw>H+N`{f#FF?ZxFlLZg7z7#cr4X>id z{9kUD`d2=w_Zlb{^c`5IOxWCZ1k<0T1D1Z31IU0Q2edsZ1K0xv$pQVYq2KEp&#v#Z z?{m@Lin;*Str(C2sfF^L>{R3cjY`~#)m>Wm$Y|1fzeS0-$(Q^z@} zEO*vlb-^XK9>w&Ef^=Zzo-1AFSP#9zb~X5_+){$(eB4K z8gtW+nl{q+CTh+>v(gWrsP^DB*ge(~Q$AGxJ-eYc1isti%$%nM<_&Ev?%|??PK`$p z{f-PM{Ym8k<$$)(F9)tqzFJ?h&Dk@D?Dt{4CHKJWLs8$zy6+(R)pr@0ur)xY{=uXFFzH_> z-F^tN1y(2hG8V)GpDg%wW0Px_ep~nIjD~*HCSxDi0y`H!`V*~RHs^uQsb1*bK1qGpmd zB1m`Cjw0`nLBF2|umz+a#2X$c?Lj;M?Lj;MUp*d>7j~ayNAyj@SLpeH`)BgRH}byy zyQSat!;U{@O(<<2fp&oQkIy$z`_CQ-)O@RN;QD9T4y|wIJ^%U#(BF%=`i49}j!D-) zkOwPSJaG03SMkE~BzW}b_v>LA&y)EEYO6sbdnTX*$>UF|JhZ&^MSb4}Tgbne_4n+C zwI8U4i~PI>7a3{kVa8|))*%C0|K+bIbmV~a`|G#+`TU#g zXW;bWIcWsQi9c4X*RUDpIfyoPY)2bI-r9)xulm1CJDkQd6u+f)_N=w1ElgEBjprPF z3o?Ly0RVeY_{3~fPVckRMxe2lM8hj!B8F)JO z!`AP6>u>5Y&3o9t0QxBpNE=lJx#NyIbp1gD zzUYBIPYHIv9ngk-Zt~<)62^1Zs1LLYMh@_tP^I7EX-9)Ed0^@y{k65Gp0KRcTmMWw zU|+)qx{#q0SL+4q?Q`i0>COIIF8a0Cf&C`hbMj?LmG9K&iW-?PJt*u)38tTXAP>@R zZL6uH^!RYNq$p>PKz7f-zvg>OKXcZ8h!%Vo@{VUZp|+iUD_xb(N~G|6c#oQK^nHZU zKg#F6<)+`rf~k*Xjjye+syV{bwU2glMMMs-^ss4`bYaVroXzn`YQUd__UlZL_mLs z(vO}k!~(mi|L+(5&;>r<;|OHnbXBE78LruP;{yBxZ6y7K3)nMo-{6PCI7gQi6+rF_ zkPod!Z8n}q46ykrlQS|hVB(}(2Kf7BCZ>Vc;V>ccbk2~NGaf6wGQH@W9&?Zt3v(h*P4xDrN>ex7+jH*+Qg z%^jH$&+*!v{sQ!xkWN4+>|b}qGvEd6ANzgqoVy5Qfws}ef2QqF{iiR5{pT}PS&yjo z>lron#va-p=v;m>WB+XVz|o;UJFdjo5_!RRD|6W{4}A2a#bZv)gS_`b|KsSH)Sd_JIr%<%n06TX&t{&!H#{)?4W9hlJ`R1>FyugOh3=D_{einr zu(Wf`qTkvED+gEULO0I*Hs%f;&=`=X4;N8Ovf28x$A*11`dmfy2=$+PNqX>XcG`h% zJY&A6@&)*WT^rC(Caj}2+|X|6cICm5h0OK0cGB_!wEKFZJU)OQ+TZ1q2bTx9hxnq& z$9ee|f9|0M^)#E&Pr4)f?o&DMM4w>Ksb{hF(0|wh+5_{vPow{V%TFzU2za&gjttNi zIyR9qA56dX52Qbv2aY^g`U7R43-p`#sO1A=KS2aKgfR+Yu^bQ*i-qu z%0mP;Ap)B~zZgO9lG^`325gOf?iUHF{~7jyGC)3L(eL(SQ70VzR~wLN18tnx(Cz2~ zctBl1kI)wAe+cxWHw*NW-d;=pd+>+wd$a@GBju*wFvabSaPtHiT!o#QFC+wBVwYo3s=y;z1jM+M=Fj!FZM>UzpL-eZzOT( zhmZmEfWa=%KE#V3-ZK5#v!Hzd{zc^{ctF~- z>DT-U`}5!fk$aj24`#uGdB7r`>oX5tU|d*b|N3V1lXmv%MGrvE(dXG)^-J*LA>$LE z7kut4`zE)v{@Op|(|@i#c>tM!12FQh?}PfA0`Bp%=%*RiXVzLDXnXtE@4B)5uR}a> zbNU}q+712pIrM`k^odG8dKtG$zwHmQI^c}tfjx5?egx3!e%JRm_64e+>`Ra1IRfLb z1KQ`SxmH{cZfyVS5m(&`{V}Y4j6J{b17`h6KWqZ&hfc(oR zxM%w!$F(mKy05kY&lco3%zvLCxBW+t*rxO+i=qGMvobx0-<7`VUu)ka`){=ew+Ovt zg%52_{&UbkUA8aJPWsk)gYWV4`dnxI%s?7^fGpq{ZQuu=VH{-t7w~K%_E<8`zS;V- zKTho*>;UQQul^1GT^HCt@I-q?)&4!QDgBndn?3sNKYKCQFU4LGKJ$n@Je$&w9@E$X z^p@iJ(v&`1(tq~1zc>0Vow-KR&vm!GUzT?Eqgnc)leZ9p)-Z*C!zqb=-$XG0 z^!8RfuQs5s>Q~qcz92(a_Q+KH?C*vCTr~UdTiR`JGuNH8v(J|FTiSEcPrBpmHRtmd zI2Jng0J=bXK);YY^rM?jzn?~X-Pe`GbAy{D)Y6D&1GY-EBcy%Bq?bKh?A>DD9DD!p z?{q02wno2sraGUkZv5dx+J8)&K$)No43Zr(*S`FEdL!4C)}WE}vJd%{S6-3VUw>Wp z?Aasv`T0^%P$2vE?L+Qhj~qB~K%eW)xH(=b_jU}TLD&BP*Pc9hz@Z=e0nkpLkWl}> z_5J^i(9Z7$(XG9~I3sY)`OGZ#_L06+Dy4E>UstcP-rU@xJ$&rxvo!n1Ao`P~KLU-8 z{zDgN4-&A6N!kPSYbQ&7sLufi`YtE2uN$S?e&5n>Y4(q#|KP!cc1j)T^QrUXMPFaP z_SoYO8S8G}Z$?AL4`;pE?7J5K8yWqy23>cCT2{=-)+A$X^-I9=e!@J@A&-;Ufc)`H}c(VI&;0x zrrGv()5mjP%jXzS{^|29?bLNXS0bC%p!YXI!;O457rjCEEzMkGf~B3$T}dXBO23tP z+Ci>;5UoM?C@bU@f9G1^X3=ly&ZeFH<@|RnOG--A&)fd)AUgjw?%izq{p(KJ`EP0v z2mU)P!+3t@X14DA=E2RR-|p${GZ9ETX=d+kJRZL$nSa0daI@&oUUxnZg0xd_xu>Vz lzF#z5%kSKX?YLH3ll^(hI(_`L*t#Iva2Ede*Z;>H_