diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d4f90d --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +[Oo]bj +[Bb]in +*.user +*.suo +*.[Cc]ache +*.bak +*.ncb +*.log +*.DS_Store +[Tt]humbs.db +_ReSharper.* +*.svn \ No newline at end of file diff --git a/Administration/UpgrateDependencies.ps1 b/Administration/UpgrateDependencies.ps1 new file mode 100644 index 0000000..cc27c8f Binary files /dev/null and b/Administration/UpgrateDependencies.ps1 differ diff --git a/Build.bat b/Build.bat new file mode 100644 index 0000000..05348ea --- /dev/null +++ b/Build.bat @@ -0,0 +1,5 @@ +@echo off +pushd Suteki.Shop +nant\nant.exe +popd +pause \ No newline at end of file diff --git a/CompileViews.bat b/CompileViews.bat new file mode 100644 index 0000000..87ccbf2 --- /dev/null +++ b/CompileViews.bat @@ -0,0 +1,5 @@ +@echo off +pushd Suteki.Shop +nant\nant.exe compile-views +popd +pause \ No newline at end of file diff --git a/Database/001_create_database.sql b/Database/001_create_database.sql new file mode 100644 index 0000000..85ab92c Binary files /dev/null and b/Database/001_create_database.sql differ diff --git a/Database/002_insert_static_data.sql b/Database/002_insert_static_data.sql new file mode 100644 index 0000000..0bb4856 --- /dev/null +++ b/Database/002_insert_static_data.sql @@ -0,0 +1,58 @@ +use SutekiShop + + +insert [role] (RoleId, [Name]) values(1, 'Administrator') +insert [role] (RoleId, [Name]) values(2, 'Order Processor') +insert [role] (RoleId, [Name]) values(3, 'Customer') +insert [role] (RoleId, [Name]) values(4, 'Guest') + +insert [user] (Email, [Password], RoleId, IsEnabled) + values('admin@sutekishop.co.uk', 'D033E22AE348AEB5660FC2140AEC35850C4DA997', 1, 1) + +set identity_insert category on +insert category (CategoryId, [Name], ParentId, Position, IsActive) values(1, '- Root', null, 1, 1) +set identity_insert category off + +insert CardType values(1, 'Visa / Delta / Electron', 0) +insert CardType values(2, 'Master Card / Euro Card', 0) +insert CardType values(3, 'American Express', 0) +insert CardType values(4, 'Switch / Solo / Maestro', 1) + +insert OrderStatus values(1, 'Created') +insert OrderStatus values(2, 'Dispatched') +insert OrderStatus values(3, 'Rejected') + +insert ContentType values(1, 'Menu') +insert ContentType values(2, 'Text') +insert ContentType values(3, 'Action') +insert ContentType values(4, 'Top') + +set identity_insert [content] on + +insert [content](contentId, parentContentId, contentTypeId, [name], UrlName, [text], controller, [action], position, isActive) +values(1, null, 1, 'Main Menu', 'Main_menu', null, null, null, 1, 1) + +insert [content](contentId, parentContentId, contentTypeId, [name], UrlName, [text], controller, [action], position, isActive) +values(2, 1, 4, 'Home', 'Home', 'Homepage Content', null, null, 2, 1) + +insert [content](contentId, parentContentId, contentTypeId, [name], UrlName, [text], controller, [action], position, isActive) +values(3, 1, 3, 'Online Shop', 'Online_Shop', null, 'Home', 'Index', 3, 1) + +insert [content](contentId, parentContentId, contentTypeId, [name], UrlName, [text], controller, [action], position, isActive) +values(4, null, 2, 'Shopfront', 'Shopfront', '

Wecome to our online shop

', null, null, 4, 1) + +set identity_insert [content] off + +set identity_insert PostZone on + +insert PostZone (PostZoneId, [Name], Multiplier, AskIfMaxWeight, Position, IsActive, FlatRate) +values(1, 'United Kingdom', 1, 0, 1, 1, 10) + +set identity_insert PostZone off + +set identity_insert [Country] on + +insert [Country] (CountryId, [Name], Position, IsActive, PostZoneId) +values(1, 'United Kingdom', 1, 1, 1) + +set identity_insert [Country] off \ No newline at end of file diff --git a/Database/003_Add_notes_to_order.sql b/Database/003_Add_notes_to_order.sql new file mode 100644 index 0000000..d090bdd --- /dev/null +++ b/Database/003_Add_notes_to_order.sql @@ -0,0 +1,3 @@ +use SutekiShop +ALTER TABLE dbo.[Order] +ADD Note nvarchar(1000) NULL \ No newline at end of file diff --git a/Database/004_Add_CategoryImage.sql b/Database/004_Add_CategoryImage.sql new file mode 100644 index 0000000..de39a82 --- /dev/null +++ b/Database/004_Add_CategoryImage.sql @@ -0,0 +1,3 @@ +use SutekiShop +alter table Category + ADD ImageId int \ No newline at end of file diff --git a/Database/005_Add_ContactMe_To_Order.sql b/Database/005_Add_ContactMe_To_Order.sql new file mode 100644 index 0000000..fa57611 --- /dev/null +++ b/Database/005_Add_ContactMe_To_Order.sql @@ -0,0 +1,3 @@ +use SutekiShop +ALTER TABLE dbo.[Order] ADD + ContactMe bit NOT NULL CONSTRAINT DF_Order_ContactMe DEFAULT 0 \ No newline at end of file diff --git a/Database/006_Change_products_to_have_many_categories.sql b/Database/006_Change_products_to_have_many_categories.sql new file mode 100644 index 0000000..17f88df --- /dev/null +++ b/Database/006_Change_products_to_have_many_categories.sql @@ -0,0 +1,41 @@ +use SutekiShop +create table ProductCategory ( + Id int not null identity(1, 1), + ProductId int not null, + CategoryId int not null +) ON [PRIMARY] + +ALTER TABLE ProductCategory ADD CONSTRAINT PK_ProductCategory PRIMARY KEY CLUSTERED ( + Id +) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] + +insert into ProductCategory (ProductId, CategoryId) +select ProductId, CategoryId from Product + +ALTER TABLE dbo.ProductCategory ADD CONSTRAINT + FK_ProductCategory_Category FOREIGN KEY + ( + CategoryId + ) REFERENCES dbo.Category + ( + CategoryId + ) ON UPDATE NO ACTION + ON DELETE NO ACTION + + +ALTER TABLE dbo.ProductCategory ADD CONSTRAINT + FK_ProductCategory_Product FOREIGN KEY + ( + ProductId + ) REFERENCES dbo.Product + ( + ProductId + ) ON UPDATE NO ACTION + ON DELETE NO ACTION + + +alter table Product + drop constraint FK_Product_Category + +alter table Product + drop column CategoryId \ No newline at end of file diff --git a/Database/007_Add_pending_status.sql b/Database/007_Add_pending_status.sql new file mode 100644 index 0000000..ff3247c --- /dev/null +++ b/Database/007_Add_pending_status.sql @@ -0,0 +1,3 @@ +use SutekiShop +insert into OrderStatus (OrderStatusId, Name) +VALUES (0, 'Pending') \ No newline at end of file diff --git a/Database/008_Add_Reviews.sql b/Database/008_Add_Reviews.sql new file mode 100644 index 0000000..7fa4c4f --- /dev/null +++ b/Database/008_Add_Reviews.sql @@ -0,0 +1,14 @@ +use SutekiShop +CREATE TABLE dbo.Review ( + Id int NOT NULL IDENTITY (1, 1), + ProductId int NOT NULL, + Approved bit NOT NULL, + [Text] nvarchar(MAX) NULL, + Reviewer nvarchar(250), + Rating int +) ON [PRIMARY] +TEXTIMAGE_ON [PRIMARY] + +ALTER TABLE dbo.Review ADD CONSTRAINT PK_Review PRIMARY KEY CLUSTERED ( + Id +) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] \ No newline at end of file diff --git a/Database/009_Add_MailingListSubscriptions.sql b/Database/009_Add_MailingListSubscriptions.sql new file mode 100644 index 0000000..70a3362 --- /dev/null +++ b/Database/009_Add_MailingListSubscriptions.sql @@ -0,0 +1,34 @@ +use SutekiShop +GO +CREATE TABLE dbo.MailingListSubscription +( + Id int NOT NULL IDENTITY (1, 1), + ContactId int NOT NULL, + Email nvarchar(250) NOT NULL, + DateSubscribed datetime NOT NULL CONSTRAINT DF_MailingListSubscription_DateSubscribed DEFAULT getdate() +) ON [PRIMARY] +GO +ALTER TABLE dbo.MailingListSubscription ADD CONSTRAINT + PK_MailingListSubscription PRIMARY KEY CLUSTERED + ( + Id + ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] + +GO + +ALTER TABLE dbo.MailingListSubscription ADD CONSTRAINT +FK_MailingListSubscription_Contact FOREIGN KEY +( + ContactId +) REFERENCES dbo.Contact +( + ContactId +) ON UPDATE NO ACTION +ON DELETE NO ACTION + +GO + +IF NOT EXISTS(select * from Content where Name = 'Mailing List') + INSERT INTO Content (ParentContentId, ContentTypeId, Name, UrlName, Controller, Action, IsActive, Position) + VALUES (1, 3, 'Mailing List', 'Mailing_List', 'MailingList', 'Index', 1, 20) +GO diff --git a/Database/010_Update_For_NH_changes.sql b/Database/010_Update_For_NH_changes.sql new file mode 100644 index 0000000..0418406 --- /dev/null +++ b/Database/010_Update_For_NH_changes.sql @@ -0,0 +1,20 @@ + +-- update db for NH changes +use SutekiShop + +alter table [Content] + add ContentType varchar(50) +go +update [Content] set ContentType = 'Menu' where ContentTypeId = 1 +update [Content] set ContentType = 'TextContent' where ContentTypeId = 2 +update [Content] set ContentType = 'ActionContent' where ContentTypeId = 3 +update [Content] set ContentType = 'TopContent' where ContentTypeId = 4 + +exec sp_rename 'ProductCategory.Id', 'ProductCategoryId' +go +exec sp_rename 'Review.Id', 'ReviewId' +go +exec sp_rename 'MailingListSubscription.Id', 'MailingListSubscriptionId' +go + + diff --git a/Database/011_Update_for_modified_order_processing.sql b/Database/011_Update_for_modified_order_processing.sql new file mode 100644 index 0000000..2549957 Binary files /dev/null and b/Database/011_Update_for_modified_order_processing.sql differ diff --git a/Database/012_Update_orderLines_with_productUrlName.sql b/Database/012_Update_orderLines_with_productUrlName.sql new file mode 100644 index 0000000..6c95bc6 --- /dev/null +++ b/Database/012_Update_orderLines_with_productUrlName.sql @@ -0,0 +1,16 @@ +alter table OrderLine +add ProductUrlName nvarchar(255) null + +update OrderLine set ProductUrlName = '' + +update OrderLine set ProductUrlName = p.UrlName +from OrderLine l +join Product p on SUBSTRING(l.ProductName, 0, CHARINDEX(' - ', l.ProductName, 0)) = p.Name + +update OrderLine set ProductUrlName = p.UrlName +from OrderLine l +join Product p on l.ProductName = p.Name + +update OrderLine set ProductUrlName = p.UrlName +from OrderLine l +join Product p on SUBSTRING(l.ProductName, 0, CHARINDEX(' - ', l.ProductName, CHARINDEX(' - ', l.ProductName, 0)+1)) = p.Name diff --git a/Database/013_Update_for_new_review_and_order_adjustment_schema.sql b/Database/013_Update_for_new_review_and_order_adjustment_schema.sql new file mode 100644 index 0000000..70877a7 Binary files /dev/null and b/Database/013_Update_for_new_review_and_order_adjustment_schema.sql differ diff --git a/Database/014_Update_to_2.2.0.492.sql b/Database/014_Update_to_2.2.0.492.sql new file mode 100644 index 0000000..7c5b571 --- /dev/null +++ b/Database/014_Update_to_2.2.0.492.sql @@ -0,0 +1,910 @@ +/* +Upgrade Suteki Shop database to version 2.2.0.492 + +Script created by SQL Compare version 9.0.0 from Red Gate Software Ltd at 20/02/2011 10:30:31 +Additional data tweeks by Mike Hadlow +*/ +SET NUMERIC_ROUNDABORT OFF +GO +SET ANSI_PADDING, ANSI_WARNINGS, CONCAT_NULL_YIELDS_NULL, ARITHABORT, QUOTED_IDENTIFIER, ANSI_NULLS ON +GO +IF EXISTS (SELECT * FROM tempdb..sysobjects WHERE id=OBJECT_ID('tempdb..#tmpErrors')) DROP TABLE #tmpErrors +GO +CREATE TABLE #tmpErrors (Error int) +GO +SET XACT_ABORT ON +GO +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE +GO +BEGIN TRANSACTION +GO +PRINT N'Dropping foreign keys from [dbo].[Content]' +GO +ALTER TABLE [dbo].[Content] DROP +CONSTRAINT [FK4FEDF4B6E53A5F80], +CONSTRAINT [FK4FEDF4B6722A22C5] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[BasketItem]' +GO +ALTER TABLE [dbo].[BasketItem] DROP +CONSTRAINT [FK729387BD8B64590B], +CONSTRAINT [FK729387BD318371E4] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[Order]' +GO +ALTER TABLE [dbo].[Order] DROP +CONSTRAINT [FK3117099BB1026BB4], +CONSTRAINT [FK3117099B6BE90924], +CONSTRAINT [FK3117099B9A379CC1], +CONSTRAINT [FK3117099BD8D66880], +CONSTRAINT [FK3117099B8703A8E0], +CONSTRAINT [FK3117099BA708CB3A] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[ProductCategory]' +GO +ALTER TABLE [dbo].[ProductCategory] DROP +CONSTRAINT [FK4022F9D7EEC058F7], +CONSTRAINT [FK4022F9D7655B84E7] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[Category]' +GO +ALTER TABLE [dbo].[Category] DROP +CONSTRAINT [FK6482F24565E1830], +CONSTRAINT [FK6482F242EC1B23E] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[Comment]' +GO +ALTER TABLE [dbo].[Comment] DROP +CONSTRAINT [FKE24667035273ED9D] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[MailingListSubscription]' +GO +ALTER TABLE [dbo].[MailingListSubscription] DROP +CONSTRAINT [FK1CD411ADF3F5630] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[Contact]' +GO +ALTER TABLE [dbo].[Contact] DROP +CONSTRAINT [FK4FF8F4B2350E3BF0] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[Basket]' +GO +ALTER TABLE [dbo].[Basket] DROP +CONSTRAINT [FKD4474AC2350E3BF0], +CONSTRAINT [FKD4474AC28703A8E0] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[Review]' +GO +ALTER TABLE [dbo].[Review] DROP +CONSTRAINT [FKD28CAB635273ED9D], +CONSTRAINT [FKD28CAB63655B84E7] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[ProductImage]' +GO +ALTER TABLE [dbo].[ProductImage] DROP +CONSTRAINT [FKD8C46FDB2EC1B23E], +CONSTRAINT [FKD8C46FDB655B84E7] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[OrderLine]' +GO +ALTER TABLE [dbo].[OrderLine] DROP +CONSTRAINT [FK9D642A8F71931278] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[OrderAdjustment]' +GO +ALTER TABLE [dbo].[OrderAdjustment] DROP +CONSTRAINT [FKF3A9DF3A71931278] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[Country]' +GO +ALTER TABLE [dbo].[Country] DROP +CONSTRAINT [FK2ABF29C369941CDB] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[Size]' +GO +ALTER TABLE [dbo].[Size] DROP +CONSTRAINT [FKF1577525655B84E7] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping foreign keys from [dbo].[User]' +GO +ALTER TABLE [dbo].[User] DROP +CONSTRAINT [FK7185C17CEF538E9E] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Basket]' +GO +ALTER TABLE [dbo].[Basket] DROP CONSTRAINT [PK__Basket__8FDA77B50EA330E9] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[BasketItem]' +GO +ALTER TABLE [dbo].[BasketItem] DROP CONSTRAINT [PK__BasketIt__6051AA0B403A8C7D] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Card]' +GO +ALTER TABLE [dbo].[Card] DROP CONSTRAINT [PK__Card__55FECDAE4F7CD00D] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Category]' +GO +ALTER TABLE [dbo].[Category] DROP CONSTRAINT [PK__Category__19093A0B07020F21] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Comment]' +GO +ALTER TABLE [dbo].[Comment] DROP CONSTRAINT [PK__Comment__9A61929130F848ED] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Contact]' +GO +ALTER TABLE [dbo].[Contact] DROP CONSTRAINT [PK__Contact__5C66259B03317E3D] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Content]' +GO +ALTER TABLE [dbo].[Content] DROP CONSTRAINT [PK__Content__2907A81E1A14E395] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Country]' +GO +ALTER TABLE [dbo].[Country] DROP CONSTRAINT [PK__Country__10D1609F47DBAE45] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[IComment]' +GO +ALTER TABLE [dbo].[IComment] DROP CONSTRAINT [PK__IComment__5ADFDE572D27B809] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Image]' +GO +ALTER TABLE [dbo].[Image] DROP CONSTRAINT [PK__Image__7516F70C164452B1] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[MailingListSubscription]' +GO +ALTER TABLE [dbo].[MailingListSubscription] DROP CONSTRAINT [PK__MailingL__75B998BF5AEE82B9] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Order]' +GO +ALTER TABLE [dbo].[Order] DROP CONSTRAINT [PK__Order__C3905BCF534D60F1] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[OrderAdjustment]' +GO +ALTER TABLE [dbo].[OrderAdjustment] DROP CONSTRAINT [PK__OrderAdj__CDB07B715EBF139D] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[OrderLine]' +GO +ALTER TABLE [dbo].[OrderLine] DROP CONSTRAINT [PK__OrderLin__29068A107F60ED59] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[OrderStatus]' +GO +ALTER TABLE [dbo].[OrderStatus] DROP CONSTRAINT [PK__OrderSta__BC674CA129572725] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Postage]' +GO +ALTER TABLE [dbo].[Postage] DROP CONSTRAINT [PK__Postage__203F354A3C69FB99] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[PostZone]' +GO +ALTER TABLE [dbo].[PostZone] DROP CONSTRAINT [PK__PostZone__F5DC874238996AB5] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Product]' +GO +ALTER TABLE [dbo].[Product] DROP CONSTRAINT [PK__Product__B40CC6CD1273C1CD] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[ProductCategory]' +GO +ALTER TABLE [dbo].[ProductCategory] DROP CONSTRAINT [PK__ProductC__3224ECCE25869641] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[ProductImage]' +GO +ALTER TABLE [dbo].[ProductImage] DROP CONSTRAINT [PK__ProductI__07B2B1B8571DF1D5] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Review]' +GO +ALTER TABLE [dbo].[Review] DROP CONSTRAINT [PK__Review__9A61929134C8D9D1] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Role]' +GO +ALTER TABLE [dbo].[Role] DROP CONSTRAINT [PK__Role__8AFACE1A1DE57479] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[Size]' +GO +ALTER TABLE [dbo].[Size] DROP CONSTRAINT [PK__Size__83BD097A4BAC3F29] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[User]' +GO +ALTER TABLE [dbo].[User] DROP CONSTRAINT [PK__User__1788CC4C21B6055D] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping constraints from [dbo].[ContentType]' +GO +ALTER TABLE [dbo].[ContentType] DROP CONSTRAINT [PK__ContentT__2026064A440B1D61] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Dropping [dbo].[ContentType]' +GO +DROP TABLE [dbo].[ContentType] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Altering [dbo].[OrderLine]' +GO +ALTER TABLE [dbo].[OrderLine] ADD +[ProductId] [int] NULL, +[SizeName] [nvarchar] (255) COLLATE Latin1_General_CI_AS NULL +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__OrderLin__29068A1034C8D9D1] on [dbo].[OrderLine]' +GO +ALTER TABLE [dbo].[OrderLine] ADD CONSTRAINT [PK__OrderLin__29068A1034C8D9D1] PRIMARY KEY CLUSTERED ([OrderLineId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating [dbo].[StockItem]' +GO +CREATE TABLE [dbo].[StockItem] +( +[StockItemId] [int] NOT NULL IDENTITY(1, 1), +[Level] [int] NULL, +[IsActive] [bit] NULL, +[ProductName] [nvarchar] (255) COLLATE Latin1_General_CI_AS NULL, +[SizeName] [nvarchar] (255) COLLATE Latin1_General_CI_AS NULL +) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__StockIte__454484BC628FA481] on [dbo].[StockItem]' +GO +ALTER TABLE [dbo].[StockItem] ADD CONSTRAINT [PK__StockIte__454484BC628FA481] PRIMARY KEY CLUSTERED ([StockItemId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating [dbo].[StockItemHistoryBase]' +GO +CREATE TABLE [dbo].[StockItemHistoryBase] +( +[StockItemHistoryBaseId] [int] NOT NULL IDENTITY(1, 1), +[StockItemHistoryType] [nvarchar] (255) COLLATE Latin1_General_CI_AS NOT NULL, +[DateTime] [datetime] NULL, +[User] [nvarchar] (255) COLLATE Latin1_General_CI_AS NULL, +[Level] [int] NULL, +[Comment] [nvarchar] (255) COLLATE Latin1_General_CI_AS NULL, +[StockItemId] [int] NULL, +[NumberOfItemsDispatched] [int] NULL, +[OrderNumber] [int] NULL, +[NumberOfItemsRecieved] [int] NULL, +[NewLevel] [int] NULL, +[OldProductName] [nvarchar] (255) COLLATE Latin1_General_CI_AS NULL, +[NewProductName] [nvarchar] (255) COLLATE Latin1_General_CI_AS NULL +) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__StockIte__D0EE2EF65EBF139D] on [dbo].[StockItemHistoryBase]' +GO +ALTER TABLE [dbo].[StockItemHistoryBase] ADD CONSTRAINT [PK__StockIte__D0EE2EF65EBF139D] PRIMARY KEY CLUSTERED ([StockItemHistoryBaseId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Altering [dbo].[Content]' +GO +ALTER TABLE [dbo].[Content] DROP +COLUMN [ContentTypeId] +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Content__2907A81E21B6055D] on [dbo].[Content]' +GO +ALTER TABLE [dbo].[Content] ADD CONSTRAINT [PK__Content__2907A81E21B6055D] PRIMARY KEY CLUSTERED ([ContentId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Basket__8FDA77B503317E3D] on [dbo].[Basket]' +GO +ALTER TABLE [dbo].[Basket] ADD CONSTRAINT [PK__Basket__8FDA77B503317E3D] PRIMARY KEY CLUSTERED ([BasketId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__BasketIt__6051AA0B7F60ED59] on [dbo].[BasketItem]' +GO +ALTER TABLE [dbo].[BasketItem] ADD CONSTRAINT [PK__BasketIt__6051AA0B7F60ED59] PRIMARY KEY CLUSTERED ([BasketItemId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Card__55FECDAE07020F21] on [dbo].[Card]' +GO +ALTER TABLE [dbo].[Card] ADD CONSTRAINT [PK__Card__55FECDAE07020F21] PRIMARY KEY CLUSTERED ([CardId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Category__19093A0B0EA330E9] on [dbo].[Category]' +GO +ALTER TABLE [dbo].[Category] ADD CONSTRAINT [PK__Category__19093A0B0EA330E9] PRIMARY KEY CLUSTERED ([CategoryId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Comment__9A619291164452B1] on [dbo].[Comment]' +GO +ALTER TABLE [dbo].[Comment] ADD CONSTRAINT [PK__Comment__9A619291164452B1] PRIMARY KEY CLUSTERED ([ObjectId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Contact__5C66259B1DE57479] on [dbo].[Contact]' +GO +ALTER TABLE [dbo].[Contact] ADD CONSTRAINT [PK__Contact__5C66259B1DE57479] PRIMARY KEY CLUSTERED ([ContactId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Country__10D1609F25869641] on [dbo].[Country]' +GO +ALTER TABLE [dbo].[Country] ADD CONSTRAINT [PK__Country__10D1609F25869641] PRIMARY KEY CLUSTERED ([CountryId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__IComment__5ADFDE571273C1CD] on [dbo].[IComment]' +GO +ALTER TABLE [dbo].[IComment] ADD CONSTRAINT [PK__IComment__5ADFDE571273C1CD] PRIMARY KEY CLUSTERED ([ICommentId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Image__7516F70C29572725] on [dbo].[Image]' +GO +ALTER TABLE [dbo].[Image] ADD CONSTRAINT [PK__Image__7516F70C29572725] PRIMARY KEY CLUSTERED ([ImageId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__MailingL__75B998BF2D27B809] on [dbo].[MailingListSubscription]' +GO +ALTER TABLE [dbo].[MailingListSubscription] ADD CONSTRAINT [PK__MailingL__75B998BF2D27B809] PRIMARY KEY CLUSTERED ([MailingListSubscriptionId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Order__C3905BCF38996AB5] on [dbo].[Order]' +GO +ALTER TABLE [dbo].[Order] ADD CONSTRAINT [PK__Order__C3905BCF38996AB5] PRIMARY KEY CLUSTERED ([OrderId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__OrderAdj__CDB07B7130F848ED] on [dbo].[OrderAdjustment]' +GO +ALTER TABLE [dbo].[OrderAdjustment] ADD CONSTRAINT [PK__OrderAdj__CDB07B7130F848ED] PRIMARY KEY CLUSTERED ([OrderAdjustmentId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__OrderSta__BC674CA13C69FB99] on [dbo].[OrderStatus]' +GO +ALTER TABLE [dbo].[OrderStatus] ADD CONSTRAINT [PK__OrderSta__BC674CA13C69FB99] PRIMARY KEY CLUSTERED ([OrderStatusId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Postage__203F354A403A8C7D] on [dbo].[Postage]' +GO +ALTER TABLE [dbo].[Postage] ADD CONSTRAINT [PK__Postage__203F354A403A8C7D] PRIMARY KEY CLUSTERED ([PostageId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__PostZone__F5DC8742440B1D61] on [dbo].[PostZone]' +GO +ALTER TABLE [dbo].[PostZone] ADD CONSTRAINT [PK__PostZone__F5DC8742440B1D61] PRIMARY KEY CLUSTERED ([PostZoneId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Product__B40CC6CD4F7CD00D] on [dbo].[Product]' +GO +ALTER TABLE [dbo].[Product] ADD CONSTRAINT [PK__Product__B40CC6CD4F7CD00D] PRIMARY KEY CLUSTERED ([ProductId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__ProductC__3224ECCE47DBAE45] on [dbo].[ProductCategory]' +GO +ALTER TABLE [dbo].[ProductCategory] ADD CONSTRAINT [PK__ProductC__3224ECCE47DBAE45] PRIMARY KEY CLUSTERED ([ProductCategoryId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__ProductI__07B2B1B84BAC3F29] on [dbo].[ProductImage]' +GO +ALTER TABLE [dbo].[ProductImage] ADD CONSTRAINT [PK__ProductI__07B2B1B84BAC3F29] PRIMARY KEY CLUSTERED ([ProductImageId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Review__9A6192911A14E395] on [dbo].[Review]' +GO +ALTER TABLE [dbo].[Review] ADD CONSTRAINT [PK__Review__9A6192911A14E395] PRIMARY KEY CLUSTERED ([ObjectId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Role__8AFACE1A534D60F1] on [dbo].[Role]' +GO +ALTER TABLE [dbo].[Role] ADD CONSTRAINT [PK__Role__8AFACE1A534D60F1] PRIMARY KEY CLUSTERED ([RoleId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__Size__83BD097A571DF1D5] on [dbo].[Size]' +GO +ALTER TABLE [dbo].[Size] ADD CONSTRAINT [PK__Size__83BD097A571DF1D5] PRIMARY KEY CLUSTERED ([SizeId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Creating primary key [PK__User__1788CC4C5AEE82B9] on [dbo].[User]' +GO +ALTER TABLE [dbo].[User] ADD CONSTRAINT [PK__User__1788CC4C5AEE82B9] PRIMARY KEY CLUSTERED ([UserId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[Content]' +GO +ALTER TABLE [dbo].[Content] ADD +CONSTRAINT [FK4FEDF4B6B6D51D18] FOREIGN KEY ([ParentContentId]) REFERENCES [dbo].[Content] ([ContentId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[StockItemHistoryBase]' +GO +ALTER TABLE [dbo].[StockItemHistoryBase] ADD +CONSTRAINT [FKBAB29AA9D1E5AA7A] FOREIGN KEY ([StockItemId]) REFERENCES [dbo].[StockItem] ([StockItemId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[BasketItem]' +GO +ALTER TABLE [dbo].[BasketItem] ADD +CONSTRAINT [FK729387BD8B64590B] FOREIGN KEY ([BasketId]) REFERENCES [dbo].[Basket] ([BasketId]), +CONSTRAINT [FK729387BD318371E4] FOREIGN KEY ([SizeId]) REFERENCES [dbo].[Size] ([SizeId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[Order]' +GO +ALTER TABLE [dbo].[Order] ADD +CONSTRAINT [FK3117099BB1026BB4] FOREIGN KEY ([CardId]) REFERENCES [dbo].[Card] ([CardId]), +CONSTRAINT [FK3117099B6BE90924] FOREIGN KEY ([DeliveryContactId]) REFERENCES [dbo].[Contact] ([ContactId]), +CONSTRAINT [FK3117099B9A379CC1] FOREIGN KEY ([CardContactId]) REFERENCES [dbo].[Contact] ([ContactId]), +CONSTRAINT [FK3117099BD8D66880] FOREIGN KEY ([OrderStatusId]) REFERENCES [dbo].[OrderStatus] ([OrderStatusId]), +CONSTRAINT [FK3117099B8703A8E0] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([UserId]), +CONSTRAINT [FK3117099BA708CB3A] FOREIGN KEY ([ModifiedById]) REFERENCES [dbo].[User] ([UserId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[ProductCategory]' +GO +ALTER TABLE [dbo].[ProductCategory] ADD +CONSTRAINT [FK4022F9D7EEC058F7] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[Category] ([CategoryId]), +CONSTRAINT [FK4022F9D7655B84E7] FOREIGN KEY ([ProductId]) REFERENCES [dbo].[Product] ([ProductId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[Category]' +GO +ALTER TABLE [dbo].[Category] ADD +CONSTRAINT [FK6482F24565E1830] FOREIGN KEY ([ParentId]) REFERENCES [dbo].[Category] ([CategoryId]), +CONSTRAINT [FK6482F242EC1B23E] FOREIGN KEY ([ImageId]) REFERENCES [dbo].[Image] ([ImageId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[Comment]' +GO +ALTER TABLE [dbo].[Comment] ADD +CONSTRAINT [FKE24667035273ED9D] FOREIGN KEY ([ObjectId]) REFERENCES [dbo].[IComment] ([ICommentId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[MailingListSubscription]' +GO +ALTER TABLE [dbo].[MailingListSubscription] ADD +CONSTRAINT [FK1CD411ADF3F5630] FOREIGN KEY ([ContactId]) REFERENCES [dbo].[Contact] ([ContactId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[Contact]' +GO +ALTER TABLE [dbo].[Contact] ADD +CONSTRAINT [FK4FF8F4B2350E3BF0] FOREIGN KEY ([CountryId]) REFERENCES [dbo].[Country] ([CountryId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[Basket]' +GO +ALTER TABLE [dbo].[Basket] ADD +CONSTRAINT [FKD4474AC2350E3BF0] FOREIGN KEY ([CountryId]) REFERENCES [dbo].[Country] ([CountryId]), +CONSTRAINT [FKD4474AC28703A8E0] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([UserId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[Review]' +GO +ALTER TABLE [dbo].[Review] ADD +CONSTRAINT [FKD28CAB635273ED9D] FOREIGN KEY ([ObjectId]) REFERENCES [dbo].[IComment] ([ICommentId]), +CONSTRAINT [FKD28CAB63655B84E7] FOREIGN KEY ([ProductId]) REFERENCES [dbo].[Product] ([ProductId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[ProductImage]' +GO +ALTER TABLE [dbo].[ProductImage] ADD +CONSTRAINT [FKD8C46FDB2EC1B23E] FOREIGN KEY ([ImageId]) REFERENCES [dbo].[Image] ([ImageId]), +CONSTRAINT [FKD8C46FDB655B84E7] FOREIGN KEY ([ProductId]) REFERENCES [dbo].[Product] ([ProductId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[OrderLine]' +GO +ALTER TABLE [dbo].[OrderLine] ADD +CONSTRAINT [FK9D642A8F71931278] FOREIGN KEY ([OrderId]) REFERENCES [dbo].[Order] ([OrderId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[OrderAdjustment]' +GO +ALTER TABLE [dbo].[OrderAdjustment] ADD +CONSTRAINT [FKF3A9DF3A71931278] FOREIGN KEY ([OrderId]) REFERENCES [dbo].[Order] ([OrderId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[Country]' +GO +ALTER TABLE [dbo].[Country] ADD +CONSTRAINT [FK2ABF29C369941CDB] FOREIGN KEY ([PostZoneId]) REFERENCES [dbo].[PostZone] ([PostZoneId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[Size]' +GO +ALTER TABLE [dbo].[Size] ADD +CONSTRAINT [FKF1577525655B84E7] FOREIGN KEY ([ProductId]) REFERENCES [dbo].[Product] ([ProductId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +PRINT N'Adding foreign keys to [dbo].[User]' +GO +ALTER TABLE [dbo].[User] ADD +CONSTRAINT [FK7185C17CEF538E9E] FOREIGN KEY ([RoleId]) REFERENCES [dbo].[Role] ([RoleId]) +GO +IF @@ERROR<>0 AND @@TRANCOUNT>0 ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT=0 BEGIN INSERT INTO #tmpErrors (Error) SELECT 1 BEGIN TRANSACTION END +GO +IF EXISTS (SELECT * FROM #tmpErrors) ROLLBACK TRANSACTION +GO +IF @@TRANCOUNT>0 BEGIN +PRINT 'The database update succeeded' +COMMIT TRANSACTION +END +ELSE PRINT 'The database update failed' +GO +DROP TABLE #tmpErrors +GO + +-- insert StockItems with initial history +insert StockItem ([Level], IsActive, ProductName, SizeName) +select + 0, + s.IsActive, + p.UrlName as ProductName, + s.Name as SizeName +from Size s +join Product p on s.ProductId = p.ProductId + +insert StockItemHistoryBase ( + StockItemHistoryType, + [DateTime], + [User], + [Level], + StockItemId +) +select + 'StockItemCreated', + GETDATE(), + 'stock control upgrade script', + 0, + StockItemId +from StockItem + +GO + +-- add IsInStock column +alter table StockItem +add IsInStock bit default 1 + +GO + +-- update is in stock +update StockItem set IsInStock = s.IsInStock +from StockItem +join (select + Size.Name as SizeName, + Product.UrlName as ProductName, + Size.IsInStock as IsInStock + from Size + join Product on Size.ProductId = Product.ProductId + where ((Size.IsActive = 1 and Product.IsActive = 1) or Size.Name = '-')) s + on s.SizeName = StockItem.SizeName and s.ProductName = StockItem.ProductName + +GO + +-- update old order lines +update OrderLine set ProductId = OrderLineNew.ProductId, SizeName = OrderLineNew.NewSizeName +from OrderLine +join ( + select + OrderLineId, + Product.ProductId, + Product.UrlName, + NewSizeName = CASE SUBSTRING(ProductName, len(Product.Name)+4, LEN(ProductName)) + WHEN '' THEN '-' + ELSE SUBSTRING(ProductName, len(Product.Name)+4, LEN(ProductName)) + END + from OrderLine + join Product on OrderLine.ProductUrlName = Product.UrlName + where OrderLine.SizeName is null + ) AS OrderLineNew on OrderLineNew.OrderLineId = OrderLine.OrderLineId +left join Size on OrderLineNew.ProductId = Size.ProductId AND ltrim(rtrim(Size.Name)) = ltrim(rtrim(NewSizeName)) AND Size.IsActive = 1 +where Size.Name is not null +GO + +-- update orders for default sizes AND any old orders with inactive sizes. +update OrderLine set ProductId = OrderLineNew.ProductId, SizeName = OrderLineNew.NewSizeName +from OrderLine +join ( + select + OrderLineId, + Product.ProductId, + Product.UrlName, + NewSizeName = CASE SUBSTRING(ProductName, len(Product.Name)+4, LEN(ProductName)) + WHEN '' THEN '-' + ELSE SUBSTRING(ProductName, len(Product.Name)+4, LEN(ProductName)) + END + from OrderLine + join Product on OrderLine.ProductUrlName = Product.UrlName + where OrderLine.SizeName is null + ) AS OrderLineNew on OrderLineNew.OrderLineId = OrderLine.OrderLineId +left join Size on OrderLineNew.ProductId = Size.ProductId AND ltrim(rtrim(Size.Name)) = ltrim(rtrim(NewSizeName)) +where Size.Name is not null +GO \ No newline at end of file diff --git a/Database/015_Update_for_answers_on_comments_2.2.0.507.sql b/Database/015_Update_for_answers_on_comments_2.2.0.507.sql new file mode 100644 index 0000000..0387fb5 --- /dev/null +++ b/Database/015_Update_for_answers_on_comments_2.2.0.507.sql @@ -0,0 +1,2 @@ +alter table IComment + add Answer nvarchar(max) null \ No newline at end of file diff --git a/Design/favicon.design b/Design/favicon.design new file mode 100644 index 0000000..0bc42f0 Binary files /dev/null and b/Design/favicon.design differ diff --git a/Design/logo.design b/Design/logo.design new file mode 100644 index 0000000..d10f9d4 Binary files /dev/null and b/Design/logo.design differ diff --git a/Design/logo.png b/Design/logo.png new file mode 100644 index 0000000..715199a Binary files /dev/null and b/Design/logo.png differ diff --git a/Design/logo_white.pdn b/Design/logo_white.pdn new file mode 100644 index 0000000..7c039da Binary files /dev/null and b/Design/logo_white.pdn differ diff --git a/Design/logo_white.png b/Design/logo_white.png new file mode 100644 index 0000000..2b1867f Binary files /dev/null and b/Design/logo_white.png differ diff --git a/Selenium_test_scripts/Add_Surprise_to_Offers b/Selenium_test_scripts/Add_Surprise_to_Offers new file mode 100644 index 0000000..9913c30 --- /dev/null +++ b/Selenium_test_scripts/Add_Surprise_to_Offers @@ -0,0 +1,67 @@ + + + + + + +Add_Surprise_to_Offers + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Add_Surprise_to_Offers
open/shop/Category
clickAndWaitlink=Online Shop
clickAndWaitlink=Frigates
clickAndWait//div[@id='mainContent']/div/div[2]/div/img
clickAndWaitlink=Edit
addSelectioncategorieslabel=Offers
clickAndWait//input[@value='Save Changes']
verifyTextPresentProduct successfully saved
clickAndWaitlink=Offers
verifyTextPresentSurprise
+ + diff --git a/Selenium_test_scripts/Add_new_admin_user b/Selenium_test_scripts/Add_new_admin_user new file mode 100644 index 0000000..a9d3629 --- /dev/null +++ b/Selenium_test_scripts/Add_new_admin_user @@ -0,0 +1,67 @@ + + + + + + +Add_new_admin_user + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Add_new_admin_user
open/shop
verifyTextPresentadmin@sutekishop.co.uk
assertTextlink=LogoutLogout
clickAndWaitlink=Users
assertTitleSuteki Shop - User
clickAndWaitlink=New User
typeUser_Emailmike@mike.com
typepasswordmike
clickAndWait//input[@value='Save']
assertTextlink=mike@mike.commike@mike.com
+ + diff --git a/Selenium_test_scripts/Attempt_to_order_out_of_stock_Surprise b/Selenium_test_scripts/Attempt_to_order_out_of_stock_Surprise new file mode 100644 index 0000000..1c8327a --- /dev/null +++ b/Selenium_test_scripts/Attempt_to_order_out_of_stock_Surprise @@ -0,0 +1,57 @@ + + + + + + +Attempt_to_order_out_of_stock_Surprise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Attempt_to_order_out_of_stock_Surprise
open/cms/Home
clickAndWaitlink=Online Shop
clickAndWaitlink=Frigates
clickAndWait//div[@id='mainContent']/div/div[2]/div/img
selectbasketItem_size_Idlabel=20cm Out of Stock
clickAndWaitaddToBasket
assertTitleSuteki Shop - Surprise
verifyTextPresentSorry, Surprise, Size 20cm is out of stock
+ + diff --git a/Selenium_test_scripts/Basket_Add_Surprise b/Selenium_test_scripts/Basket_Add_Surprise new file mode 100644 index 0000000..81c230c --- /dev/null +++ b/Selenium_test_scripts/Basket_Add_Surprise @@ -0,0 +1,82 @@ + + + + + + +Basket_Add_Surprise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Basket_Add_Surprise
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Frigates
assertTitleSuteki Shop - Frigates
clickAndWait//div[@id='mainContent']/div/div[2]/div/img
assertTitleSuteki Shop - Surprise
selectbasketItem_size_Idlabel=30cm
selectbasketItem_Quantitylabel=3
clickAndWaitaddToBasket
assertTitleSuteki Shop - Basket
assertTextlink=SurpriseSurprise
assertText//form[@id='basketForm']/table/tbody/tr[2]/td[4]£150.00
assertText//form[@id='basketForm']/table/tbody/tr[2]/td[5]£450.00
+ + diff --git a/Selenium_test_scripts/Basket_Add_Victory b/Selenium_test_scripts/Basket_Add_Victory new file mode 100644 index 0000000..88104d4 --- /dev/null +++ b/Selenium_test_scripts/Basket_Add_Victory @@ -0,0 +1,97 @@ + + + + + + +Basket_Add_Victory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Basket_Add_Victory
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Ships of the line
assertTitleSuteki Shop - Ships of the line
clickAndWait//div[@id='mainContent']/div/div[2]/div/img
assertTitleSuteki Shop - Victory
selectbasketItem_size_Idlabel=20cm
selectbasketItem_Quantitylabel=2
clickAndWaitaddToBasket
assertText//form[@id='basketForm']/table/tbody/tr[3]/td[2]20cm
assertText//form[@id='basketForm']/table/tbody/tr[3]/td[3]2
assertText//form[@id='basketForm']/table/tbody/tr[3]/td[4]£300.00
assertText//form[@id='basketForm']/table/tbody/tr[3]/td[5]£600.00
assertText//form[@id='basketForm']/table/tbody/tr[4]/td[5]£1050.00
assertText//form[@id='basketForm']/table/tbody/tr[5]/td[5]£10.00
assertText//form[@id='basketForm']/table/tbody/tr[7]/td[5]£1060.00
+ + diff --git a/Selenium_test_scripts/Basket_Remove_Victory b/Selenium_test_scripts/Basket_Remove_Victory new file mode 100644 index 0000000..561504a --- /dev/null +++ b/Selenium_test_scripts/Basket_Remove_Victory @@ -0,0 +1,52 @@ + + + + + + +Basket_Remove_Victory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Basket_Remove_Victory
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Basket
assertTitleSuteki Shop - Basket
assertTextlink=VictoryVictory
clickAndWait//form[@id='basketForm']/table/tbody/tr[3]/td[6]/a
assertText//form[@id='basketForm']/table/tbody/tr[3]/td[5]£450.00
+ + diff --git a/Selenium_test_scripts/Category_Add_Offers b/Selenium_test_scripts/Category_Add_Offers new file mode 100644 index 0000000..e4fd54b --- /dev/null +++ b/Selenium_test_scripts/Category_Add_Offers @@ -0,0 +1,52 @@ + + + + + + +Category_Add_Offers + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Category_Add_Offers
open/shop
clickAndWaitlink=Categories
clickAndWaitlink=New Category
typeCategory_NameOffers
clickCategory_IsActive
clickAndWait//input[@type='submit']
verifyTextPresentNew category has been added
+ + diff --git a/Selenium_test_scripts/Category_Add_Two b/Selenium_test_scripts/Category_Add_Two new file mode 100644 index 0000000..17aa70c --- /dev/null +++ b/Selenium_test_scripts/Category_Add_Two @@ -0,0 +1,107 @@ + + + + + + +Category_Add_Two + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Category_Add_Two
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Categories
assertTitleSuteki Shop - Category
clickAndWaitlink=New Category
typeCategory_NameFrigates
clickCategory_IsActive
clickAndWait//input[@type='submit']
verifyTextPresentNew category has been added
assertText//div[@id='mainContent']/div/div[2]/div[2]/ul/li/a[1]Frigates
clickAndWaitlink=New Category
typeCategory_NameShips of the line
clickCategory_IsActive
clickAndWait//input[@type='submit']
verifyTextPresentNew category has been added
assertText//div[@id='mainContent']/div/div[2]/div[2]/ul/li[2]/a[1]Ships of the line
assertTextlink=FrigatesFrigates
assertTextlink=Ships of the lineShips of the line
+ + diff --git a/Selenium_test_scripts/Category_Move_up_and_down b/Selenium_test_scripts/Category_Move_up_and_down new file mode 100644 index 0000000..a89e25c --- /dev/null +++ b/Selenium_test_scripts/Category_Move_up_and_down @@ -0,0 +1,52 @@ + + + + + + +Category_Move_up_and_down + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Category_Move_up_and_down
open/shop/Category
clickAndWait//div[@id='mainContent']/div/div[2]/div/ul/li[2]/a[3]/img
assertText//div[@id='mainContent']/div/div[2]/div/ul/li[1]/a[1]Ships of the line
assertText//div[@id='mainContent']/div/div[2]/div/ul/li[2]/a[1]Frigates
clickAndWait//img[@alt='Move Down']
assertText//div[@id='mainContent']/div/div[2]/div/ul/li[1]/a[1]Frigates
assertText//div[@id='mainContent']/div/div[2]/div/ul/li[2]/a[1]Ships of the line
+ + diff --git a/Selenium_test_scripts/Checkout_complete b/Selenium_test_scripts/Checkout_complete new file mode 100644 index 0000000..543a3cf --- /dev/null +++ b/Selenium_test_scripts/Checkout_complete @@ -0,0 +1,237 @@ + + + + + + +Checkout_complete + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Checkout_complete
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Basket
assertTitleSuteki Shop - Basket
assertText//form[@id='basketForm']/table/tbody/tr[6]/td[5]£460.00
clickAndWait//input[@value='Checkout']
assertTitleSuteki Shop - Checkout
assertText//div[@id='mainContent']/div[1]/div[2]/table/tbody/tr[6]/td[5]£460.00
typeCardContactFirstNameMike
typeCardContactLastNameHadlow
typeCardContactAddress15 Lovely Street
typeCardContactTownHove
typeCardContactCountyEast Sussex
typeCardContactPostcodeBN3 6BB
typeCardContactTelephone01273123456
typeEmailmike@mike.com
typeEmailConfirmmike@mike.com
typeAdditionalInformationCan I have the Captain Aubery figure as well for free?
typeCardHolderMr Mike Hadlow
typeCardNumber1111111111111117
selectCardExpiryMonthlabel=06
selectCardExpiryYearlabel=2011
selectCardStartMonthlabel=01
selectCardStartYearlabel=2007
typeCardSecurityCode123
clickContactMe
clickAndWaitsubmitButton
assertTitleSuteki Shop - Checkout
verifyTextPresent£460.00
verifyTextPresentCan I have the Captain Aubery figure as well for free?
clickAndWait//input[@value='Place Order']
assertTitleSuteki Shop - Order
verifyTextPresentOrder Confirmation
verifyTextPresent£460.00
verifyTextPresentHadlow
verifyTextPresent5 Lovely Street
verifyTextPresentHove
verifyTextPresentEast Sussex
verifyTextPresentBN3 6BB
verifyTextPresentUnited Kingdom
verifyTextPresent01273123456
assertTextlink=mike@mike.commike@mike.com
verifyTextPresentYes
verifyTextPresentCan I have the Captain Aubery figure as well for free?
+ + diff --git a/Selenium_test_scripts/Country_Add b/Selenium_test_scripts/Country_Add new file mode 100644 index 0000000..703031f --- /dev/null +++ b/Selenium_test_scripts/Country_Add @@ -0,0 +1,67 @@ + + + + + + +Country_Add + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Country_Add
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Countries
assertTitleSuteki Shop - Country
clickAndWaitlink=New Country
typeItem_NameFrance
clickItem_IsActive
selectItem_PostZone_Idlabel=Europe
clickAndWait//input[@value='Save']
assertTextlink=FranceFrance
+ + diff --git a/Selenium_test_scripts/Create_Help_Menu b/Selenium_test_scripts/Create_Help_Menu new file mode 100644 index 0000000..9b21577 --- /dev/null +++ b/Selenium_test_scripts/Create_Help_Menu @@ -0,0 +1,47 @@ + + + + + + +Create_Help_Menu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Create_Help_Menu
open/cms/Home
clickAndWaitlink=Edit this menu
clickAndWaitlink=New Menu
typeContent_NameHelp
clickAndWait//input[@value='Save Changes']
verifyTextPresentHelp
+ + diff --git a/Selenium_test_scripts/Dispatch_Order b/Selenium_test_scripts/Dispatch_Order new file mode 100644 index 0000000..6cf4dab --- /dev/null +++ b/Selenium_test_scripts/Dispatch_Order @@ -0,0 +1,97 @@ + + + + + + +Dispatch_Order + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Dispatch_Order
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Orders
assertTitleSuteki Shop - Order
clickAndWaitlink=1
assertTitleSuteki Shop - Order
verifyTextPresentMike
verifyTextPresentCan I have the Captain Aubery figure as well for free?
clickAndWait//input[@value='Dispatch']
verifyTextPresentDispatched
verifyTextPresentHadlow
verifyTextPresent5 Lovely Street
verifyTextPresentHove
verifyTextPresentEast Sussex
verifyTextPresentBN3 6BB
verifyTextPresentUnited Kingdom
+ + diff --git a/Selenium_test_scripts/Images/frigate_category.jpg b/Selenium_test_scripts/Images/frigate_category.jpg new file mode 100644 index 0000000..a5a44ad Binary files /dev/null and b/Selenium_test_scripts/Images/frigate_category.jpg differ diff --git a/Selenium_test_scripts/Images/sohpie2.jpg b/Selenium_test_scripts/Images/sohpie2.jpg new file mode 100644 index 0000000..fbcbe6d Binary files /dev/null and b/Selenium_test_scripts/Images/sohpie2.jpg differ diff --git a/Selenium_test_scripts/Images/sophie1.jpg b/Selenium_test_scripts/Images/sophie1.jpg new file mode 100644 index 0000000..7fd0530 Binary files /dev/null and b/Selenium_test_scripts/Images/sophie1.jpg differ diff --git a/Selenium_test_scripts/Images/sophie3.jpg b/Selenium_test_scripts/Images/sophie3.jpg new file mode 100644 index 0000000..842db87 Binary files /dev/null and b/Selenium_test_scripts/Images/sophie3.jpg differ diff --git a/Selenium_test_scripts/Images/surprise1.jpg b/Selenium_test_scripts/Images/surprise1.jpg new file mode 100644 index 0000000..0df3561 Binary files /dev/null and b/Selenium_test_scripts/Images/surprise1.jpg differ diff --git a/Selenium_test_scripts/Images/surprise2.jpg b/Selenium_test_scripts/Images/surprise2.jpg new file mode 100644 index 0000000..288e4a2 Binary files /dev/null and b/Selenium_test_scripts/Images/surprise2.jpg differ diff --git a/Selenium_test_scripts/Images/surprise3.jpg b/Selenium_test_scripts/Images/surprise3.jpg new file mode 100644 index 0000000..ca5bd28 Binary files /dev/null and b/Selenium_test_scripts/Images/surprise3.jpg differ diff --git a/Selenium_test_scripts/Images/victory1.jpg b/Selenium_test_scripts/Images/victory1.jpg new file mode 100644 index 0000000..1cf5d21 Binary files /dev/null and b/Selenium_test_scripts/Images/victory1.jpg differ diff --git a/Selenium_test_scripts/Images/victory2.jpg b/Selenium_test_scripts/Images/victory2.jpg new file mode 100644 index 0000000..3acfddf Binary files /dev/null and b/Selenium_test_scripts/Images/victory2.jpg differ diff --git a/Selenium_test_scripts/Images/victory3.jpg b/Selenium_test_scripts/Images/victory3.jpg new file mode 100644 index 0000000..862dcac Binary files /dev/null and b/Selenium_test_scripts/Images/victory3.jpg differ diff --git a/Selenium_test_scripts/Login b/Selenium_test_scripts/Login new file mode 100644 index 0000000..45cb8d3 --- /dev/null +++ b/Selenium_test_scripts/Login @@ -0,0 +1,61 @@ + + + + + + +Login + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Login
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Login
assertTitleSuteki Shop - Login
typeemailadmin@sutekishop.co.uk
typepasswordadmin
clickAndWait//input[@type='submit']
assertTitleSuteki Shop - Home
verifyTextPresentadmin@sutekishop.co.uk
+ + diff --git a/Selenium_test_scripts/Logout b/Selenium_test_scripts/Logout new file mode 100644 index 0000000..42f9db5 --- /dev/null +++ b/Selenium_test_scripts/Logout @@ -0,0 +1,42 @@ + + + + + + +Logout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Logout
open/shop
verifyTextPresentadmin@sutekishop.co.uk
clickAndWaitlink=Logout
verifyTextPresentGuest
assertTextlink=LoginLogin
+ + diff --git a/Selenium_test_scripts/Page_Add b/Selenium_test_scripts/Page_Add new file mode 100644 index 0000000..f77061b --- /dev/null +++ b/Selenium_test_scripts/Page_Add @@ -0,0 +1,62 @@ + + + + + + +Page_Add + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Page_Add
open/shop/Menu/List/1
clickAndWaitlink=Help
clickAndWait//div[@id='mainContent']/div[2]/div[1]/div/div/ul/li/a
clickAndWaitlink=New Page
typeContent_NameChoosing your ship
clickAndWait//input[@value='Save Changes']
verifyTextPresentChoosing your ship
clickAndWait//div[@id='mainContent']/div[2]/div[2]/table/tbody/tr/td[2]/a
clickAndWaitlink=Home
+ + diff --git a/Selenium_test_scripts/Page_Edit b/Selenium_test_scripts/Page_Edit new file mode 100644 index 0000000..74f07bb --- /dev/null +++ b/Selenium_test_scripts/Page_Edit @@ -0,0 +1,67 @@ + + + + + + +Page_Edit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Page_Edit
open/cms/Home
clickAndWaitlink=Help
clickAndWaitlink=Edit
selectFrameContent_Text_ifr
focustinymce
typetinymceAdd some text
selectFramerelative=parent
clickAndWait//input[@value='Save Changes']
clickAndWait//div[@id='mainContent']/div[2]/div[2]/table/tbody/tr/td[2]/a
verifyTextPresentAdd some text
+ + diff --git a/Selenium_test_scripts/PostZone_Add b/Selenium_test_scripts/PostZone_Add new file mode 100644 index 0000000..e92511d --- /dev/null +++ b/Selenium_test_scripts/PostZone_Add @@ -0,0 +1,77 @@ + + + + + + +PostZone_Add + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PostZone_Add
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Postage Zones
assertTitleSuteki Shop - PostZone
clickAndWaitlink=New Postage Zone
assertTitleSuteki Shop - PostZone
typeItem_NameEurope
typeItem_Multiplier1.5
typeItem_FlatRate10.00
clickItem_IsActive
clickAndWait//input[@value='Save']
assertTextlink=EuropeEurope
+ + diff --git a/Selenium_test_scripts/PostageBand_Add_A b/Selenium_test_scripts/PostageBand_Add_A new file mode 100644 index 0000000..dc153b7 --- /dev/null +++ b/Selenium_test_scripts/PostageBand_Add_A @@ -0,0 +1,82 @@ + + + + + + +PostageBand_Add_A + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PostageBand_Add_A
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Postage Bands
assertTitleSuteki Shop - Postage
clickAndWaitlink=New Postage Band
typeItem_NameA
typeItem_MaxWeight100
typeItem_Price1.00
clickItem_IsActive
clickAndWait//input[@value='Save Changes']
assertTextlink=AA
verifyTextPresent100
verifyTextPresent£1.00
+ + diff --git a/Selenium_test_scripts/PostageBand_Add_B b/Selenium_test_scripts/PostageBand_Add_B new file mode 100644 index 0000000..a06cca5 --- /dev/null +++ b/Selenium_test_scripts/PostageBand_Add_B @@ -0,0 +1,67 @@ + + + + + + +PostageBand_Add_B + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PostageBand_Add_B
open/shop/Postage
clickAndWaitlink=New Postage Band
typeItem_NameB
typeItem_MaxWeight500
typeItem_Price3.00
clickItem_IsActive
clickAndWait//input[@value='Save Changes']
assertTextlink=BB
verifyTextPresent500
verifyTextPresent£3.00
+ + diff --git a/Selenium_test_scripts/PostageBand_Move_up_and_down b/Selenium_test_scripts/PostageBand_Move_up_and_down new file mode 100644 index 0000000..9cb3a7d --- /dev/null +++ b/Selenium_test_scripts/PostageBand_Move_up_and_down @@ -0,0 +1,32 @@ + + + + + + +PostageBand_Move_up_and_down + + + + + + + + + + + + + + + + + + + + + + +
PostageBand_Move_up_and_down
open/shop/Postage
clickAndWait//div[@id='mainContent']/div/div[2]/table/tbody/tr[2]/td[5]/a/img
clickAndWait//img[@alt='Move Down']
+ + diff --git a/Selenium_test_scripts/Product_Add_Surprise b/Selenium_test_scripts/Product_Add_Surprise new file mode 100644 index 0000000..0f84b1c --- /dev/null +++ b/Selenium_test_scripts/Product_Add_Surprise @@ -0,0 +1,121 @@ + + + + + + +Product_Add_Surprise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Product_Add_Surprise
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Frigates
assertTitleSuteki Shop - Frigates
clickAndWaitlink=New Product
selectFrameDescription_ifr
focustinymce
typetinymceThe Frigate Surprise was made famous by Patrick O'Brien's Master and Commander series
selectFramerelative=parent
typeNameSurprise
typeWeight250
typePrice150.00
clickIsActive
typeSizes_0_10cm
typeSizes_1_20cm
typeSizes_2_30cm
clickAndWait//input[@value='Save Changes']
verifyValueNameSurprise
clickAndWaitlink=Preview
assertText//div[@id='mainContent']/div/div[2]/h1Surprise     £150.00
clickAndWaitlink=Home
+ + diff --git a/Selenium_test_scripts/Product_Add_Victory b/Selenium_test_scripts/Product_Add_Victory new file mode 100644 index 0000000..e461dd4 --- /dev/null +++ b/Selenium_test_scripts/Product_Add_Victory @@ -0,0 +1,126 @@ + + + + + + +Product_Add_Victory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Product_Add_Victory
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Ships of the line
assertTitleSuteki Shop - Ships of the line
clickAndWaitlink=New Product
typeNameVictory
typeWeight450
typePrice300.00
clickIsActive
selectFrameDescription_ifr
focustinymce
typetinymceHMS Victory was Nelson's flagship at the battle of Trafalguar
selectFramerelative=parent
typeSizes_0_10cm
typeSizes_1_20cm
typeSizes_2_30cm
clickAndWait//input[@value='Save Changes']
verifyValueNameVictory
clickAndWaitlink=Preview
assertText//div[@id='mainContent']/div/div[2]/h1Victory     £300.00
assertTextbasketItem_size_Id10cm 20cm 30cm
assertTextbasketItem_Quantity1 2 3 4 5 6 7 8 9 10
+ + diff --git a/Selenium_test_scripts/Product_Edit_Surprise_Add_Images b/Selenium_test_scripts/Product_Edit_Surprise_Add_Images new file mode 100644 index 0000000..89a3d74 --- /dev/null +++ b/Selenium_test_scripts/Product_Edit_Surprise_Add_Images @@ -0,0 +1,86 @@ + + + + + + +Product_Edit_Surprise_Add_Images + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Product_Edit_Surprise_Add_Images
open/shop
clickAndWaitlink=Frigates
assertTitleSuteki Shop - Frigates
clickAndWait//div[@onclick="location.href='/product/Surprise'"]
assertTitleSuteki Shop - Surprise
clickAndWaitlink=Edit
typeimage_0${projectDir}\Images\surprise1.jpg
typeimage_1${projectDir}\Images\surprise2.jpg
typeimage_2${projectDir}\Images\surprise3.jpg
clickAndWait//input[@value='Save Changes']
clickAndWaitlink=Preview
click//img[@onclick='onThumbnailClick(this)']
click//div[@id='mainContent']/div/div[2]/div[2]/div[2]/img[2]
click//div[@id='mainContent']/div/div[2]/div[2]/div[2]/img[3]
+ + diff --git a/Selenium_test_scripts/Product_Edit_Victory_Add_Images b/Selenium_test_scripts/Product_Edit_Victory_Add_Images new file mode 100644 index 0000000..6474570 --- /dev/null +++ b/Selenium_test_scripts/Product_Edit_Victory_Add_Images @@ -0,0 +1,91 @@ + + + + + + +Product_Edit_Victory_Add_Images + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Product_Edit_Victory_Add_Images
open/shop
assertTitleSuteki Shop - Home
clickAndWaitlink=Ships of the line
assertTitleSuteki Shop - Ships of the line
clickAndWait//div[@onclick="location.href='/product/Victory'"]
assertTitleSuteki Shop - Victory
clickAndWaitlink=Edit
typeimage_0${projectDir}\Images\victory1.jpg
typeimage_1${projectDir}\Images\victory2.jpg
typeimage_2${projectDir}\Images\victory3.jpg
clickAndWait//input[@value='Save Changes']
clickAndWaitlink=Preview
click//img[@onclick='onThumbnailClick(this)']
click//div[@id='mainContent']/div/div[2]/div[2]/div[2]/img[2]
click//div[@id='mainContent']/div/div[2]/div[2]/div[2]/img[3]
+ + diff --git a/Selenium_test_scripts/Set_20cm_Surprise_out_of_stock b/Selenium_test_scripts/Set_20cm_Surprise_out_of_stock new file mode 100644 index 0000000..4a0a2fa --- /dev/null +++ b/Selenium_test_scripts/Set_20cm_Surprise_out_of_stock @@ -0,0 +1,72 @@ + + + + + + +Set_20cm_Surprise_out_of_stock + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Set_20cm_Surprise_out_of_stock
open/cms/Home
clickAndWaitlink=Online Shop
clickAndWaitlink=Stock
click//div[@id='mainContent']/div/div[2]/div/form/div[5]/div[3]/input[1]
clickAndWait//input[@type='submit']
clickAndWaitlink=Offers
clickAndWait//div[@id='mainContent']/div/div[2]/div/img
verifyTextPresent20cm Out of Stock
clickAndWaitlink=Frigates
clickAndWait//div[@id='mainContent']/div/div[2]/div/img
verifyTextPresent20cm Out of Stock
+ + diff --git a/Selenium_test_scripts/Set_Project_Directory b/Selenium_test_scripts/Set_Project_Directory new file mode 100644 index 0000000..1774db3 --- /dev/null +++ b/Selenium_test_scripts/Set_Project_Directory @@ -0,0 +1,22 @@ + + + + + + +Set_Project_Directory + + + + + + + + + + + + +
Set_Project_Directory
storeEvaleditor.suiteTreeView.currentTestCase.file.path.replace(/[^\\]*$/, '')projectDir
+ + diff --git a/Selenium_test_scripts/SutekiShop_test_suite.html b/Selenium_test_scripts/SutekiShop_test_suite.html new file mode 100644 index 0000000..41d3af5 --- /dev/null +++ b/Selenium_test_scripts/SutekiShop_test_suite.html @@ -0,0 +1,35 @@ + + + + + + Test Suite + + + + + + + + + + + + + + + + + + + + + + + + + + +
Test Suite
Set_Project_Directory
Login
Add_new_admin_user
User_modify
PostZone_Add
Country_Add
PostageBand_Add_A
PostageBand_Add_B
PostageBand_Move_up_and_down
Category_Add_Two
Category_Move_up_and_down
Product_Add_Surprise
Product_Add_Victory
Product_Edit_Surprise_Add_Images
Product_Edit_Victory_Add_Images
Logout
Basket_Add_Surprise
Basket_Add_Victory
Basket_Remove_Victory
Checkout_complete
Login
Dispatch_Order
+ + diff --git a/Selenium_test_scripts/User_modify b/Selenium_test_scripts/User_modify new file mode 100644 index 0000000..cf7eecc --- /dev/null +++ b/Selenium_test_scripts/User_modify @@ -0,0 +1,62 @@ + + + + + + +User_modify + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
User_modify
open/shop
clickAndWaitlink=Users
assertTitleSuteki Shop - User
clickAndWaitlink=mike@mike.com
typepasswordmike1
clickUser_IsEnabled
clickAndWait//input[@value='Save']
verifyTextPresentChanges have been saved
clickAndWaitlink=Users
+ + diff --git a/Suteki.Shop/Dependencies.mvc3/CookComputing.XmlRpcV2.dll b/Suteki.Shop/Dependencies.mvc3/CookComputing.XmlRpcV2.dll new file mode 100644 index 0000000..0d2bf18 Binary files /dev/null and b/Suteki.Shop/Dependencies.mvc3/CookComputing.XmlRpcV2.dll differ diff --git a/Suteki.Shop/Dependencies.mvc3/HibernatingRhinos.Profiler.Appender.dll b/Suteki.Shop/Dependencies.mvc3/HibernatingRhinos.Profiler.Appender.dll new file mode 100644 index 0000000..3ede7f3 Binary files /dev/null and b/Suteki.Shop/Dependencies.mvc3/HibernatingRhinos.Profiler.Appender.dll differ diff --git a/Suteki.Shop/Dependencies.mvc3/Microsoft.ServiceModel.Samples.XmlRpc.dll b/Suteki.Shop/Dependencies.mvc3/Microsoft.ServiceModel.Samples.XmlRpc.dll new file mode 100644 index 0000000..fe66063 Binary files /dev/null and b/Suteki.Shop/Dependencies.mvc3/Microsoft.ServiceModel.Samples.XmlRpc.dll differ diff --git a/Suteki.Shop/Dependencies.mvc3/Microsoft.Web.Infrastructure.dll b/Suteki.Shop/Dependencies.mvc3/Microsoft.Web.Infrastructure.dll new file mode 100644 index 0000000..85f1138 Binary files /dev/null and b/Suteki.Shop/Dependencies.mvc3/Microsoft.Web.Infrastructure.dll differ diff --git a/Suteki.Shop/Dependencies.mvc3/System.Data.SQLite.DLL b/Suteki.Shop/Dependencies.mvc3/System.Data.SQLite.DLL new file mode 100644 index 0000000..aa398bb Binary files /dev/null and b/Suteki.Shop/Dependencies.mvc3/System.Data.SQLite.DLL differ diff --git a/Suteki.Shop/Dependencies.mvc3/System.Web.Mvc.dll b/Suteki.Shop/Dependencies.mvc3/System.Web.Mvc.dll new file mode 100644 index 0000000..eed0d99 Binary files /dev/null and b/Suteki.Shop/Dependencies.mvc3/System.Web.Mvc.dll differ diff --git a/Suteki.Shop/Dependencies.mvc3/System.Web.Razor.dll b/Suteki.Shop/Dependencies.mvc3/System.Web.Razor.dll new file mode 100644 index 0000000..cd950e6 Binary files /dev/null and b/Suteki.Shop/Dependencies.mvc3/System.Web.Razor.dll differ diff --git a/Suteki.Shop/Dependencies.mvc3/System.Web.WebPages.Deployment.dll b/Suteki.Shop/Dependencies.mvc3/System.Web.WebPages.Deployment.dll new file mode 100644 index 0000000..2e09c48 Binary files /dev/null and b/Suteki.Shop/Dependencies.mvc3/System.Web.WebPages.Deployment.dll differ diff --git a/Suteki.Shop/Dependencies.mvc3/System.Web.WebPages.Razor.dll b/Suteki.Shop/Dependencies.mvc3/System.Web.WebPages.Razor.dll new file mode 100644 index 0000000..9846dd3 Binary files /dev/null and b/Suteki.Shop/Dependencies.mvc3/System.Web.WebPages.Razor.dll differ diff --git a/Suteki.Shop/Dependencies.mvc3/System.Web.WebPages.dll b/Suteki.Shop/Dependencies.mvc3/System.Web.WebPages.dll new file mode 100644 index 0000000..9fea012 Binary files /dev/null and b/Suteki.Shop/Dependencies.mvc3/System.Web.WebPages.dll differ diff --git a/Suteki.Shop/Design/Buttons/SSL_Secured.design b/Suteki.Shop/Design/Buttons/SSL_Secured.design new file mode 100644 index 0000000..159e1e9 Binary files /dev/null and b/Suteki.Shop/Design/Buttons/SSL_Secured.design differ diff --git a/Suteki.Shop/Suteki.Common.Tests/App.config b/Suteki.Shop/Suteki.Common.Tests/App.config new file mode 100644 index 0000000..9fd381c --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/App.config @@ -0,0 +1,11 @@ + + + + +
+ + + + + + diff --git a/Suteki.Shop/Suteki.Common.Tests/Binders/BindUsingAttributeTester.cs b/Suteki.Shop/Suteki.Common.Tests/Binders/BindUsingAttributeTester.cs new file mode 100644 index 0000000..f966b44 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Binders/BindUsingAttributeTester.cs @@ -0,0 +1,69 @@ +using System; +using System.Web.Mvc; +using NUnit.Framework; +using Suteki.Common.Binders; +using Suteki.Common.Windsor; + +namespace Suteki.Common.Tests.Binders +{ + [TestFixture] + public class BindUsingAttributeTester + { + [SetUp] + public void Setup() + { + IocContainer.SetResolveFunction(Activator.CreateInstance); + } + + [TearDown] + public void Teardown() + { + IocContainer.Reset(); + } + + [Test] + public void Should_delegate_to_inner_binder() + { + var attribute = new BindUsingAttribute(typeof(TestBinder)); + attribute.GetBinder().ShouldBe(); + } + + [Test] + public void Should_throw_if_type_is_not_IModelBinder() + { + typeof(InvalidOperationException).ShouldBeThrownBy(() => new BindUsingAttribute(typeof(IDisposable))); + } + + [Test] + public void Binder_should_Accept_attribute() + { + var attribute = new BindUsingAttribute(typeof(TestBinder2)); + var binder = attribute.GetBinder() as TestBinder2; + binder.Attribute.ShouldNotBeNull(); + } + + private class TestBinder2 : IModelBinder, IAcceptsAttribute + { + public BindUsingAttribute Attribute; + + public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + return null; + } + + + public void Accept(Attribute attribute) + { + this.Attribute = (BindUsingAttribute) attribute; + } + } + + private class TestBinder : IModelBinder + { + public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + return null; + } + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Binders/DefaultModelBinderSpike.cs b/Suteki.Shop/Suteki.Common.Tests/Binders/DefaultModelBinderSpike.cs new file mode 100644 index 0000000..8a952d9 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Binders/DefaultModelBinderSpike.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Web; +using System.Web.Mvc; +using NUnit.Framework; +using Rhino.Mocks; + +namespace Suteki.Common.Tests.Binders +{ + [TestFixture, Explicit] + public class DefaultModelBinderSpike + { + private DefaultModelBinder binder; + ControllerContext controllerContext; + HttpRequestBase request; + + [SetUp] + public void SetUp() + { + binder = new DefaultModelBinder(); + controllerContext = new ControllerContext + { + HttpContext = MockRepository.GenerateStub() + }; + request = MockRepository.GenerateStub(); + controllerContext.HttpContext.Stub(x => x.Request).Return(request); + } + + [Test] + public void Lets_see_how_it_works() + { + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetModelMetadata(null), + ValueProvider = new FakeValueProvider() + }; + + var entity = binder.BindModel(controllerContext, bindingContext) as BindableEntity; + + PrintEntity(entity); + PrintErrors(bindingContext.ModelState); + } + + [Test] + public void Provide_some_data() + { + var values = new NameValueCollection + { + { "Id", "10" }, + { "Name", "The Bound Thing" }, + { "Date", "30/1/2010" }, + { "Reference", "41b16710-1f27-4bb3-8231-f8bba148396c" }, + { "Price", "123.45" } + }; + + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetModelMetadata(null), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + var entity = binder.BindModel(controllerContext, bindingContext) as BindableEntity; + + PrintEntity(entity); + PrintErrors(bindingContext.ModelState); + } + + [Test] + public void Is_it_case_sensitive() + { + var values = new NameValueCollection + { + { "id", "10" }, + { "name", "The Bound Thing" }, + { "date", "30/1/2010" }, + { "reference", "41b16710-1f27-4bb3-8231-f8bba148396c" }, + { "price", "123.45" } + }; + + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetModelMetadata(null), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + var entity = binder.BindModel(controllerContext, bindingContext) as BindableEntity; + + PrintEntity(entity); + PrintErrors(bindingContext.ModelState); + } + + [Test] + public void Use_a_prefix() + { + var values = new NameValueCollection + { + { "foo.Id", "10" }, + { "foo.Name", "The Bound Thing" }, + { "foo.Date", "30/1/2010" }, + { "foo.Reference", "41b16710-1f27-4bb3-8231-f8bba148396c" }, + { "foo.Price", "123.45" } + }; + + var bindingContext = new ModelBindingContext + { + ModelName = "foo", + ModelMetadata = GetModelMetadata(null), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + var entity = binder.BindModel(controllerContext, bindingContext) as BindableEntity; + + PrintEntity(entity); + PrintErrors(bindingContext.ModelState); + } + + [Test] + public void Include_child_entity_value() + { + var values = new NameValueCollection + { + { "Id", "10" }, + { "Name", "The Bound Thing" }, + { "Date", "30/1/2010" }, + { "Reference", "41b16710-1f27-4bb3-8231-f8bba148396c" }, + { "Price", "123.45" }, + { "ChildEntity.Name", "The Child Thing"} + }; + + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetModelMetadata(null), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + var entity = binder.BindModel(controllerContext, bindingContext) as BindableEntity; + + PrintEntity(entity); + PrintErrors(bindingContext.ModelState); + } + + [Test] + public void Providing_a_model_means_it_gets_updated() + { + var providedEntity = new BindableEntity + { + Id = 4, + Name = "The Original Name", + Date = new DateTime(1900, 1, 1), + Reference = Guid.NewGuid(), + Price = 111.11M, + ChildEntity = new ChildEntity + { + Name = "The Orginal Child" + } + }; + + var values = new NameValueCollection + { + { "Id", "10" }, + { "Name", "The Bound Thing" }, + { "Date", "30/1/2010" }, + { "Reference", "41b16710-1f27-4bb3-8231-f8bba148396c" }, + { "Price", "123.45" }, + { "ChildEntity.Name", "The Child Thing"} + }; + + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetModelMetadata(() => providedEntity), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + var entity = binder.BindModel(controllerContext, bindingContext) as BindableEntity; + + PrintEntity(entity); + PrintErrors(bindingContext.ModelState); + } + + private static ModelMetadata GetModelMetadata(Func modelAccessor) + { + return new ModelMetadata( + new DataAnnotationsModelMetadataProvider(), + null, + modelAccessor, + typeof (BindableEntity), + null); + } + + static void PrintErrors(IDictionary modelState) + { + foreach (var key in modelState.Keys) + { + foreach (var error in modelState[key].Errors) + { + Console.WriteLine(error.ErrorMessage); + } + } + } + + public void PrintEntity(BindableEntity entity) + { + if (entity == null) + { + Console.WriteLine("entity is null"); + } + + Console.Out.WriteLine("entity.Id = {0}", entity.Id); + Console.Out.WriteLine("entity.Name = {0}", entity.Name); + Console.Out.WriteLine("entity.Date = {0}", entity.Date); + Console.Out.WriteLine("entity.Reference = {0}", entity.Reference); + Console.Out.WriteLine("entity.Price = {0}", entity.Price); + Console.Out.WriteLine("entity.ChildEntity.Name = {0}", entity.ChildEntity==null ? "" : entity.ChildEntity.Name); + } + } + + public class FakeValueProvider : IValueProvider + { + public bool ContainsPrefix(string prefix) + { + Console.WriteLine("ContainsPrefix prefix = '{0}'", prefix); + return true; + } + + public ValueProviderResult GetValue(string key) + { + Console.WriteLine("GetValue key = '{0}'", key); + return null; + } + } + + public class BindableEntity + { + public int Id { get; set; } + [Required(ErrorMessage = "Name is required!!")] + public string Name { get; set; } + public DateTime Date { get; set; } + public Guid Reference { get; set; } + public decimal Price { get; set; } + + [Required(ErrorMessage = "ChildEntity was not provided")] + public ChildEntity ChildEntity { get; set; } + } + + public class ChildEntity + { + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Binders/EntityModelBinderBindingSubclassTests.cs b/Suteki.Shop/Suteki.Common.Tests/Binders/EntityModelBinderBindingSubclassTests.cs new file mode 100644 index 0000000..b5878b6 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Binders/EntityModelBinderBindingSubclassTests.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Web; +using System.Web.Mvc; +using NUnit.Framework; +using Rhino.Mocks; +using Suteki.Common.Binders; +using Suteki.Common.Models; +using Suteki.Common.Repositories; + +namespace Suteki.Common.Tests.Binders +{ + [TestFixture] + public class EntityModelBinderBindingSubclassTests + { + EntityModelBinder entityModelBinder; + const int myBaseId = 89; + MySubclass mySubclass; + + ControllerContext controllerContext; + HttpRequestBase request; + + [SetUp] + public void SetUp() + { + mySubclass = new MySubclass {Id = myBaseId, Name = "the name"}; + + var repository = MockRepository.GenerateStub(); + var repositoryResolver = MockRepository.GenerateStub(); + + repositoryResolver.Stub(r => r.GetRepository(typeof (MyBase))).Return(repository); + repositoryResolver.Stub(r => r.GetRepository(typeof (MySubclass))).Return(repository); + repository.Stub(r => r.GetById(myBaseId)).Return(mySubclass); + + entityModelBinder = new EntityModelBinder(repositoryResolver); + + controllerContext = new ControllerContext + { + HttpContext = MockRepository.GenerateStub() + }; + request = MockRepository.GenerateStub(); + controllerContext.HttpContext.Stub(x => x.Request).Return(request); + } + + [Test] + public void Binder_should_bind_subclass_name_correctly() + { + var values = new NameValueCollection + { + { "Id", myBaseId.ToString() }, + { "Name", "The New Name" } + }; + + var bindingContext = new ModelBindingContext + { + // bind only works if the subclass type is specified in the model metadata + ModelMetadata = GetModelMetadata(null, typeof(MySubclass)), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + entityModelBinder.BindModel(controllerContext, bindingContext); + + mySubclass.Name.ShouldEqual("The New Name"); + } + + private static ModelMetadata GetModelMetadata(Func modelAccessor, Type theBoundType) + { + return new ModelMetadata( + new DataAnnotationsModelMetadataProvider(), + null, + modelAccessor, + theBoundType, + null); + } + + static void PrintErrors(IDictionary modelState) + { + foreach (var key in modelState.Keys) + { + foreach (var error in modelState[key].Errors) + { + Console.WriteLine(error.ErrorMessage); + Assert.Fail("Model binding errors occured"); + } + } + } + } + + public class MyBase : IEntity + { + public int Id { get; set; } + } + + public class MySubclass : MyBase + { + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Binders/EntityModelBinderTests.cs b/Suteki.Shop/Suteki.Common.Tests/Binders/EntityModelBinderTests.cs new file mode 100644 index 0000000..bfbe702 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Binders/EntityModelBinderTests.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Web; +using System.Web.Mvc; +using NUnit.Framework; +using Rhino.Mocks; +using Suteki.Common.Binders; +using Suteki.Common.Models; +using Suteki.Common.Repositories; + +namespace Suteki.Common.Tests.Binders +{ + [TestFixture] + public class EntityModelBinderTests + { + IRepositoryResolver repositoryResolver; + IRepository parentRepository; + IRepository childRepository; + + EntityModelBinder entityModelBinder; + ControllerContext controllerContext; + HttpRequestBase request; + + Parent parent; + + [SetUp] + public void SetUp() + { + parent = new Parent + { + Id = 10, + Name = "Parent from repository", + Child = new Child + { + Id = 3, + Name = "Child of Parent from repository" + } + }; + + parentRepository = new FakeRepository(id => parent); + childRepository = new FakeRepository(id => parent.Child); + + repositoryResolver = MockRepository.GenerateStub(); + repositoryResolver.Stub(r => r.GetRepository(typeof (Parent))).Return(parentRepository); + repositoryResolver.Stub(r => r.GetRepository(typeof (Child))).Return(childRepository); + + entityModelBinder = new EntityModelBinder(repositoryResolver); + controllerContext = new ControllerContext + { + HttpContext = MockRepository.GenerateStub() + }; + request = MockRepository.GenerateStub(); + controllerContext.HttpContext.Stub(x => x.Request).Return(request); + + entityModelBinder.SetModelBinderDictionary(new ModelBinderDictionary { DefaultBinder = entityModelBinder }); + } + + [Test] + public void Should_get_child_from_repository() + { + var values = new NameValueCollection + { + { "Id", "10" }, + { "Name", "The Parent" }, + { "Child.Id", "3" }, + { "Child.Name", "The Child" } + }; + + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetModelMetadata(null), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + entityModelBinder.Accept(new EntityBindAttribute { Fetch = false }); + var boundParent = entityModelBinder.BindModel(controllerContext, bindingContext) as Parent; + + boundParent.ShouldNotBeNull("Parent is null"); + boundParent.Id.ShouldEqual(10); + boundParent.Name.ShouldEqual("The Parent"); + boundParent.ShouldBeNotBeTheSameAs(parent); + + boundParent.Child.Id.ShouldEqual(3); + boundParent.Child.ShouldBeTheSameAs(boundParent.Child); + + // expect the Child.Name to have been updated, even though the actual child entity is sourced + // from the repository + boundParent.Child.Name.ShouldEqual("The Child"); + + PrintErrors(bindingContext.ModelState); + } + + [Test] + public void Should_not_get_child_from_repository_if_id_is_not_given() + { + var values = new NameValueCollection + { + { "Id", "10" }, + { "Name", "The Parent" }, + { "Child.Name", "The Child" } + }; + + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetModelMetadata(null), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + entityModelBinder.Accept(new EntityBindAttribute { Fetch = false }); + var boundParent = entityModelBinder.BindModel(controllerContext, bindingContext) as Parent; + + boundParent.ShouldNotBeNull("Parent is null"); + boundParent.Id.ShouldEqual(10); + boundParent.Name.ShouldEqual("The Parent"); + boundParent.ShouldBeNotBeTheSameAs(parent); + + boundParent.Child.Id.ShouldEqual(0); + boundParent.Child.Name.ShouldEqual("The Child"); + boundParent.Child.ShouldBeNotBeTheSameAs(parent.Child); + + PrintErrors(bindingContext.ModelState); + } + + [Test] + public void Should_get_parent_from_repository_if_fetch_is_true() + { + var values = new NameValueCollection + { + { "Id", "10" }, + { "Child.Name", "Updated child name" } + }; + + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetModelMetadata(null), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + entityModelBinder.Accept(new EntityBindAttribute { Fetch = true }); + var boundParent = entityModelBinder.BindModel(controllerContext, bindingContext) as Parent; + + boundParent.ShouldNotBeNull("Parent is null"); + boundParent.Id.ShouldEqual(10); + boundParent.Name.ShouldEqual("Parent from repository"); + boundParent.ShouldBeTheSameAs(parent); + + boundParent.Child.Id.ShouldEqual(3); + boundParent.Child.Name.ShouldEqual("Updated child name"); + boundParent.Child.ShouldBeTheSameAs(parent.Child); + + PrintErrors(bindingContext.ModelState); + } + + [Test] + public void Should_get_child_if_child_id_is_missing() + { + var values = new NameValueCollection + { + { "Id", "10" } + }; + + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetModelMetadata(null), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + entityModelBinder.Accept(new EntityBindAttribute { Fetch = true }); + var boundParent = entityModelBinder.BindModel(controllerContext, bindingContext) as Parent; + + boundParent.ShouldNotBeNull("Parent is null"); + boundParent.Id.ShouldEqual(10); + boundParent.Name.ShouldEqual("Parent from repository"); + boundParent.ShouldBeTheSameAs(parent); + + boundParent.Child.Id.ShouldEqual(3); + boundParent.Child.ShouldBeTheSameAs(parent.Child); + } + + [Test] + public void Should_not_get_parent_from_repository_if_no_id_is_given() + { + var values = new NameValueCollection + { + { "Name", "Parent Name" }, + { "Child.Name", "Child Name" } + }; + + var bindingContext = new ModelBindingContext + { + ModelMetadata = GetModelMetadata(null), + ValueProvider = new NameValueCollectionValueProvider(values, CultureInfo.GetCultureInfo("EN-GB")) + }; + + entityModelBinder.Accept(new EntityBindAttribute { Fetch = true }); // but id not set + var boundParent = entityModelBinder.BindModel(controllerContext, bindingContext) as Parent; + + boundParent.ShouldNotBeNull("Parent is null"); + boundParent.Id.ShouldEqual(0); + boundParent.Name.ShouldEqual("Parent Name"); + boundParent.ShouldBeNotBeTheSameAs(parent); + + boundParent.Child.Id.ShouldEqual(0); + boundParent.Child.Name.ShouldEqual("Child Name"); + boundParent.Child.ShouldBeNotBeTheSameAs(parent.Child); + + PrintErrors(bindingContext.ModelState); + } + + private static ModelMetadata GetModelMetadata(Func modelAccessor) + { + return new ModelMetadata( + new DataAnnotationsModelMetadataProvider(), + null, + modelAccessor, + typeof(Parent), + null); + } + + static void PrintErrors(IDictionary modelState) + { + foreach (var key in modelState.Keys) + { + foreach (var error in modelState[key].Errors) + { + Console.WriteLine(error.ErrorMessage); + Assert.Fail("Model binding errors occured"); + } + } + } + + private class Parent : IEntity + { + public int Id { get; set; } + public string Name { get; set; } + public Child Child { get; set; } + } + + private class Child : IEntity + { + public int Id { get; set; } + public string Name { get; set; } + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Events/DomainEventsTests.cs b/Suteki.Shop/Suteki.Common.Tests/Events/DomainEventsTests.cs new file mode 100644 index 0000000..96de9e6 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Events/DomainEventsTests.cs @@ -0,0 +1,95 @@ +// ReSharper disable InconsistentNaming +using System; +using System.Reflection; +using Castle.MicroKernel.Registration; +using Castle.Windsor; +using NUnit.Framework; +using Suteki.Common.Events; + +namespace Suteki.Common.Tests.Events +{ + [TestFixture] + public class DomainEventsTests + { + [Test] + public void Should_be_able_to_test_raising_a_domain_event() + { + var @event = new SomethingAwesomeHappened(); + IDomainEvent rasisedEvent = null; + + DomainEvent.RaiseAction = e => rasisedEvent = e; + DomainEvent.Raise(@event); + + rasisedEvent.ShouldBeTheSameAs(@event); + + DomainEvent.RaiseAction = null; + } + + [Test] + public void Should_be_able_to_resolve_the_event_handler() + { + using (var container = new WindsorContainer() + .Register( + AllTypes.FromAssembly(Assembly.GetExecutingAssembly()) + .BasedOn(typeof(IHandle<>)).WithService.Base() + .Configure(c => c.LifeStyle.Transient) + )) + { + var handlers = container.ResolveAll>(); + + handlers.Length.ShouldEqual(2); + handlers[0].ShouldBe(); + handlers[1].ShouldBe(); + } + } + + [Test] + public void Should_execute_handlers_on_event() + { + var messages = new System.Collections.Generic.List(); + + using(var container = new WindsorContainer() + .Register( + Component.For>().Instance(messages.Add), + AllTypes.FromAssembly(Assembly.GetExecutingAssembly()) + .BasedOn(typeof(IHandle<>)).WithService.Base() + .Configure(c => c.LifeStyle.Transient) + )) + { + DomainEvent.ReturnContainer = () => container; + + var @event = new SomethingAwesomeHappened(); + DomainEvent.Raise(@event); + + DomainEvent.ReturnContainer = null; + } + + messages.Count.ShouldEqual(2); + messages[0].ShouldEqual("handler1"); + messages[1].ShouldEqual("handler2"); + } + } + + public class SomethingAwesomeHappened : IDomainEvent + { + } + + public class SendEmailWhenSomethingAwesomeHappened : IHandle + { + public Action EventTestLogger { get; set; } + public void Handle(SomethingAwesomeHappened @event) + { + EventTestLogger("handler1"); + } + } + + public class DoSomethingElseWhenAwesomeHappened : IHandle + { + public Action EventTestLogger { get; set; } + public void Handle(SomethingAwesomeHappened @event) + { + EventTestLogger("handler2"); + } + } +} +// ReSharper restore InconsistentNaming \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Extensions/TypeExtensionTests.cs b/Suteki.Shop/Suteki.Common.Tests/Extensions/TypeExtensionTests.cs new file mode 100644 index 0000000..3858531 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Extensions/TypeExtensionTests.cs @@ -0,0 +1,38 @@ +using NUnit.Framework; +using Suteki.Common.Extensions; +using Suteki.Common.Models; + +namespace Suteki.Common.Tests.Extensions +{ + [TestFixture] + public class TypeExtensionTests + { + private class SomeEntity : IEntity + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + } + + private class SomeOtherEntity : IEntity + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + } + + [Test] + public void Should_be_able_to_get_primary_key_of_SomeEntity() + { + var id = typeof (SomeEntity).GetPrimaryKey(); + id.Name.ShouldEqual("Id"); + } + + [Test] + public void Should_be_able_to_get_primary_key_of_SomeOtherType() + { + var id = typeof (SomeOtherEntity).GetPrimaryKey(); + id.Name.ShouldEqual("Id"); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Extensions/UrlExtensionsHelperTests.cs b/Suteki.Shop/Suteki.Common.Tests/Extensions/UrlExtensionsHelperTests.cs new file mode 100644 index 0000000..0094a04 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Extensions/UrlExtensionsHelperTests.cs @@ -0,0 +1,65 @@ +using System; +using System.Web.Mvc; +using NUnit.Framework; +using Suteki.Common.Extensions; +using Rhino.Mocks; + +namespace Suteki.Common.Tests.Extensions +{ + [TestFixture] + public class UrlExtensionsHelperTests + { + private UrlExtensionsHelper urlExtensionsHelper; + private MockRepository mocks; + + [SetUp] + public void SetUp() + { + mocks = new MockRepository(); + urlExtensionsHelper = mocks.PartialMock(); + urlExtensionsHelper.Stub(eh => eh.UseSsl()).Return(true).Repeat.Any(); + } + + [Test] + public void ToSslUrl_ShouldAddHttpsToExistingUrlWithQueryString() + { + const string currentUrl = "http://jtg.sutekishop.co.uk/shop/Order/UpdateCountry/66?countryId=1"; + urlExtensionsHelper.Stub(eh => eh.GetRequestUri()).Return(new Uri(currentUrl)); + + var existingUrl = MvcHtmlString.Create("/shop/Order/PlaceOrder"); + const string expectedUrl = "https://jtg.sutekishop.co.uk/shop/Order/PlaceOrder"; + + mocks.ReplayAll(); + + Assert.That(urlExtensionsHelper.ToSslUrl(existingUrl).ToString(), Is.EqualTo(expectedUrl)); + } + + [Test] + public void ToSslUrl_ShouldAddHttpsToExistingUrlWithoutQueryString() + { + const string currentUrl = "http://jtg.sutekishop.co.uk/shop/Order/UpdateCountry/66"; + urlExtensionsHelper.Expect(eh => eh.GetRequestUri()).Return(new Uri(currentUrl)); + + var existingUrl = MvcHtmlString.Create("/shop/Order/PlaceOrder"); + const string expectedUrl = "https://jtg.sutekishop.co.uk/shop/Order/PlaceOrder"; + + mocks.ReplayAll(); + + Assert.That(urlExtensionsHelper.ToSslUrl(existingUrl).ToString(), Is.EqualTo(expectedUrl)); + } + + [Test] + public void ToSslUrl_ShouldAddHttpsToExistingUrlWithHttps() + { + const string currentUrl = "https://jtg.sutekishop.co.uk/shop/Order/UpdateCountry/66"; + urlExtensionsHelper.Expect(eh => eh.GetRequestUri()).Return(new Uri(currentUrl)); + + var existingUrl = MvcHtmlString.Create("/shop/Order/PlaceOrder"); + const string expectedUrl = "https://jtg.sutekishop.co.uk/shop/Order/PlaceOrder"; + + mocks.ReplayAll(); + + Assert.That(urlExtensionsHelper.ToSslUrl(existingUrl).ToString(), Is.EqualTo(expectedUrl)); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Filters/FilterUsingAttributeTester.cs b/Suteki.Shop/Suteki.Common.Tests/Filters/FilterUsingAttributeTester.cs new file mode 100644 index 0000000..845b218 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Filters/FilterUsingAttributeTester.cs @@ -0,0 +1,118 @@ +using System; +using System.Web.Mvc; +using NUnit.Framework; +using Suteki.Common.Filters; +using Suteki.Common.Tests; +using Suteki.Common.Windsor; + +namespace Suteki.Shop.Tests.Filters +{ + [TestFixture] + public class FilterUsingAttributeTester + { + [SetUp] + public void Setup() + { + IocContainer.SetResolveFunction(Activator.CreateInstance); + } + + [TearDown] + public void Teardown() + { + IocContainer.Reset(); + } + + [Test] + public void Should_store_filter_types() + { + var attribute = new FilterUsingAttribute(typeof (TestActionFilter)); + attribute.FilterType.ShouldEqual(typeof (TestActionFilter)); + } + + [Test] + public void Should_throw_if_type_is_not_filter() + { + typeof (InvalidOperationException).ShouldBeThrownBy(() => new FilterUsingAttribute(typeof (IDisposable))); + } + + [Test] + public void Should_delegate_to_actionfilter_executing() + { + var attribute = new FilterUsingAttribute(typeof(TestActionFilter)); + var context = new ActionExecutingContext(); + attribute.OnActionExecuting(context); + context.Result.ShouldBe(); + } + + [Test] + public void Should_delegate_to_actionfilter_executed() + { + var attribute = new FilterUsingAttribute(typeof(TestActionFilter)); + var context = new ActionExecutedContext(); + attribute.OnActionExecuted(context); + context.Result.ShouldBe(); + } + + [Test] + public void Should_delegate_to_authorization_filter() + { + var attribute = new FilterUsingAttribute(typeof(TestAuthFilter)); + var context = new AuthorizationContext(); + attribute.OnAuthorization(context); + context.Result.ShouldBe(); + } + + [Test] + public void Should_delegate_to_result_filter_executing() + { + var attribute = new FilterUsingAttribute(typeof(TestResultFilter)); + var context = new ResultExecutingContext(); + attribute.OnResultExecuting(context); + context.Result.ShouldBe(); + } + + [Test] + public void Should_delegate_to_result_filter_executed() + { + var attribute = new FilterUsingAttribute(typeof(TestResultFilter)); + var context = new ResultExecutedContext(); + attribute.OnResultExecuted(context); + context.Result.ShouldBe(); + } + + + private class TestAuthFilter : IAuthorizationFilter + { + public void OnAuthorization(AuthorizationContext filterContext) + { + filterContext.Result = new EmptyResult(); + } + } + + private class TestResultFilter : IResultFilter + { + public void OnResultExecuting(ResultExecutingContext filterContext) + { + filterContext.Result = new EmptyResult(); + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + filterContext.Result = new EmptyResult(); + } + } + + private class TestActionFilter : IActionFilter + { + public void OnActionExecuting(ActionExecutingContext filterContext) + { + filterContext.Result = new EmptyResult(); + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + filterContext.Result = new EmptyResult(); + } + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Filters/UnitOfWorkFilterTester.cs b/Suteki.Shop/Suteki.Common.Tests/Filters/UnitOfWorkFilterTester.cs new file mode 100644 index 0000000..e0b5769 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Filters/UnitOfWorkFilterTester.cs @@ -0,0 +1,78 @@ +using System.Web.Mvc; +using NHibernate; +using NUnit.Framework; +using Rhino.Mocks; +using Suteki.Common.Filters; +using Suteki.Common.Repositories; +using Suteki.Common.Tests.TestHelpers; + +namespace Suteki.Common.Tests.Filters +{ + [TestFixture] + public class UnitOfWorkFilterTester + { + ISessionManagerFactory sessionManagerFactory; + ISessionManager sessionManager; + ISession session; + ITransaction transaction; + UnitOfWorkFilter filter; + + [SetUp] + public void Setup() + { + sessionManagerFactory = MockRepository.GenerateStub(); + sessionManager = MockRepository.GenerateStub(); + session = MockRepository.GenerateStub(); + transaction = MockRepository.GenerateStub(); + + sessionManagerFactory.Stub(x => x.Resolve()).Return(sessionManager).Repeat.Any(); + sessionManager.Stub(s => s.OpenSession()).Return(session).Repeat.Any(); + session.Stub(s => s.BeginTransaction()).Return(transaction).Repeat.Any(); + + var perActionTransactionStore = new MockPerActionTransactionStore(); + + filter = new UnitOfWorkFilter(perActionTransactionStore, sessionManagerFactory); + } + + [Test] + public void Transaction_should_be_started_when_action_is_run() + { + filter.OnActionExecuting(new ActionExecutingContext { Controller = new TestController() }); + session.AssertWasCalled(s => s.BeginTransaction()); + } + + [Test] + public void Transaction_should_be_commited_when_action_completes() + { + var controller = new TestController(); + filter.OnActionExecuting(new ActionExecutingContext { Controller = controller }); + filter.OnActionExecuted(new ActionExecutedContext { Controller = controller }); + transaction.AssertWasCalled(t => t.Commit()); + } + + [Test] + public void Transaction_should_be_rolled_back_if_there_are_errors_in_modelstate() + { + var controller = new TestController(); + controller.ModelState.AddModelError("foo", "bar"); + filter.OnActionExecuting(new ActionExecutingContext { Controller = controller }); + filter.OnActionExecuted(new ActionExecutedContext { Controller = controller }); + transaction.AssertWasCalled(t => t.Rollback()); + } + } + + public class MockPerActionTransactionStore : IPerActionTransactionStore + { + private ITransaction transaction; + + public void StoreTransaction(ActionExecutingContext filterContext, ITransaction transaction) + { + this.transaction = transaction; + } + + public ITransaction RetrieveTransaction(ActionExecutedContext filterContext) + { + return transaction; + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/HtmlHelpers/ComboForTests.cs b/Suteki.Shop/Suteki.Common.Tests/HtmlHelpers/ComboForTests.cs new file mode 100644 index 0000000..9c5f514 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/HtmlHelpers/ComboForTests.cs @@ -0,0 +1,63 @@ +// ReSharper disable InconsistentNaming +using System; +using System.Linq; +using NUnit.Framework; +using Suteki.Common.HtmlHelpers; +using Suteki.Common.Models; +using Rhino.Mocks; +using Suteki.Common.Repositories; +using Suteki.Common.Tests.TestHelpers; + +namespace Suteki.Common.Tests.HtmlHelpers +{ + [TestFixture] + public class ComboForTests + { + private ComboFor comboFor; + private IRepository repository; + + [SetUp] + public void SetUp() + { + repository = MockRepository.GenerateStub>(); + comboFor = new ComboFor(repository) + { + HtmlHelper = MvcTestHelpers.CreateMockHtmlHelper() + }; + } + + [Test] + public void BindTo_should_return_the_correct_select_list() + { + var ids = new[] {2, 3}; + var loopups = new System.Collections.Generic.List() + { + new ComboForLookup {Id = 1, Name = "one"}, + new ComboForLookup {Id = 2, Name = "two"}, + new ComboForLookup {Id = 3, Name = "three"}, + new ComboForLookup {Id = 4, Name = "four"}, + }; + repository.Stub(x => x.GetAll()).Return(loopups.AsQueryable()); + + var result = comboFor.Multiple().BoundTo("PropertyName", ids); + //Console.Out.WriteLine("result = {0}", result); + result.ShouldEqual(expectedSelectList); + } + + private const string expectedSelectList = +@""; + } + + public class ComboForModel{} + public class ComboForLookup : INamedEntity + { + public int Id { get; set; } + public string Name { get; set; } + } +} + +// ReSharper restore InconsistentNaming diff --git a/Suteki.Shop/Suteki.Common.Tests/HtmlHelpers/PostActionTests.cs b/Suteki.Shop/Suteki.Common.Tests/HtmlHelpers/PostActionTests.cs new file mode 100644 index 0000000..ab5d891 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/HtmlHelpers/PostActionTests.cs @@ -0,0 +1,89 @@ +// ReSharper disable InconsistentNaming +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Web.Mvc; +using NUnit.Framework; +using Suteki.Common.Models; +using Suteki.Common.HtmlHelpers; +using Suteki.Common.Tests.TestHelpers; + +namespace Suteki.Common.Tests.HtmlHelpers +{ + [TestFixture] + public class PostActionTests + { + PostAction postAction; + StringWriter writer; + + [SetUp] + public void SetUp() + { + writer = new StringWriter(); + var htmlHelper = MvcTestHelpers.CreateMockHtmlHelper(writer); + var entity = new TestEntity {Id = 4}; + + postAction = new PostAction(htmlHelper, c => c.DoSomething(entity), "the button text"); + } + + [Test] + public void Render_should_render_the_form_correctly() + { + postAction.Render(); + + var result = writer.GetStringBuilder().ToString(); + + Console.Out.WriteLine("result = {0}", result); + } + + [Test] + public void GetExpressionDetails_should_work() + { + var entity = new TestEntity { Id = 4 }; + Expression> action = c => c.DoSomething(entity); + + var expressionDetails = PostAction.GetExpressionDetails(action); + + expressionDetails.MethodName.ShouldEqual("DoSomething"); + expressionDetails.IdValue.ShouldEqual(4); + } + + [Test] + public void GetExpressionDetails_should_work_on_an_enumerated_type() + { + var entities = new List + { + new TestEntity { Id = 3 }, + new TestEntity { Id = 5 }, + new TestEntity { Id = 7 } + }; + + foreach (var entity in entities) + { + Expression> action = c => c.DoSomething(entity); + var expressionDetails = PostAction.GetExpressionDetails(action); + + expressionDetails.MethodName.ShouldEqual("DoSomething"); + expressionDetails.IdValue.ShouldEqual(entity.Id); + } + } + + const string expectedResult = @""; + } + + public class TestController : Controller + { + public ActionResult DoSomething(TestEntity entity) + { + return null; + } + } + + public class TestEntity : IEntity + { + public int Id { get; set; } + } +} +// ReSharper restore InconsistentNaming \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Models/MoneyTests.cs b/Suteki.Shop/Suteki.Common.Tests/Models/MoneyTests.cs new file mode 100644 index 0000000..3938ba4 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Models/MoneyTests.cs @@ -0,0 +1,56 @@ +// ReSharper disable InconsistentNaming +using NUnit.Framework; +using Suteki.Common.Models; + +namespace Suteki.Common.Tests.Models +{ + [TestFixture] + public class MoneyTests + { + [Test] + public void Should_be_able_to_add_money() + { + var result = new Money(2.32M) + new Money(3.44M); + result.Amount.ShouldEqual(5.76M); + } + + [Test] + public void Should_be_able_to_do_other_arithmetic_with_money() + { + var x = new Money(43.5M); + var result = x * 3 + x/2 - 4; + result.Amount.ShouldEqual(148.25M); + } + + [Test] + public void ToString_should_not_include_currency_symbol() + { + new Money(342.11M).ToString().ShouldEqual("342.11"); + } + + [Test] + public void ToStringWithSymbol_should_include_currency_symbol() + { + new Money(123.45M).ToStringWithSymbol().ShouldEqual("123.45"); + } + + // ReSharper disable EqualExpressionComparison + [Test] + public void Equals_should_work() + { + var result1 = new Money(123.45M) == new Money(123.45M); + result1.ShouldBeTrue(); + + var result2 = new Money(123.45M) == null; + result2.ShouldBeFalse(); + + var result3 = null == new Money(123.45M); + result3.ShouldBeFalse(); + + var result4 = new Money(123.45M) == new Money(123.46M); + result4.ShouldBeFalse(); + } + // ReSharper restore EqualExpressionComparison + } +} +// ReSharper restore InconsistentNaming \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Models/StringValueExtensionsTests.cs b/Suteki.Shop/Suteki.Common.Tests/Models/StringValueExtensionsTests.cs new file mode 100644 index 0000000..4c64b9a --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Models/StringValueExtensionsTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using Suteki.Common.Models; + +namespace Suteki.Common.Tests.Models +{ + [TestFixture] + public class StringValueExtensionsTests + { + [Test] + public void ToStringValue_ShouldConvertIntEnumerableToStringValueEnumerable() + { + var ints = new[] {1, 2, 3, 4}; + var enumerator = ints.ToStringValues().GetEnumerator(); + + enumerator.MoveNext(); + Assert.That(enumerator.Current.Value, Is.EqualTo("01")); + enumerator.MoveNext(); + Assert.That(enumerator.Current.Value, Is.EqualTo("02")); + enumerator.MoveNext(); + Assert.That(enumerator.Current.Value, Is.EqualTo("03")); + enumerator.MoveNext(); + Assert.That(enumerator.Current.Value, Is.EqualTo("04")); + } + + [Test] + public void AddBlankFirstValue_ShouldAddABlankValueToBeginingOfList() + { + var values = new[] + { + new StringValue("First"), + new StringValue("Second") + }; + var enumerator = values.AddBlankFirstValue().GetEnumerator(); + + enumerator.MoveNext(); + Assert.That(enumerator.Current.Value, Is.EqualTo("0")); + enumerator.MoveNext(); + Assert.That(enumerator.Current.Value, Is.EqualTo("First")); + enumerator.MoveNext(); + Assert.That(enumerator.Current.Value, Is.EqualTo("Second")); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Properties/AssemblyInfo.cs b/Suteki.Shop/Suteki.Common.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2531ccb --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,24 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Suteki.Common.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Suteki.Common.Tests")] +[assembly: AssemblyCopyright("Copyright © 2008")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("25355d4c-7098-4f9e-8bb0-37afe92bdbda")] + diff --git a/Suteki.Shop/Suteki.Common.Tests/Services/EmailBuilderTester.cs b/Suteki.Shop/Suteki.Common.Tests/Services/EmailBuilderTester.cs new file mode 100644 index 0000000..2cf932c --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Services/EmailBuilderTester.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using NUnit.Framework; +using NVelocity.Runtime; +using Suteki.Common.Services; + +namespace Suteki.Common.Tests.Services +{ + [TestFixture] + public class EmailBuilderTester + { + IEmailBuilder builder; + const string testView = "test"; + const string testViewWithPlaceholders = "testWithPlaceholders"; + + [SetUp] + public void Setup() + { + builder = new EmailBuilder("Services\\Templates"); + } + + [Test] + public void Should_render_template() + { + var result = builder.GetEmailContent(testView, new Dictionary()); + result.ShouldEqual("Test Email"); + } + + [Test] + public void Should_throw_when_template_does_not_exist() + { + typeof(InvalidOperationException).ShouldBeThrownBy(() => + builder.GetEmailContent("foo", new Dictionary()) + ); + } + + [Test] + public void Should_merge_viewdata() + { + var viewdata = new Dictionary { { "name", "Jeremy" } }; + var result = builder.GetEmailContent(testViewWithPlaceholders, viewdata); + result.ShouldEqual("Hello Jeremy"); + } + + [Test] + public void Throws_when_view_is_null() + { + typeof(ArgumentException).ShouldBeThrownBy(() => builder.GetEmailContent(null, new Dictionary())); + } + + [Test] + public void Throws_when_viewdata_null() + { + typeof(ArgumentNullException).ShouldBeThrownBy(() => builder.GetEmailContent(testView, null)); + } + + [Test] + public void Parses_partial_views() + { + var result = builder.GetEmailContent("PartialTest", new Dictionary()); + result.ShouldEqual("Rendered by Partial"); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Services/EmailSenderTests.cs b/Suteki.Shop/Suteki.Common.Tests/Services/EmailSenderTests.cs new file mode 100644 index 0000000..80bce55 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Services/EmailSenderTests.cs @@ -0,0 +1,50 @@ +using NUnit.Framework; +using Suteki.Common.Services; + +namespace Suteki.Common.Tests.Services +{ + [TestFixture] + public class EmailSenderTests + { + private EmailSender emailSender; + + private const string smtpAddress = "smtp.sutekishop.co.uk"; + private const string fromAddress = "info@sutekishop.co.uk"; + private const string username = "admin"; + private const string password = "adm1n"; + + [SetUp] + public void SetUp() + { + emailSender = new EmailSender(smtpAddress, fromAddress, username, password); + } + + [Test] + public void HasNetworkCredentials_Should_return_true() + { + Assert.That(emailSender.HasNetworkCredentials(), Is.True); + } + + [Test] + public void BuildMessage_Should_return_correct_MailMessage() + { + var subject = "The subject"; + var body = "The body"; + var isHtml = true; + var toAddresses = new[] + { + "mike@sutekishop.co.uk", + "info@sutekishop.co.uk" + }; + + var message = emailSender.BuildMessage(subject, body, isHtml, toAddresses); + + Assert.That(message.Subject, Is.EqualTo(subject)); + Assert.That(message.Body, Is.EqualTo(body)); + Assert.That(message.IsBodyHtml, Is.True); + Assert.That(message.To.Count, Is.EqualTo(2)); + Assert.That(message.To[0].Address, Is.EqualTo(toAddresses[0])); + Assert.That(message.To[1].Address, Is.EqualTo(toAddresses[1])); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Services/Templates/PartialTest.vm b/Suteki.Shop/Suteki.Common.Tests/Services/Templates/PartialTest.vm new file mode 100644 index 0000000..f0b6aea --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Services/Templates/PartialTest.vm @@ -0,0 +1 @@ +#parse("_testPartial.vm") \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Services/Templates/_testPartial.vm b/Suteki.Shop/Suteki.Common.Tests/Services/Templates/_testPartial.vm new file mode 100644 index 0000000..a7905e4 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Services/Templates/_testPartial.vm @@ -0,0 +1 @@ +Rendered by Partial \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Services/Templates/test.vm b/Suteki.Shop/Suteki.Common.Tests/Services/Templates/test.vm new file mode 100644 index 0000000..3a02086 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Services/Templates/test.vm @@ -0,0 +1 @@ +Test Email \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Services/Templates/testWithPlaceholders.vm b/Suteki.Shop/Suteki.Common.Tests/Services/Templates/testWithPlaceholders.vm new file mode 100644 index 0000000..4d8868d --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Services/Templates/testWithPlaceholders.vm @@ -0,0 +1 @@ +Hello $name \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Spikes/NamesSpike.cs b/Suteki.Shop/Suteki.Common.Tests/Spikes/NamesSpike.cs new file mode 100644 index 0000000..886ae2f --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Spikes/NamesSpike.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; + +namespace Suteki.Common.Tests.Spikes +{ + public class NamesSpike + { + public void Example() + { + var centre = new Centre + { + Name = new Text + { + Description = "The centre's name", + } + }; + + centre.Name.Translations.Add(new Translation + { + TranslationText = "Brighton", + Language = new Language + { + Name = "English", + IsDefault = true + } + }); + + Console.WriteLine(centre.Name); + } + } + + public class Centre + { + public Text Name { get; set; } + } + + public class Text + { + public string Description { get; set; } + private readonly IList translations = new List(); + + public IList Translations + { + get { return translations; } + } + + public override string ToString() + { + foreach (var translation in translations) + { + if (translation.Language.Name == Language.Current.Name) return translation.ToString(); + } + throw new ApplicationException("No default translation found"); + } + } + + public class Translation + { + public Text Text { get; set; } + public Language Language { get; set; } + public string TranslationText { get; set; } + + public override string ToString() + { + return TranslationText; + } + } + + public class Language + { + public string Name { get; set; } + public bool IsDefault { get; set; } + + public static Language Current + { + get + { + return new Language {Name = "English", IsDefault = true}; + } + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Spikes/ReflectionSpikes.cs b/Suteki.Shop/Suteki.Common.Tests/Spikes/ReflectionSpikes.cs new file mode 100644 index 0000000..b2900ee --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Spikes/ReflectionSpikes.cs @@ -0,0 +1,45 @@ +using System; +using System.Reflection; +using NUnit.Framework; + +namespace Suteki.Common.Tests.Spikes +{ + [TestFixture] + public class ReflectionSpikes + { + [Test] + public void DeclaringTypeIsNotInterfaceType() + { + var thing = new Thing(); + PropertyInfo propertyInfo = thing.GetType().GetProperty("Id"); + Assert.That(propertyInfo.DeclaringType, Is.Not.EqualTo(typeof(IThing))); + } + + [Test] + public void ReflectionOnAnonymousTypes() + { + var id = 4; + var name = "Freddy"; + var now = DateTime.Now; + + var anonymous = new { id, name, now }; + + var idValue = (int)anonymous.GetType().GetProperty("id").GetValue(anonymous, null); + Assert.That(idValue, Is.EqualTo(id)); + var nameValue = (string)anonymous.GetType().GetProperty("name").GetValue(anonymous, null); + Assert.That(nameValue, Is.EqualTo(name)); + var nowValue = (DateTime)anonymous.GetType().GetProperty("now").GetValue(anonymous, null); + Assert.That(nowValue, Is.EqualTo(now)); + } + + public interface IThing + { + int Id { get; set; } + } + + public class Thing : IThing + { + public int Id { get; set; } + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Spikes/WindsorBugs.cs b/Suteki.Shop/Suteki.Common.Tests/Spikes/WindsorBugs.cs new file mode 100644 index 0000000..3f7345e --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Spikes/WindsorBugs.cs @@ -0,0 +1,27 @@ +using Castle.MicroKernel.Registration; +using Castle.Windsor; +using NUnit.Framework; + +namespace Suteki.Common.Tests.Spikes +{ + public class WindsorBugs + { + public void AllInterfaces_not_working_with_generic_interfaces() + { + var container = new WindsorContainer().Register( + AllTypes.FromThisAssembly().BasedOn(typeof(IThing<>)).WithService.AllInterfaces() + ); + + // Throws Object reference not set to an instance of an object. + var things = container.ResolveAll>(); + + Assert.That(things.Length == 1); + } + + public class A{} + public class B{} + + public interface IThing{} + public class Thing : IThing, IThing {} + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Suteki.Common.Tests.csproj b/Suteki.Shop/Suteki.Common.Tests/Suteki.Common.Tests.csproj new file mode 100644 index 0000000..c9a9711 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Suteki.Common.Tests.csproj @@ -0,0 +1,191 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {DF5362C9-C088-46DD-9678-0767DA6F6AC2} + Library + Properties + Suteki.Common.Tests + Suteki.Common.Tests + v4.0 + 512 + + + + + + + + + + + 3.5 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + AllRules.ruleset + + + + ..\packages\Castle.Core.2.5.2\lib\NET35\Castle.Core.dll + + + False + ..\packages\Castle.Services.Transaction.3.0.7.2007\lib\net40\Castle.Services.Transaction.dll + + + ..\packages\Castle.Windsor.2.5.3\lib\NET40\Castle.Windsor.dll + + + False + ..\packages\FluentNHibernate.1.2.0.712\lib\FluentNHibernate.dll + + + ..\packages\Iesi.Collections.3.2.0.2001\lib\Net35\Iesi.Collections.dll + + + ..\packages\log4net.1.2.10\lib\2.0\log4net.dll + + + False + ..\packages\Mvc3Futures.3.0.20105.0\lib\Microsoft.Web.Mvc.dll + + + False + ..\packages\MvcContrib.Mvc3-ci.3.0.68.0\lib\MvcContrib.dll + + + False + ..\packages\NHibernate.3.1.0.4000\lib\Net35\NHibernate.dll + + + False + ..\packages\NHibernate.Castle.3.1.0.4000\lib\Net35\NHibernate.ByteCode.Castle.dll + + + False + ..\packages\NUnit.2.5.10.11092\lib\nunit.framework.dll + + + False + ..\packages\NUnit.2.5.10.11092\lib\nunit.mocks.dll + + + ..\packages\NVelocity.1.0.3\lib\NVelocity.dll + + + False + ..\packages\NUnit.2.5.10.11092\lib\pnunit.framework.dll + + + ..\packages\RhinoMocks.3.6\lib\Rhino.Mocks.dll + + + + 3.5 + + + ..\packages\Rx-Core.1.0.2856.0\lib\Net4\System.CoreEx.dll + + + + ..\packages\Rx-Interactive.1.0.2856.0\lib\Net4\System.Interactive.dll + + + ..\packages\Rx-Main.1.0.2856.0\lib\Net4\System.Reactive.dll + + + + + + 3.5 + + + False + ..\Dependencies.mvc3\System.Web.Mvc.dll + + + + + + Version.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {5A734DD0-928C-4A1E-8D12-7DDD607D89C6} + Suteki.Common + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/TestExtensions.cs b/Suteki.Shop/Suteki.Common.Tests/TestExtensions.cs new file mode 100644 index 0000000..bc8be75 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/TestExtensions.cs @@ -0,0 +1,66 @@ +using System; +using NUnit.Framework; + +namespace Suteki.Common.Tests +{ + public static class TestExtensions + { + public static T ShouldNotBeNull(this T obj) + { + Assert.IsNotNull(obj); + return obj; + } + + public static T ShouldNotBeNull(this T obj, string message) + { + Assert.IsNotNull(obj, message); + return obj; + } + + public static T ShouldEqual(this T actual, object expected) + { + Assert.AreEqual(expected, actual); + return actual; + } + + public static Exception ShouldBeThrownBy(this Type exceptionType, TestDelegate testDelegate) + { + return Assert.Throws(exceptionType, testDelegate); + } + + public static void ShouldBe(this object actual) + { + Assert.IsInstanceOf(actual); + } + + public static void ShouldBeNull(this object actual) + { + Assert.IsNull(actual); + } + + public static void ShouldBeTheSameAs(this object actual, object expected) + { + Assert.AreSame(expected, actual); + } + + public static void ShouldBeNotBeTheSameAs(this object actual, object expected) + { + Assert.AreNotSame(expected, actual); + } + + public static T CastTo(this object source) + { + return (T)source; + } + + public static void ShouldBeTrue(this bool source) + { + Assert.IsTrue(source); + } + + public static void ShouldBeFalse(this bool source) + { + Assert.IsFalse(source); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/TestHelpers/MvcTestHelpers.cs b/Suteki.Shop/Suteki.Common.Tests/TestHelpers/MvcTestHelpers.cs new file mode 100644 index 0000000..a06aa93 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/TestHelpers/MvcTestHelpers.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using System.IO; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; +using Rhino.Mocks; + +namespace Suteki.Common.Tests.TestHelpers +{ + public class MvcTestHelpers + { + public static HtmlHelper CreateMockHtmlHelper(TextWriter writer, RouteData routeData) + { + ViewContext viewContext; + IViewDataContainer viewDataContainer = GetViewDataContainer(writer, routeData, out viewContext); + + return new HtmlHelper(viewContext, viewDataContainer); + } + + public static HtmlHelper CreateMockHtmlHelper(TextWriter writer, RouteData routeData) + { + ViewContext viewContext; + IViewDataContainer viewDataContainer = GetViewDataContainer(writer, routeData, out viewContext); + + return new HtmlHelper(viewContext, viewDataContainer); + } + + private static IViewDataContainer GetViewDataContainer(TextWriter writer, RouteData routeData, out ViewContext viewContext) + { + var mocks = new MockRepository(); + + if (writer == null) + { + throw new ArgumentNullException("writer"); + } + + mocks.Record(); + + var contextItems = new Hashtable(); + var form = new NameValueCollection(); + var httpContext = mocks.StrictMock(); + var httpRequestBase = mocks.StrictMock(); + var httpResponseBase = mocks.StrictMock(); + var view = mocks.Stub(); + + httpContext.Expect(context => context.Request).Return(httpRequestBase).Repeat.Any(); + httpContext.Expect(context => context.Response).Return(httpResponseBase).Repeat.Any(); + httpContext.Expect(context => context.Items).Return(contextItems).Repeat.Any(); + + httpRequestBase.Expect(request => request.Form).Return(form).Repeat.Any(); + httpRequestBase.Expect(request => request.QueryString).Return(form).Repeat.Any(); + httpRequestBase.Expect(request => request.RequestType).Return("GET").Repeat.Any(); + + httpResponseBase.Expect(response => response.Output).Return(writer).Repeat.Any(); + httpResponseBase.Expect(response => response.Write(null)) + .Callback(s => { writer.Write(s); return true; }).Repeat.Any(); + + if (routeData == null) + { + routeData = new RouteData(); + routeData.Values.Add("action", "Index"); + routeData.Values.Add("controller", "Home"); + } + + var controller = MockRepository.GenerateMock(); + + var viewDataDictionary = new ViewDataDictionary(); + + viewContext = new ViewContext(new ControllerContext(httpContext, routeData, controller), view, viewDataDictionary, new TempDataDictionary(), writer); + + var viewDataContainer = mocks.StrictMock(); + viewDataContainer.Expect(vdc => vdc.ViewData).Return(viewDataDictionary).Repeat.Any(); + + mocks.ReplayAll(); + return viewDataContainer; + } + + public static HtmlHelper CreateMockHtmlHelper(TextWriter writer) + { + return CreateMockHtmlHelper(writer, null); + } + + public static HtmlHelper CreateMockHtmlHelper(TextWriter writer) + { + return CreateMockHtmlHelper(writer, null); + } + + public static HtmlHelper CreateMockHtmlHelper() + { + var writer = new StringWriter(); + return CreateMockHtmlHelper(writer, null); + } + + public static HtmlHelper CreateMockHtmlHelper() + { + var writer = new StringWriter(); + return CreateMockHtmlHelper(writer, null); + } + + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/TestHelpers/TestController.cs b/Suteki.Shop/Suteki.Common.Tests/TestHelpers/TestController.cs new file mode 100644 index 0000000..fd3816a --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/TestHelpers/TestController.cs @@ -0,0 +1,9 @@ +using System.Web.Mvc; + +namespace Suteki.Common.Tests.TestHelpers +{ + public class TestController : Controller + { + + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Utils/ExpressionHelperTests.cs b/Suteki.Shop/Suteki.Common.Tests/Utils/ExpressionHelperTests.cs new file mode 100644 index 0000000..d810e2f --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Utils/ExpressionHelperTests.cs @@ -0,0 +1,34 @@ +// ReSharper disable InconsistentNaming +using System; +using NUnit.Framework; +using Suteki.Common.Utils; + +namespace Suteki.Common.Tests.Utils +{ + [TestFixture] + public class ExpressionHelperTests + { + [Test] + public void Should_be_able_to_get_property_name_from_expression() + { + var propertyName = ExpressionHelper.GetDottedPropertyNameFromExpression(g => g.Parent.Child.Name); + propertyName.ShouldEqual("Parent.Child.Name"); + } + } + + public class Grandparent + { + public Parent Parent { get; set; } + } + + public class Parent + { + public Child Child { get; set; } + } + + public class Child + { + public string Name { get; set; } + } +} +// ReSharper restore InconsistentNaming \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Utils/UriBuilderTests.cs b/Suteki.Shop/Suteki.Common.Tests/Utils/UriBuilderTests.cs new file mode 100644 index 0000000..730a0d2 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Utils/UriBuilderTests.cs @@ -0,0 +1,70 @@ +// ReSharper disable InconsistentNaming +using System.Web.Routing; +using System.Web.Mvc; +using System.Web; +using NUnit.Framework; + +namespace Suteki.Common.Tests.Utils +{ + [TestFixture] + public class UriBuilderTests + { + [SetUp] + public void SetUp() + { + } + + [Test] + public void CreateUriFromRouteValues_should_create_the_correct_virtual_path() + { + var routes = new RouteCollection(); + routes.MapRoute( + "ProgRock", + "{band}/{album}/{track}", + new { controller = "Home", action = "Index", id = UrlParameter.Optional }); + + var uriBuilder = new Suteki.Common.Utils.UriBuilder(() => routes, () => new FakeHttpContext()); + + var uri = uriBuilder.CreateUriFromRouteValues(new + { + band = "Yes", + album = "Fragile", + track = "Roundabout", + info = "great keyboard solo" + }); + + uri.ShouldEqual("/Yes/Fragile/Roundabout?info=great%20keyboard%20solo"); + } + } + + public class FakeHttpContext : HttpContextBase + { + public override HttpRequestBase Request + { + get { return new FakeRequest(); } + } + + public override HttpResponseBase Response + { + get { return new FakeResponse(); } + } + } + + public class FakeRequest : HttpRequestBase + { + public override string ApplicationPath + { + get { return "/"; } + } + } + + public class FakeResponse : HttpResponseBase + { + public override string ApplyAppPathModifier(string virtualPath) + { + return virtualPath; + } + } +} + +// ReSharper restore InconsistentNaming diff --git a/Suteki.Shop/Suteki.Common.Tests/Validation/ValidationExtensionsTests.cs b/Suteki.Shop/Suteki.Common.Tests/Validation/ValidationExtensionsTests.cs new file mode 100644 index 0000000..2f8a4b0 --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Validation/ValidationExtensionsTests.cs @@ -0,0 +1,27 @@ +using System.Web.Mvc; +using NUnit.Framework; +using Suteki.Common.Validation; + +namespace Suteki.Common.Tests.Validation +{ + [TestFixture] + public class ValidationExtensionsTests + { + public void CollectingExceptionsSpike() + { + var validator = new Validator + { + () => { throw new ValidationException("the first one"); }, + () => { throw new ValidationException("the second one"); }, + () => { throw new ValidationException("the third one"); }, + }; + + var modelState = new ModelStateDictionary(); + validator.Validate(modelState); + + modelState["validation_error_0"].Errors[0].ErrorMessage.ShouldEqual("the first one"); + modelState["validation_error_1"].Errors[0].ErrorMessage.ShouldEqual("the second one"); + modelState["validation_error_2"].Errors[0].ErrorMessage.ShouldEqual("the third one"); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/Windsor/ArrayResolverTests.cs b/Suteki.Shop/Suteki.Common.Tests/Windsor/ArrayResolverTests.cs new file mode 100644 index 0000000..45937ce --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/Windsor/ArrayResolverTests.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using Castle.MicroKernel.Registration; +using NUnit.Framework; +using Castle.MicroKernel; +using Castle.MicroKernel.Handlers; +using Suteki.Common.Windsor; + +namespace Suteki.Common.Tests.Windsor +{ + [TestFixture] + public class ArrayResolverTests + { + [Test, ExpectedException(typeof(HandlerException))] + public void DoesNotResolveArraysByDefault() + { + var kernel = new DefaultKernel(); + + kernel.Register( + Component.For(), + Component.For().ImplementedBy(), + Component.For().ImplementedBy(), + Component.For().ImplementedBy() + ); + + var thing = kernel.Resolve(); + } + + [Test] + public void ShouldResolveArrayOfDependencies() + { + var kernel = new DefaultKernel(); + kernel.Resolver.AddSubResolver(new ArrayResolver(kernel)); + + kernel.Register( + Component.For(), + Component.For().ImplementedBy(), + Component.For().ImplementedBy(), + Component.For().ImplementedBy() + ); + + var thing = kernel.Resolve(); + + Assert.That(thing.SubThings.Count, Is.EqualTo(3)); + Assert.That(thing.SubThings[0], Is.InstanceOf(typeof(First))); + Assert.That(thing.SubThings[1], Is.InstanceOf(typeof(Second))); + Assert.That(thing.SubThings[2], Is.InstanceOf(typeof(Third))); + } + + [Test] + public void DoesNotDiscoverCircularDependencies() + { + var kernel = new DefaultKernel(); + kernel.Resolver.AddSubResolver(new ArrayResolver(kernel)); + + // a circular reference exception should be thrown here + kernel.Register( + Component.For(), + Component.For().ImplementedBy() + ); + + // this crashes the test framework! + // var thing = kernel.Resolve(); + } + + public class Thing + { + readonly List subThings = new List(); + + public Thing(params ISubThing[] subThings) + { + this.subThings.AddRange(subThings); + } + + public List SubThings + { + get { return subThings; } + } + } + + public interface ISubThing {} + + public class First : ISubThing {} + public class Second : ISubThing {} + public class Third : ISubThing {} + + public class Circular : ISubThing + { + public Thing Thing { get; private set; } + + public Circular(Thing thing) + { + this.Thing = thing; + } + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common.Tests/packages.config b/Suteki.Shop/Suteki.Common.Tests/packages.config new file mode 100644 index 0000000..8b075db --- /dev/null +++ b/Suteki.Shop/Suteki.Common.Tests/packages.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/AddIns/AddinControllerBase.cs b/Suteki.Shop/Suteki.Common/AddIns/AddinControllerBase.cs new file mode 100644 index 0000000..3d2b636 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/AddIns/AddinControllerBase.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using System.Linq; +using System.Web.Mvc; +using Suteki.Common.Extensions; + +namespace Suteki.Common.AddIns +{ + public class AddinControllerBase : Controller + { + /// + /// Override View to provde a resource path for the view file. + /// + /// + /// + /// + /// + protected override ViewResult View(string viewName, string masterName, object model) + { + if (viewName == null) + { + throw new ArgumentNullException("viewName", "You must provide a view name in AddinControllers"); + } + + var assembly = this.GetType().Assembly; + var assemblyFileName = Path.GetFileName(assembly.Location); + var assmblyName = assembly.GetName().Name; + var controllerName = this.GetType().Name; + if (!controllerName.EndsWith("Controller")) + { + throw new ApplicationException( + "Controllers must have a name ending with Controller, e.g: CustomerController"); + } + var controllerShortenedName = controllerName.Substring(0, controllerName.Length - 10); + + // ~/App_Resource/Mike.MefAreas.AddIn.dll/Mike.MefAreas.AddIn/Views/Customer/ + var viewPath = string.Format("~/App_Resource/{0}/{1}/Views/{2}/", assemblyFileName, assmblyName, controllerShortenedName); + + // Suteki.Shop.StockControl.AddIn.Views.StockControl.List.ascx + var resourcePath = "{0}.Views.{1}.{2}.".With(assmblyName, controllerShortenedName, viewName); + var extension = "aspx"; + if (!ResourceExists(resourcePath + extension)) + { + extension = "ascx"; + if (!ResourceExists(resourcePath + extension)) + { + throw new SutekiCommonException("Could not find view resource at '{0}aspx' or '{0}ascx' in assembly {1}", + resourcePath, + GetType().Assembly); + } + } + + return base.View(viewPath + viewName + "." + extension, masterName, model); + } + + private bool ResourceExists(string resrouceName) + { + return GetType().Assembly.GetManifestResourceNames().Any(x => x == resrouceName); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/AddIns/AssemblyResourceVirtualPathProvider.cs b/Suteki.Shop/Suteki.Common/AddIns/AssemblyResourceVirtualPathProvider.cs new file mode 100644 index 0000000..879604a --- /dev/null +++ b/Suteki.Shop/Suteki.Common/AddIns/AssemblyResourceVirtualPathProvider.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Web; +using System.Web.Caching; +using System.Web.Hosting; + +namespace Suteki.Common.AddIns +{ + // From http://www.codeproject.com/KB/aspnet/ASP2UserControlLibrary.aspx + + public class AssemblyResourceVirtualPathProvider : VirtualPathProvider + { + private readonly Dictionary nameAssemblyCache; + + public AssemblyResourceVirtualPathProvider() + { + nameAssemblyCache = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + } + + private static bool IsAppResourcePath(string virtualPath) + { + string checkPath = VirtualPathUtility.ToAppRelative(virtualPath); + + return checkPath.StartsWith("~/App_Resource/", + StringComparison.InvariantCultureIgnoreCase); + } + + public override bool FileExists(string virtualPath) + { + return (IsAppResourcePath(virtualPath) || + base.FileExists(virtualPath)); + } + + public override VirtualFile GetFile(string virtualPath) + { + if (IsAppResourcePath(virtualPath)) + { + return new AssemblyResourceFile(nameAssemblyCache, virtualPath); + } + + return base.GetFile(virtualPath); + } + + public override CacheDependency GetCacheDependency( + string virtualPath, + IEnumerable virtualPathDependencies, + DateTime utcStart) + { + if (IsAppResourcePath(virtualPath)) + { + return null; + } + + return base.GetCacheDependency(virtualPath, + virtualPathDependencies, utcStart); + } + + private class AssemblyResourceFile : VirtualFile + { + private readonly IDictionary nameAssemblyCache; + private readonly string assemblyPath; + + public AssemblyResourceFile(IDictionary nameAssemblyCache, string virtualPath) : + base(virtualPath) + { + this.nameAssemblyCache = nameAssemblyCache; + assemblyPath = VirtualPathUtility.ToAppRelative(virtualPath); + } + + public override Stream Open() + { + // ~/App_Resource/WikiExtension.dll/WikiExtension/Presentation/Views/Wiki/Index.aspx (or .ascx) + var parts = assemblyPath.Split(new[] { '/' }, 4); + + if (parts.Length != 4 || parts[0] != "~" || parts[1] != "App_Resource") + { + throw new SutekiCommonException("Wrong number of parts in assmbly path: '{0}'. Expected ~/App_Resource//.aspx (or .ascx)"); + } + + var assemblyName = parts[2]; + var resourceName = parts[3].Replace('/', '.'); + + Assembly assembly; + + lock (nameAssemblyCache) + { + if (!nameAssemblyCache.TryGetValue(assemblyName, out assembly)) + { + var path = Path.Combine(HttpRuntime.BinDirectory, assemblyName); + assembly = Assembly.LoadFrom(path); + + // TODO: Assert is not null + nameAssemblyCache[assemblyName] = assembly; + } + } + + if (assembly == null) + { + throw new SutekiCommonException("Could not load AddIn assembly '{0}' when attempting to load AddIn view '{1}'", assemblyName, assemblyPath); + } + + var resourceStream = assembly.GetManifestResourceStream(resourceName); + + if (resourceStream == null) + { + throw new SutekiCommonException("Could not load AddIn view. Failed to find resource '{0}' in assembly '{1}'", resourceName, assemblyName); + } + + return resourceStream; + } + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Binders/BindUsingAttribute.cs b/Suteki.Shop/Suteki.Common/Binders/BindUsingAttribute.cs new file mode 100644 index 0000000..f7b8ad2 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Binders/BindUsingAttribute.cs @@ -0,0 +1,34 @@ +using System; +using System.Web.Mvc; +using Suteki.Common.Extensions; +using Suteki.Common.Windsor; + +namespace Suteki.Common.Binders +{ + public class BindUsingAttribute : CustomModelBinderAttribute + { + private readonly Type binderType; + + public BindUsingAttribute(Type binderType) + { + if(!typeof(IModelBinder).IsAssignableFrom(binderType)) + { + throw new InvalidOperationException("Type '{0}' does not implement IModelBinder.".With(binderType.Name)); + } + + this.binderType = binderType; + } + + public override IModelBinder GetBinder() + { + var binder = (IModelBinder) IocContainer.Resolve(binderType); + + if(binder is IAcceptsAttribute) + { + ((IAcceptsAttribute)binder).Accept(this); + } + + return binder; + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Binders/EntityModelBinder.cs b/Suteki.Shop/Suteki.Common/Binders/EntityModelBinder.cs new file mode 100644 index 0000000..8afed68 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Binders/EntityModelBinder.cs @@ -0,0 +1,137 @@ +using System; +using System.Web.Mvc; +using Suteki.Common.Extensions; +using Suteki.Common.Models; +using Suteki.Common.Repositories; + +namespace Suteki.Common.Binders +{ + public class EntityBindAttribute : BindUsingAttribute + { + public EntityBindAttribute() + : this(typeof(EntityModelBinder)) + { + } + + protected EntityBindAttribute(Type binderType) + : base(binderType) + { + // by default we always fetch any model that implements IEntity + Fetch = true; + } + + public bool Fetch { get; set; } + } + + public class EntityModelBinder : DefaultModelBinder, IAcceptsAttribute + { + readonly IRepositoryResolver repositoryResolver; + EntityBindAttribute declaringAttribute; + + public EntityModelBinder(IRepositoryResolver repositoryResolver) + { + this.repositoryResolver = repositoryResolver; + } + + protected override object CreateModel( + ControllerContext controllerContext, + ModelBindingContext bindingContext, + Type modelType) + { + if (modelType.IsEntity() && FetchFromRepository) + { + var id = GetIdFromValueProvider(bindingContext, modelType); + if (id.HasValue && id.Value != 0) + { + var repository = repositoryResolver.GetRepository(modelType); + object entity; + try + { + entity = repository.GetById(id.Value); + } + finally + { + repositoryResolver.Release(repository); + } + return entity; + } + } + + // Fall back to default model creation if the target is not an existing entity + return base.CreateModel(controllerContext, bindingContext, modelType); + } + + protected override object GetPropertyValue( + ControllerContext controllerContext, + ModelBindingContext bindingContext, + System.ComponentModel.PropertyDescriptor propertyDescriptor, + IModelBinder propertyBinder) + { + // any child entity property which has been changed, needs to be retrieved by calling CreateModel, + // we can force BindComplexModel (in DefaultModelBinder) do this by + // setting the bindingContext.ModelMetadata.Model to null + var entity = bindingContext.ModelMetadata.Model as IEntity; + if (entity != null) + { + var id = GetIdFromValueProvider(bindingContext, bindingContext.ModelType); + if (id.HasValue && id.Value != entity.Id) + { + bindingContext.ModelMetadata.Model = null; + } + } + return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder); + } + + + private static int? GetIdFromValueProvider(ModelBindingContext bindingContext, Type modelType) + { + var fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, modelType.GetPrimaryKey().Name); + if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey)) + { + return null; + } + + var result = bindingContext.ValueProvider.GetValue(fullPropertyKey); + if (result == null) return null; + var idAsObject = result.ConvertTo(typeof (Int32)); + if (idAsObject == null) return null; + return (int) idAsObject; + } + + public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + bindingContext.ModelMetadata.ConvertEmptyStringToNull = false; + var model = base.BindModel(controllerContext, bindingContext); + ValidateEntity(bindingContext, controllerContext, model); + return model; + } + + protected virtual void ValidateEntity( + ModelBindingContext bindingContext, + ControllerContext controllerContext, + object entity) + { + // override to provide additional validation. + } + + private bool FetchFromRepository + { + get + { + // by default we always fetch any model that implements IEntity + return declaringAttribute == null ? true : declaringAttribute.Fetch; + } + } + + public virtual void Accept(Attribute attribute) + { + declaringAttribute = (EntityBindAttribute)attribute; + } + + // For unit tests + public void SetModelBinderDictionary(ModelBinderDictionary modelBinderDictionary) + { + Binders = modelBinderDictionary; + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Binders/IAcceptsAttribute.cs b/Suteki.Shop/Suteki.Common/Binders/IAcceptsAttribute.cs new file mode 100644 index 0000000..f793918 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Binders/IAcceptsAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Suteki.Common.Binders +{ + public interface IAcceptsAttribute + { + void Accept(Attribute attribute); + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Binders/MoneyBinder.cs b/Suteki.Shop/Suteki.Common/Binders/MoneyBinder.cs new file mode 100644 index 0000000..7a1722b --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Binders/MoneyBinder.cs @@ -0,0 +1,32 @@ +using System; +using System.Web.Mvc; +using Suteki.Common.Models; + +namespace Suteki.Common.Binders +{ + public class MoneyBinder : IModelBinder + { + public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + { + if (!typeof (Money).IsAssignableFrom(bindingContext.ModelType)) + { + throw new SutekiCommonException( + "MoneyBinder has attempted to bind to type '{0}', but may only bind to Money", + bindingContext.ModelType); + } + + var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); + + try + { + var decimalValue = (decimal)valueProviderResult.ConvertTo(typeof(decimal)); + return new Money(decimalValue); + } + catch (Exception) + { + bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Not a valid price."); + return new Money(0M); + } + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Controllers/ControllerBase.cs b/Suteki.Shop/Suteki.Common/Controllers/ControllerBase.cs new file mode 100644 index 0000000..0c5b14c --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Controllers/ControllerBase.cs @@ -0,0 +1,11 @@ +using System.Web.Mvc; +using Suteki.Common.Services; + +namespace Suteki.Common.Controllers +{ + [HandleError] + public abstract class ControllerBase : Controller + { + public IDebugWritingService DebugWritingService { get; set; } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Controllers/OrderableScaffoldController.cs b/Suteki.Shop/Suteki.Common/Controllers/OrderableScaffoldController.cs new file mode 100644 index 0000000..9d05f40 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Controllers/OrderableScaffoldController.cs @@ -0,0 +1,42 @@ +using System.Web.Mvc; +using MvcContrib.Pagination; +using Suteki.Common.Filters; +using Suteki.Common.Repositories; +using Suteki.Common.Services; +using Suteki.Common.ViewData; + +namespace Suteki.Common.Controllers +{ + public class OrderableScaffoldController : ScaffoldController where T : class, IOrderable, new() + { + public IOrderableService OrderableService { get; set; } + + protected override ActionResult RenderIndexView(int? page) + { + var items = Repository.GetAll().InOrder().AsPagination(page ?? 1); + return View("Index", ScaffoldView.Data().With(items)); + } + + public override ActionResult New() + { + T item = new T + { + Position = OrderableService.NextPosition + }; + return View("Edit", (object)BuildEditViewData().With(item)); + } + + [UnitOfWork] + public virtual ActionResult MoveUp(int id, int? page) + { + OrderableService.MoveItemAtPosition(id).UpOne(); + return RedirectToAction("Index"); + } + [UnitOfWork] + public virtual ActionResult MoveDown(int id, int? page) + { + OrderableService.MoveItemAtPosition(id).DownOne(); + return RedirectToAction("Index"); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Controllers/ScaffoldController.cs b/Suteki.Shop/Suteki.Common/Controllers/ScaffoldController.cs new file mode 100644 index 0000000..2dc5a73 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Controllers/ScaffoldController.cs @@ -0,0 +1,112 @@ +using System.Reflection; +using System.Web.Mvc; +using MvcContrib.Pagination; +using Suteki.Common.Binders; +using Suteki.Common.Extensions; +using Suteki.Common.Repositories; +using Suteki.Common.Services; +using Suteki.Common.ViewData; + +namespace Suteki.Common.Controllers +{ + public class ScaffoldController : Controller where T : class, new() + { + public IRepository Repository { get; set; } + public IRepositoryResolver RepositoryResolver { get; set; } + public IHttpContextService HttpContextService { get; set; } + + public virtual ActionResult Index(int? page) + { + return RenderIndexView(page); + } + + protected virtual ActionResult RenderIndexView(int? page) + { + var items = Repository.GetAll().AsPagination(page ?? 1); + return View("Index", ScaffoldView.Data().With(items)); + } + + public virtual ActionResult New() + { + var item = new T(); + return View("Edit", BuildEditViewData().With(item)); + } + + + [AcceptVerbs(HttpVerbs.Post)] + public ActionResult New([EntityBind(Fetch = false)] T item) + { + if(ModelState.IsValid) + { + Repository.SaveOrUpdate(item); + TempData["message"] = "Item successfully added."; //Make use of the CopyMessageFromTempDataToViewData filter to show this in the view. + return RedirectToAction("Index"); //can't use strongly typed redirect here or the wrong controller name will be picked up + } + + return View("Edit", BuildEditViewData().With(item)); + } + + [NonAction] + public virtual ScaffoldViewData BuildEditViewData() + { + var viewData = ScaffoldView.Data(); + AppendLookupLists(viewData); + return viewData; + } + + public virtual ActionResult Edit(int id) + { + T item = Repository.GetById(id); + return View("Edit", BuildEditViewData().With(item)); + } + + [AcceptVerbs(HttpVerbs.Post)] + public virtual ActionResult Edit(T item) + { + if(ModelState.IsValid) + { + TempData["message"] = "Item successfully updated."; + return RedirectToAction("Index"); + } + + return View("Edit", BuildEditViewData().With(item)); + } + + public virtual ActionResult Delete(int id, int? page) + { + T item = Repository.GetById(id); + Repository.DeleteOnSubmit(item); + //Repository.SubmitChanges(); + + return RedirectToAction("Index", new {page}); + } + + /// + /// Appends any lookup lists T might need for editing + /// + /// + [NonAction] + public virtual void AppendLookupLists(ScaffoldViewData viewData) + { + // find any properties that are attributed as a linq entity + foreach (var property in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (property.PropertyType.IsEntity()) + { + AppendLookupList(viewData, property); + } + } + } + + private void AppendLookupList(ScaffoldViewData viewData, PropertyInfo property) + { + var repository = RepositoryResolver.GetRepository(property.PropertyType); + + // get the items + object items = repository.GetAll(); + + // add the items to the viewData + viewData.WithLookupList(property.PropertyType, items); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Events/DomainEvent.cs b/Suteki.Shop/Suteki.Common/Events/DomainEvent.cs new file mode 100644 index 0000000..b29ef96 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Events/DomainEvent.cs @@ -0,0 +1,91 @@ +using System; +using System.Web; +using Castle.Windsor; + +namespace Suteki.Common.Events +{ + public class DomainEvent + { + public static Action RaiseAction { get; set; } + public static Func ReturnContainer { get; set; } + + public static void Raise(TEvent @event) where TEvent : class, IDomainEvent + { + if (@event == null) + { + throw new ArgumentNullException("@event"); + } + + if(RaiseAction != null) + { + RaiseAction(@event); + return; + } + + if (ContainerUnavailable()) return; + + var container = GetContainer(); + var handlers = container.ResolveAll>(); + + foreach (var handler in handlers) + { + handler.Handle(@event); + container.Release(handler); + } + } + + private static bool ContainerUnavailable() + { + return (ReturnContainer == null) && (HttpContext.Current == null); + } + + private static IWindsorContainer GetContainer() + { + if (ReturnContainer != null) + { + return ReturnContainer(); + } + + if (HttpContext.Current == null) + { + throw new SutekiCommonException("DomainEvent.Raise can only be used in a web application. " + + "For testing, please set the DomainEvent.RaiseAction delegate before calling Raise."); + } + + var accessor = HttpContext.Current.ApplicationInstance as IContainerAccessor; + if (accessor == null) + { + throw new SutekiCommonException("The Global.asax Application class does not " + + "implement IContainerAccessor."); + } + return accessor.Container; + } + + // test helpers + + public static DomainEventReset TurnOff() + { + RaiseAction = e => { }; + return new DomainEventReset(); + } + + public static DomainEventReset TestWith(Action raiseAction) + { + RaiseAction = raiseAction; + return new DomainEventReset(); + } + + public static void Reset() + { + RaiseAction = null; + } + } + + public class DomainEventReset : IDisposable + { + public void Dispose() + { + DomainEvent.Reset(); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Events/DomainEventService.cs b/Suteki.Shop/Suteki.Common/Events/DomainEventService.cs new file mode 100644 index 0000000..b62d699 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Events/DomainEventService.cs @@ -0,0 +1,15 @@ +namespace Suteki.Common.Events +{ + public interface IDomainEventService + { + void Raise(TEvent @event) where TEvent : class, IDomainEvent; + } + + public class DomainEventService : IDomainEventService + { + public void Raise(TEvent @event) where TEvent : class, IDomainEvent + { + DomainEvent.Raise(@event); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Events/IDomainEvent.cs b/Suteki.Shop/Suteki.Common/Events/IDomainEvent.cs new file mode 100644 index 0000000..916c152 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace Suteki.Common.Events +{ + public interface IDomainEvent + { + + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Events/IHandle.cs b/Suteki.Shop/Suteki.Common/Events/IHandle.cs new file mode 100644 index 0000000..1453f1d --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Events/IHandle.cs @@ -0,0 +1,7 @@ +namespace Suteki.Common.Events +{ + public interface IHandle where TEvent : class, IDomainEvent + { + void Handle(TEvent @event); + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Extensions/EnumerableExtensions.cs b/Suteki.Shop/Suteki.Common/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..f29b834 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Extensions/EnumerableExtensions.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Suteki.Common.Models; + +namespace Suteki.Common.Extensions +{ + public static class EnumerableExtensions + { + /// + /// Use instead of a foreach loop e.g. + /// MyCollection.Each(item => DoSomething(item)); + /// + /// + /// + /// + public static void ForEach(this IEnumerable items, Action action) + { + foreach (T item in items) + { + action(item); + } + } + + public static IEnumerable Intersperse(this IEnumerable items, T separator) + { + var first = true; + foreach (var item in items) + { + if (first) first = false; + else + { + yield return separator; + } + yield return item; + } + } + + public static string Concat(this IEnumerable items) + { + return items.Aggregate("", (agg, item) => agg + item); + } + + /// + /// Convenient replacement for a range 'for' loop. e.g. return an array of int from 10 to 20: + /// int[] tenToTwenty = 10.to(20).ToArray(); + /// + /// + /// + /// + public static IEnumerable To(this int from, int to) + { + for (int i = from; i <= to; i++) + { + yield return i; + } + } + + public static void ToConsole(this IEnumerable list) + { + list.ForEach(n => Console.Write("{0} ".With(n))); + } + + public static IEnumerable AtOddPositions(this IEnumerable list) + { + bool odd = false; // 0th position is even + foreach (T item in list) + { + odd = !odd; + if (odd) + { + yield return item; + } + } + } + + public static IEnumerable AtEvenPositions(this IEnumerable list) + { + bool even = true; // 0th position is even + foreach (T item in list) + { + even = !even; + if (even) + { + yield return item; + } + } + } + + public static IEnumerable ToEnumerable(this T item) + { + yield return item; + } + + public static Money Sum(this IEnumerable amounts) + { + return new Money(amounts.Sum(amount => amount.Amount)); + } + + public static string AsCsv(this IEnumerable items) + where T : class + { + var csvBuilder = new StringBuilder(); + var properties = typeof (T).GetProperties(); + foreach (T item in items) + { + string line = properties.Select(p => p.GetValue(item, null).ToCsvValue()).ToArray().Join(","); + csvBuilder.AppendLine(line); + } + return csvBuilder.ToString(); + } + + private static string ToCsvValue(this T item) + { + if(item == null) + { + return "\"{0}\"".With(item); + } + + if (item is string) + { + return "\"{0}\"".With(item.ToString().Replace("\"", "\\\"")); + } + double dummy; + if (double.TryParse(item.ToString(), out dummy)) + { + return "{0}".With(item); + } + return "\"{0}\"".With(item); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Extensions/FunctionalExtensions.cs b/Suteki.Shop/Suteki.Common/Extensions/FunctionalExtensions.cs new file mode 100644 index 0000000..b2e68c9 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Extensions/FunctionalExtensions.cs @@ -0,0 +1,15 @@ +using System; + +namespace Suteki.Common.Extensions +{ + public static class FunctionalExtensions + { + /// + /// http://mikehadlow.blogspot.com/2010/09/more-boilerplate-code-removal.html + /// + public static void ApplyTo(this T arg, params Action[] actions) + { + Array.ForEach(actions, action => action(arg)); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Extensions/NameValue.cs b/Suteki.Shop/Suteki.Common/Extensions/NameValue.cs new file mode 100644 index 0000000..2f16646 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Extensions/NameValue.cs @@ -0,0 +1,42 @@ +using System; + +namespace Suteki.Common.Extensions +{ + /// + /// A name value pair with the useful addition that it doesn't work out the value until it's needed + /// + /// Example: + /// return new NameValue<string, object>("Name", () => myCustomer.Name); + /// + /// + /// + public class NameValue + { + public string Name { get; private set; } + public TValue Value { get { return valueFunction(); } } + + readonly Func valueFunction; + + /// + /// Example: + /// return new NameValue<string, object>("Name", () => myCustomer.Name); + /// + /// + /// + public NameValue(string name, Func valueFunction) + { + name = RemoveLeadingUnderscoreIfPresent(name); + Name = name; + this.valueFunction = valueFunction; + } + + private static string RemoveLeadingUnderscoreIfPresent(string name) + { + if (name.StartsWith("_")) + { + return name.Substring(1); + } + return name; + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Extensions/ObjectExtensions.cs b/Suteki.Shop/Suteki.Common/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..600fcf4 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Extensions/ObjectExtensions.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using NHibernate; +using NHibernate.Proxy; + +namespace Suteki.Common.Extensions +{ + public static class ObjectExtensions + { + /// + /// Returns all the public properties as a list of Name, Value pairs + /// + /// + /// + public static IEnumerable> GetProperties(this object item) + { + foreach (PropertyInfo property in item.GetType().GetProperties()) + { + yield return new NameValue(property.Name, () => property.GetValue(item, null)); + } + } + + public static void WriteProperties(this object item) + { + item.WriteProperty(-1); + } + + public static void WriteProperty(this object item, int level) + { + level++; + if(item == null) + { + Console.WriteLine(); + return; + } + + var items = item as IEnumerable; + if (items != null && item.GetType() != typeof(string)) + { + Console.WriteLine(); + foreach (var child in items) + { + Console.Write("{0}", new string('\t', level)); + child.WriteProperty(level); + } + return; + } + + Console.WriteLine("{0}", item); + } + + public static string ToYesNo(this bool source) + { + return source.ToString().Replace(bool.TrueString, "Yes").Replace(bool.FalseString, "No"); + } + + public static T CastAs(this object source) where T : class + { + if (source is INHibernateProxy) + { + var type = NHibernateUtil.GetClass(source); + if (type != typeof(T)) + { + throw new ApplicationException(string.Format("Cannot cast {0} to {1}", type.Name, typeof(T).Name)); + } + + return ((INHibernateProxy)source).HibernateLazyInitializer.GetImplementation() as T; + } + return source as T; + } + + public static IEnumerable Single(this T item) + { + yield return item; + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Extensions/PagedList.cs b/Suteki.Shop/Suteki.Common/Extensions/PagedList.cs new file mode 100644 index 0000000..1c2345b --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Extensions/PagedList.cs @@ -0,0 +1,103 @@ +// +// This code taken from Rob Conery's blog: +// http://blog.wekeroad.com/2007/12/10/aspnet-mvc-pagedlistt/ +// + +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace Suteki.Common.Extensions +{ + public interface IPagedList + { + int TotalCount { get; set; } + int PageIndex { get; set; } + int PageSize { get; set; } + bool IsPreviousPage { get; } + bool IsNextPage { get; } + int NumberOfPages { get; } + } + + public class PagedList : List, IPagedList + { + public PagedList(IQueryable source, int index, int pageSize) + { + this.TotalCount = source.Count(); + this.PageSize = pageSize; + this.PageIndex = index; + this.AddRange(source.Skip(index * pageSize).Take(pageSize).ToList()); + } + + public PagedList(IEnumerable source, int index, int pageSize) + { + this.TotalCount = source.Count(); + this.PageSize = pageSize; + this.PageIndex = index; + this.AddRange(source.Skip(index * pageSize).Take(pageSize).ToList()); + } + + public int TotalCount + { + get; + set; + } + + public int PageIndex + { + get; + set; + } + + public int PageSize + { + get; + set; + } + + public bool IsPreviousPage + { + get + { + return (PageIndex > 0); + } + } + + public bool IsNextPage + { + get + { + return (PageIndex * PageSize) <= TotalCount; + } + } + + public int NumberOfPages + { + get + { + return (int)((TotalCount / PageSize) + 1); + } + } + } + + public static class Pagination + { + public static PagedList ToPagedList(this IEnumerable source, int index, int pageSize) + { + return new PagedList(source, index, pageSize); + } + + public static PagedList ToPagedList(this IQueryable source, int index, int pageSize) + { + return new PagedList(source, index, pageSize); + } + + public static int PageNumber(this NameValueCollection formOrQuerystring) + { + if (formOrQuerystring == null) return 0; + int pageNumber = 0; + int.TryParse(formOrQuerystring["CurrentPage"], out pageNumber); + return pageNumber; + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Extensions/StringExtensions.cs b/Suteki.Shop/Suteki.Common/Extensions/StringExtensions.cs new file mode 100644 index 0000000..4049fff --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Extensions/StringExtensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Text; +using System.Text.RegularExpressions; + +namespace Suteki.Common.Extensions +{ + public static class StringExtensions + { + public static string Join(this string[] values, string joinText) + { + StringBuilder result = new StringBuilder(); + + if (values.Length == 0) return string.Empty; + + result.Append(values[0]); + + for (int i = 1; i < values.Length; i++) + { + result.Append(joinText); + result.Append(values[i]); + } + + return result.ToString(); + } + + public static string TrimWithElipsis(this string text, int length) + { + if (text.Length <= length) return text; + return text.Substring(0, length) + "..."; + } + + /// + /// replacement for String.Format + /// + public static string With(this string format, params object[] args) + { + return string.Format(format, args); + } + + /// + /// prettily renders property names + /// + /// + /// + public static string Pretty(this string text) + { + return DeCamel(text).Replace("_", " "); + } + + public static void PrettyTest() + { + Console.WriteLine("hello_worldIAmYourNemesis".Pretty()); + } + + /// + /// turns HelloWorld into Hello World + /// + /// + /// + public static string DeCamel(this string text) + { + return Regex.Replace(text, @"([A-Z])", @" $&").Trim(); + } + + public static void DeCamelTest() + { + Console.WriteLine("HelloWorldIAmYourNemesis".DeCamel()); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Extensions/TypeExtensions.cs b/Suteki.Shop/Suteki.Common/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..650f689 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Extensions/TypeExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections; +using System.Reflection; +using Suteki.Common.Models; + +namespace Suteki.Common.Extensions +{ + public static class TypeExtensions + { + public static PropertyInfo GetPrimaryKey(this Type entityType) + { + if(!entityType.IsEntity()) + { + throw new ApplicationException(string.Format("type {0} does not implement IEntity", entityType.Name)); + } + return entityType.GetProperty("Id"); + } + + public static bool IsEntity(this Type type) + { + return typeof(IEntity).IsAssignableFrom(type); + } + + public static bool IsEnumerable(this Type type) + { + return typeof(IEnumerable).IsAssignableFrom(type); + } + + public static bool IsOrderable(this Type type) + { + return typeof (IOrderable).IsAssignableFrom(type); + } + + public static bool IsActivatable(this Type type) + { + return typeof (IActivatable).IsAssignableFrom(type); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Extensions/UrlExtensions.cs b/Suteki.Shop/Suteki.Common/Extensions/UrlExtensions.cs new file mode 100644 index 0000000..aa65ba2 Binary files /dev/null and b/Suteki.Shop/Suteki.Common/Extensions/UrlExtensions.cs differ diff --git a/Suteki.Shop/Suteki.Common/Extensions/UrlExtensionsHelper.cs b/Suteki.Shop/Suteki.Common/Extensions/UrlExtensionsHelper.cs new file mode 100644 index 0000000..ea376a6 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Extensions/UrlExtensionsHelper.cs @@ -0,0 +1,108 @@ +using System; +using System.Configuration; +using System.Text.RegularExpressions; +using System.Web; +using System.Web.Mvc; +using Suteki.Common.Services; + +namespace Suteki.Common.Extensions +{ + // + // Taken from Troy Goode's blog http://www.squaredroot.com/post/2008/06/MVC-and-SSL.aspx + // + // + public class UrlExtensionsHelper + { + /// + /// Takes a relative or absolute url and returns the fully-qualified url path. + /// + /// The url to make fully-qualified. Ex: Home/About + /// The absolute url plus protocol, server, & port. Ex: http://localhost:1234/Home/About + public virtual string ToFullyQualifiedUrl(string text) + { + + //### the VirtualPathUtility doesn"t handle querystrings, so we break the original url up + var oldUrl = text; + var oldUrlArray = (oldUrl.Contains("?") ? oldUrl.Split('?') : new[] { oldUrl, "" }); + + //### we"ll use the Request.Url object to recreate the current server request"s base url + //### requestUri.AbsoluteUri = "http://domain.com:1234/Home/Index?page=123" + //### requestUri.LocalPath = "/Home/Index" + //### requestUri.Query = "?page=123" + //### subtract LocalPath and Query from AbsoluteUri and you get "http://domain.com:1234", which is urlBase + var requestUri = GetRequestUri(); + var localPathAndQuery = requestUri.LocalPath + requestUri.Query; + var urlBase = requestUri.AbsoluteUri.Substring(0, requestUri.AbsoluteUri.Length - localPathAndQuery.Length); + + //### convert the request url into an absolute path, then reappend the querystring, if one was specified + var newUrl = VirtualPathUtility.ToAbsolute(oldUrlArray[0]); + if (!string.IsNullOrEmpty(oldUrlArray[1])) + newUrl += "?" + oldUrlArray[1]; + + //### combine the old url base (protocol + server + port) with the new local path + return urlBase + newUrl; + } + + public virtual Uri GetRequestUri() + { + return HttpContext.Current.Request.Url; + } + + /// + /// Looks for Html links in the passed string and turns each relative or absolute url and returns the fully-qualified url path. + /// + /// The url to make fully-qualified. Ex: Blah + /// The absolute url plus protocol, server, & port. Ex: Blah + public virtual string ToFullyQualifiedLink(string text) + { + + var regex = new Regex( + "(?.*?)(?\".+>)", + RegexOptions.Multiline | RegexOptions.IgnoreCase + ); + + return regex.Replace(text, m => + m.Groups["Before"].Value + + ToFullyQualifiedUrl(m.Groups["Url"].Value) + + m.Groups["After"].Value + ); + + } + + /// + /// Takes a relative or absolute url and returns the fully-qualified url path using the Https protocol. + /// + /// The url to make fully-qualified. Ex: Home/About + /// The absolute url plus server, & port using the Https protocol. Ex: https://localhost:1234/Home/About + public virtual MvcHtmlString ToSslUrl(MvcHtmlString text) + { + // TODO: This won't work with .NET 4 + if (!UseSsl()) return text; + return MvcHtmlString.Create(ToFullyQualifiedUrl(text.ToString()).Replace("http:", "https:")); + } + + public virtual string ToSslUrl(string text) + { + if (!UseSsl()) return text; + return ToFullyQualifiedUrl(text).Replace("http:", "https:"); + } + + /// + /// Looks for Html links in the passed string and turns each relative or absolute url into a fully-qualified url path using the Https protocol. + /// + /// The url to make fully-qualified. Ex: Blah + /// The absolute url plus server, & port using the Https protocol. Ex: Blah + public virtual MvcHtmlString ToSslLink(MvcHtmlString text) + { + // TODO: This won't work with .NET 4 + if (!UseSsl()) return text; + return MvcHtmlString.Create(ToFullyQualifiedLink(text.ToString()).Replace("http:", "https:")); + } + + public virtual bool UseSsl() + { + var appSettings = new AppSettings(); + return "true".Equals(appSettings.GetSetting(AppSettings.UseSsl)); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Filters/FilterUsingAttribute.cs b/Suteki.Shop/Suteki.Common/Filters/FilterUsingAttribute.cs new file mode 100644 index 0000000..cc68b1a --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Filters/FilterUsingAttribute.cs @@ -0,0 +1,83 @@ +using System; +using System.Web.Mvc; +using Suteki.Common.Extensions; +using Suteki.Common.Windsor; + +namespace Suteki.Common.Filters +{ + /// + /// Filter attribute that wraps an inner filter. The inner filter is constructed by the ServiceLocator for the current IoC container. + /// Eg FilterUsing(typeof(MyFilter)) where MyFilter implements IActionFilter and is registered with the container. + /// + public class FilterUsingAttribute : FilterAttribute, IAuthorizationFilter, IActionFilter, IResultFilter + { + private readonly Type filterType; + private object instantiatedFilter; + + public FilterUsingAttribute(Type filterType) + { + if(!IsFilterType(filterType)) + { + throw new InvalidOperationException("Type '{0}' is not valid within the FilterUsing attribute as it is not a filter type.".With(filterType.Name)); + } + this.filterType = filterType; + } + + private bool IsFilterType(Type type) + { + return typeof(IAuthorizationFilter).IsAssignableFrom(type) + || typeof(IActionFilter).IsAssignableFrom(type) + || typeof(IResultFilter).IsAssignableFrom(type); + } + + public Type FilterType + { + get { return filterType; } + } + + private T GetFilter() where T : class + { + if(instantiatedFilter == null) + { + instantiatedFilter = IocContainer.Resolve(filterType); + } + + return instantiatedFilter as T; + } + + private void ExecuteFilterWhenItIs(Action action) where TFilter :class + { + var filter = GetFilter(); + + if(filter != null) + { + action(filter); + } + } + + public void OnAuthorization(AuthorizationContext filterContext) + { + ExecuteFilterWhenItIs(f => f.OnAuthorization(filterContext)); + } + + public void OnResultExecuting(ResultExecutingContext filterContext) + { + ExecuteFilterWhenItIs(f => f.OnResultExecuting(filterContext)); + } + + public void OnResultExecuted(ResultExecutedContext filterContext) + { + ExecuteFilterWhenItIs(f => f.OnResultExecuted(filterContext)); + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + ExecuteFilterWhenItIs(f => f.OnActionExecuting(filterContext)); + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + ExecuteFilterWhenItIs(f => f.OnActionExecuted(filterContext)); + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/Filters/UnitOfWorkAttribute.cs b/Suteki.Shop/Suteki.Common/Filters/UnitOfWorkAttribute.cs new file mode 100644 index 0000000..7aaaf1d --- /dev/null +++ b/Suteki.Shop/Suteki.Common/Filters/UnitOfWorkAttribute.cs @@ -0,0 +1,90 @@ +using System.Web.Mvc; +using NHibernate; +using Suteki.Common.Repositories; + +namespace Suteki.Common.Filters +{ + public class UnitOfWorkAttribute : FilterUsingAttribute + { + public UnitOfWorkAttribute() : base(typeof (UnitOfWorkFilter)) + { + } + } + + public class UnitOfWorkFilter : IActionFilter + { + // from MVC 3, ActionFilters are cached, so we can no longer rely on having a new instance + // of UnitOfWorkFilter per request. SessionManager has a PerWebRequest lifestyle, but in order + // to allow for this lifestyle in a component with a longer than request lifestyle, we use a factory. + private readonly ISessionManagerFactory sessionManagerFactory; + private readonly IPerActionTransactionStore perActionTransactionStore; + + public UnitOfWorkFilter(IPerActionTransactionStore perActionTransactionStore, ISessionManagerFactory sessionManagerFactory) + { + this.sessionManagerFactory = sessionManagerFactory; + this.perActionTransactionStore = perActionTransactionStore; + } + + public void OnActionExecuting(ActionExecutingContext filterContext) + { + var sessionManager = sessionManagerFactory.Resolve(); + try + { + var sesion = sessionManager.OpenSession(); + perActionTransactionStore.StoreTransaction(filterContext, sesion.BeginTransaction()); + } + finally + { + sessionManagerFactory.Release(sessionManager); + } + } + + public void OnActionExecuted(ActionExecutedContext filterContext) + { + var transaction = perActionTransactionStore.RetrieveTransaction(filterContext); + if (transaction == null) return; + + var thereWereNoExceptions = filterContext.Exception == null || filterContext.ExceptionHandled; + if (filterContext.Controller.ViewData.ModelState.IsValid && thereWereNoExceptions) + { + transaction.Commit(); + } + else + { + transaction.Rollback(); + } + } + } + + public interface IPerActionTransactionStore + { + void StoreTransaction(ActionExecutingContext filterContext, ITransaction transaction); + ITransaction RetrieveTransaction(ActionExecutedContext filterContext); + } + + public class PerActionTransactionStore : IPerActionTransactionStore + { + private const string transactionToken = "__transaction__"; + + public void StoreTransaction(ActionExecutingContext filterContext, ITransaction transaction) + { + var controllerActionName = + transactionToken + + filterContext.Controller.GetType().Name + + "." + + filterContext.ActionDescriptor.ActionName; + filterContext.RequestContext.HttpContext.Items[controllerActionName] = transaction; + } + + public ITransaction RetrieveTransaction(ActionExecutedContext filterContext) + { + var controllerActionName = + transactionToken + + filterContext.Controller.GetType().Name + + "." + + filterContext.ActionDescriptor.ActionName; + + return filterContext.RequestContext.HttpContext.Items[controllerActionName] as ITransaction; + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/FirstRequestInitialization.cs b/Suteki.Shop/Suteki.Common/FirstRequestInitialization.cs new file mode 100644 index 0000000..54a4aa6 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/FirstRequestInitialization.cs @@ -0,0 +1,28 @@ +using System; +using System.Web; + +namespace Suteki.Common +{ + // from http://mvolo.com/blogs/serverside/archive/2007/11/10/Integrated-mode-Request-is-not-available-in-this-context-in-Application_5F00_Start.aspx + public static class FirstRequestInitialization + { + static bool initializedAlready; + static readonly Object lockObj = new Object(); + public static void Initialize(HttpContext context, Action initializationAction) + { + if (initializedAlready) + { + return; + } + lock (lockObj) + { + if (initializedAlready) + { + return; + } + initializedAlready = true; + initializationAction(); + } + } + } +} \ No newline at end of file diff --git a/Suteki.Shop/Suteki.Common/HtmlHelpers/ComboFor.cs b/Suteki.Shop/Suteki.Common/HtmlHelpers/ComboFor.cs new file mode 100644 index 0000000..6179a25 --- /dev/null +++ b/Suteki.Shop/Suteki.Common/HtmlHelpers/ComboFor.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Web; +using System.Web.Mvc; +using Suteki.Common.Extensions; +using Suteki.Common.Models; +using Suteki.Common.Repositories; + +namespace Suteki.Common.HtmlHelpers +{ + public interface IComboFor where TEntity : class, INamedEntity + { + ComboFor Multiple(); + string BoundTo(Expression> propertyExpression, Expression> whereClause); + string BoundTo(Expression> propertyExpression, string propertyNamePrefix); + string BoundTo(Expression> propertyExpression); + string BoundTo(Expression> propertyExpression, Expression> whereClause, string propertyNamePrefix); + string BoundTo(string propertyToBind, IEnumerable selectedIds); + } + + public class ComboFor : IComboFor, IRequireHtmlHelper where TEntity : class, INamedEntity + { + readonly IRepository repository; + protected Expression> WhereClause { get; set; } + protected string PropertyNamePrefix { get; set; } + protected bool mutiple = false; + + public ComboFor(IRepository repository) + { + this.repository = repository; + } + + public ComboFor Multiple() + { + mutiple = true; + return this; + } + + public string BoundTo(Expression> propertyExpression, Expression> whereClause, string propertyNamePrefix) + { + WhereClause = whereClause; + PropertyNamePrefix = propertyNamePrefix; + return BoundTo(propertyExpression); + } + + public string BoundTo(Expression> propertyExpression, Expression> whereClause) + { + WhereClause = whereClause; + return BoundTo(propertyExpression); + } + + public string BoundTo(Expression> propertyExpression, string propertyNamePrefix) + { + PropertyNamePrefix = propertyNamePrefix; + return BoundTo(propertyExpression); + } + + public string BoundTo(Expression> propertyExpression) + { + var getPropertyValue = propertyExpression.Compile(); + var propertyName = (!String.IsNullOrEmpty(PropertyNamePrefix) ? PropertyNamePrefix : "") + + Utils.ExpressionHelper.GetDottedPropertyNameFromExpression(propertyExpression) + ".Id"; + + var viewDataModelIsNull = (!typeof(TModel).IsValueType) && HtmlHelper.ViewData.Model == null; + var selectedId = viewDataModelIsNull ? 0 : getPropertyValue(HtmlHelper.ViewData.Model).Id; + return BuildCombo(propertyName, selectedId); + } + + public string BoundTo(string propertyToBind, IEnumerable selectedIds) + { + return BuildCombo(propertyToBind, selectedIds.ToArray()); + } + + public string BoundTo(string propertyToBind, IEnumerable selectedIds, Expression> whereClause) + { + WhereClause = whereClause; + return BuildCombo(propertyToBind, selectedIds.ToArray()); + } + + public override string ToString() + { + return BuildCombo(typeof(TEntity).Name + "Id"); + } + + protected virtual string BuildCombo(string htmlId, params int[] selectedIds) + { + if (string.IsNullOrEmpty(htmlId)) + { + throw new ArgumentException("htmlId can not be null or empty"); + } + + var selectListItems = GetSelectListItems(selectedIds); + var result = DropDownListBuilder.DropDownList(htmlId, selectListItems, mutiple); + return result; + } + + protected IEnumerable GetSelectListItems(int[] selectedIds) + { + var queryable = repository.GetAll(); + if (WhereClause != null) + { + queryable = queryable.Where(WhereClause); + } + var enumerable = queryable.AsEnumerable(); + + if (typeof(TEntity).IsOrderable()) + { + enumerable = enumerable.Select(x => (IOrderable)x).InOrder().Select(x => (TEntity)x); + } + + if (typeof(TEntity).IsActivatable()) + { + enumerable = enumerable.Select(x => (IActivatable)x).Where(a => a.IsActive).Select(x => (TEntity)x); + } + + var items = enumerable + .Select(e => new SelectListItem + { + Selected = selectedIds.Any(id => id == e.Id), Text = e.Name, Value = e.Id.ToString() + }); + + return items; + } + + public HtmlHelper HtmlHelper { get; set; } + } + + /// + /// Not using HtmlHelper.DropDownList because it tries to replace selected items from binding state. + /// + public class DropDownListBuilder + { + public static string DropDownList(string htmlId, IEnumerable selectListItems, bool allowMultiple) + { + // Convert each ListItem to an