From dab6c59a37f2aa9454152219592b376c2019a002 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 22 Feb 2019 21:54:31 -0600 Subject: [PATCH] Towards using short GUIDs (#1) Also updated dependencies and moved to .NET Core 2.2; the Giraffe token router doesn't have a parser for short GUIDs yet --- src/PrayerTracker.Data/DataAccess.fs | 26 ++++++++----------- .../PrayerTracker.Data.fsproj | 7 ++--- .../PrayerTracker.Tests.fsproj | 10 +++---- src/PrayerTracker.UI/Church.fs | 6 ++--- src/PrayerTracker.UI/CommonFunctions.fs | 6 ++++- src/PrayerTracker.UI/PrayerRequest.fs | 6 ++--- src/PrayerTracker.UI/PrayerTracker.UI.fsproj | 16 ++++++------ src/PrayerTracker.UI/SmallGroup.fs | 16 ++++++------ src/PrayerTracker.UI/User.fs | 23 ++++++++-------- src/PrayerTracker/App.fs | 19 +++++--------- src/PrayerTracker/Church.fs | 3 ++- src/PrayerTracker/Extensions.fs | 5 ++-- src/PrayerTracker/PrayerTracker.fsproj | 14 +++++----- src/PrayerTracker/SmallGroup.fs | 5 ++-- src/PrayerTracker/User.fs | 12 +++------ 15 files changed, 82 insertions(+), 92 deletions(-) diff --git a/src/PrayerTracker.Data/DataAccess.fs b/src/PrayerTracker.Data/DataAccess.fs index 018234f..3a86252 100644 --- a/src/PrayerTracker.Data/DataAccess.fs +++ b/src/PrayerTracker.Data/DataAccess.fs @@ -3,15 +3,11 @@ module PrayerTracker.DataAccess open FSharp.Control.Tasks.ContextInsensitive open Microsoft.EntityFrameworkCore +open Microsoft.FSharpLu open PrayerTracker.Entities open System.Collections.Generic open System.Linq -/// EF can return null for record types with the CLIMutable attribute; this converts a possibly-null record type to an -/// option -let optRec<'T> (r : 'T) = - match box r with null -> None | _ -> Some r - type AppDbContext with (*-- DISCONNECTED DATA EXTENSIONS --*) @@ -34,7 +30,7 @@ type AppDbContext with member this.TryChurchById cId = task { let! church = this.Churches.AsNoTracking().FirstOrDefaultAsync (fun c -> c.churchId = cId) - return optRec church + return Option.fromObject church } /// Find all churches @@ -50,7 +46,7 @@ type AppDbContext with member this.TryMemberById mId = task { let! mbr = this.Members.AsNoTracking().FirstOrDefaultAsync (fun m -> m.memberId = mId) - return optRec mbr + return Option.fromObject mbr } /// Find all members for a small group @@ -74,7 +70,7 @@ type AppDbContext with member this.TryRequestById reqId = task { let! req = this.PrayerRequests.AsNoTracking().FirstOrDefaultAsync (fun pr -> pr.prayerRequestId = reqId) - return optRec req + return Option.fromObject req } /// Get all (or active) requests for a small group as of now or the specified date @@ -121,7 +117,7 @@ type AppDbContext with this.SmallGroups.AsNoTracking() .Include(fun sg -> sg.preferences) .FirstOrDefaultAsync (fun sg -> sg.smallGroupId = gId) - return optRec grp + return Option.fromObject grp } /// Get small groups that are public or password protected @@ -176,7 +172,7 @@ type AppDbContext with .ToListAsync () return grps |> Seq.map (fun grp -> grp.smallGroupId.ToString "N", sprintf "%s | %s" grp.church.name grp.name) - |> Map.ofSeq + |> List.ofSeq } /// Log on a small group @@ -213,7 +209,7 @@ type AppDbContext with member this.TryTimeZoneById tzId = task { let! tz = this.TimeZones.FirstOrDefaultAsync (fun t -> t.timeZoneId = tzId) - return optRec tz + return Option.fromObject tz } /// Get all time zones @@ -229,7 +225,7 @@ type AppDbContext with member this.TryUserById uId = task { let! user = this.Users.AsNoTracking().FirstOrDefaultAsync (fun u -> u.userId = uId) - return optRec user + return Option.fromObject user } /// Find a user by its e-mail address and authorized small group @@ -239,14 +235,14 @@ type AppDbContext with this.Users.AsNoTracking().FirstOrDefaultAsync (fun u -> u.emailAddress = email && u.smallGroups.Any (fun xref -> xref.smallGroupId = gId)) - return optRec user + return Option.fromObject user } /// Find a user by its Id (tracked entity), eagerly loading the user's groups member this.TryUserByIdWithGroups uId = task { let! user = this.Users.Include(fun u -> u.smallGroups).FirstOrDefaultAsync (fun u -> u.userId = uId) - return optRec user + return Option.fromObject user } /// Get a list of all users @@ -274,7 +270,7 @@ type AppDbContext with u.emailAddress = email && u.passwordHash = pwHash && u.smallGroups.Any (fun xref -> xref.smallGroupId = gId)) - return optRec user + return Option.fromObject user } /// Find a user based on credentials stored in a cookie diff --git a/src/PrayerTracker.Data/PrayerTracker.Data.fsproj b/src/PrayerTracker.Data/PrayerTracker.Data.fsproj index 0c40295..03de88a 100644 --- a/src/PrayerTracker.Data/PrayerTracker.Data.fsproj +++ b/src/PrayerTracker.Data/PrayerTracker.Data.fsproj @@ -16,13 +16,14 @@ - - + + + - + diff --git a/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj b/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj index b7dc751..9debc3a 100644 --- a/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj +++ b/src/PrayerTracker.Tests/PrayerTracker.Tests.fsproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + netcoreapp2.2 @@ -15,9 +15,9 @@ - - - + + + @@ -27,7 +27,7 @@ - + diff --git a/src/PrayerTracker.UI/Church.fs b/src/PrayerTracker.UI/Church.fs index 12de97d..983f8c4 100644 --- a/src/PrayerTracker.UI/Church.fs +++ b/src/PrayerTracker.UI/Church.fs @@ -3,8 +3,6 @@ open Giraffe.GiraffeViewEngine open PrayerTracker.Entities open PrayerTracker.ViewModels -open System -open System.Collections.Generic /// View for the church edit page let edit (m : EditChurch) ctx vi = @@ -14,7 +12,7 @@ let edit (m : EditChurch) ctx vi = style [ _scoped ] [ rawText "#name { width: 20rem; } #city { width: 10rem; } #st { width: 3rem; } #interfaceAddress { width: 30rem; }" ] csrfToken ctx - input [ _type "hidden"; _name "churchId"; _value (m.churchId.ToString "N") ] + input [ _type "hidden"; _name "churchId"; _value (flatGuid m.churchId) ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "name" ] [ encLocText s.["Church Name"] ] @@ -79,7 +77,7 @@ let maintain (churches : Church list) (stats : Map) ctx vi ] churches |> List.map (fun ch -> - let chId = ch.churchId.ToString "N" + let chId = flatGuid ch.churchId let delAction = sprintf "/church/%s/delete" chId let delPrompt = s.["Are you want to delete this {0}? This action cannot be undone.", sprintf "%s (%s)" (s.["Church"].Value.ToLower ()) ch.name] diff --git a/src/PrayerTracker.UI/CommonFunctions.fs b/src/PrayerTracker.UI/CommonFunctions.fs index 9c9a45b..aaf3d1e 100644 --- a/src/PrayerTracker.UI/CommonFunctions.fs +++ b/src/PrayerTracker.UI/CommonFunctions.fs @@ -7,6 +7,7 @@ open Microsoft.AspNetCore.Antiforgery open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Mvc.Localization open Microsoft.Extensions.Localization +open System open System.IO open System.Text.Encodings.Web @@ -101,8 +102,11 @@ let selectDefault text = sprintf "— %s —" text /// Generate a standard submit button with icon and text let submit attrs ico text = button (_type "submit" :: attrs) [ icon ico; rawText "  "; encLocText text ] +/// Format a GUID with no dashes (used for URLs and forms) +let flatGuid (x : Guid) = x.ToString "N" + /// An empty GUID string (used for "add" actions) -let emptyGuid = System.Guid.Empty.ToString "N" +let emptyGuid = flatGuid Guid.Empty /// blockquote tag diff --git a/src/PrayerTracker.UI/PrayerRequest.fs b/src/PrayerTracker.UI/PrayerRequest.fs index 4a4d83a..e3a7601 100644 --- a/src/PrayerTracker.UI/PrayerRequest.fs +++ b/src/PrayerTracker.UI/PrayerRequest.fs @@ -17,7 +17,7 @@ let edit (m : EditRequest) today ctx vi = let pageTitle = match m.isNew () with true -> "Add a New Request" | false -> "Edit Request" [ form [ _action "/prayer-request/save"; _method "post"; _class "pt-center-columns" ] [ csrfToken ctx - input [ _type "hidden"; _name "requestId"; _value (m.requestId.ToString "N") ] + input [ _type "hidden"; _name "requestId"; _value (flatGuid m.requestId) ] div [ _class "pt-field-row" ] [ yield div [ _class "pt-field" ] [ label [ _for "requestType" ] [ encLocText s.["Request Type"] ] @@ -139,7 +139,7 @@ let lists (grps : SmallGroup list) vi = ] grps |> List.map (fun grp -> - let grpId = grp.smallGroupId.ToString "N" + let grpId = flatGuid grp.smallGroupId tr [] [ match grp.preferences.isPublic with | true -> @@ -175,7 +175,7 @@ let maintain (reqs : PrayerRequest seq) (grp : SmallGroup) onlyActive (ctx : Htt let requests = reqs |> Seq.map (fun req -> - let reqId = req.prayerRequestId.ToString "N" + let reqId = flatGuid req.prayerRequestId let reqText = Utils.htmlToPlainText req.text let delAction = sprintf "/prayer-request/%s/delete" reqId let delPrompt = s.["Are you want to delete this prayer request? This action cannot be undone.\\n(If the prayer request has been answered, or an event has passed, consider inactivating it instead.)"].Value diff --git a/src/PrayerTracker.UI/PrayerTracker.UI.fsproj b/src/PrayerTracker.UI/PrayerTracker.UI.fsproj index 3eab2e0..56820b3 100644 --- a/src/PrayerTracker.UI/PrayerTracker.UI.fsproj +++ b/src/PrayerTracker.UI/PrayerTracker.UI.fsproj @@ -21,13 +21,13 @@ - - - - - - - + + + + + + + @@ -89,7 +89,7 @@ - + diff --git a/src/PrayerTracker.UI/SmallGroup.fs b/src/PrayerTracker.UI/SmallGroup.fs index 2642631..8650b78 100644 --- a/src/PrayerTracker.UI/SmallGroup.fs +++ b/src/PrayerTracker.UI/SmallGroup.fs @@ -20,7 +20,7 @@ let announcement isAdmin ctx vi = ] ] match isAdmin with - | true -> + | true -> yield div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [] [ encLocText s.["Send Announcement to"]; rawText ":" ] @@ -77,7 +77,7 @@ let edit (m : EditSmallGroup) (churches : Church list) ctx vi = let pageTitle = match m.isNew () with true -> "Add a New Group" | false -> "Edit Group" form [ _action "/small-group/save"; _method "post"; _class "pt-center-columns" ] [ csrfToken ctx - input [ _type "hidden"; _name "smallGroupId"; _value (m.smallGroupId.ToString "N") ] + input [ _type "hidden"; _name "smallGroupId"; _value (flatGuid m.smallGroupId) ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "name" ] [ encLocText s.["Group Name"] ] @@ -89,9 +89,9 @@ let edit (m : EditSmallGroup) (churches : Church list) ctx vi = label [ _for "churchId" ] [ encLocText s.["Church"] ] seq { yield "", selectDefault s.["Select Church"].Value - yield! churches |> List.map (fun c -> c.churchId.ToString "N", c.name) + yield! churches |> List.map (fun c -> flatGuid c.churchId, c.name) } - |> selectList "churchId" (m.churchId.ToString "N") [ _required ] + |> selectList "churchId" (flatGuid m.churchId) [ _required ] ] ] div [ _class "pt-field-row" ] [ submit [] "save" s.["Save Group"] ] @@ -108,7 +108,7 @@ let editMember (m : EditMember) (typs : (string * LocalizedString) seq) ctx vi = form [ _action "/small-group/member/save"; _method "post"; _class "pt-center-columns" ] [ style [ _scoped ] [ rawText "#memberName { width: 15rem; } #emailAddress { width: 20rem; }" ] csrfToken ctx - input [ _type "hidden"; _name "memberId"; _value (m.memberId.ToString "N") ] + input [ _type "hidden"; _name "memberId"; _value (flatGuid m.memberId) ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "memberName" ] [ encLocText s.["Member Name"] ] @@ -148,7 +148,7 @@ let logOn (grps : SmallGroup list) grpId ctx vi = | _ -> yield "", selectDefault s.["Select Group"].Value yield! grps - |> List.map (fun grp -> grp.smallGroupId.ToString "N", sprintf "%s | %s" grp.church.name grp.name) + |> List.map (fun grp -> flatGuid grp.smallGroupId, sprintf "%s | %s" grp.church.name grp.name) } |> selectList "smallGroupId" grpId [ _required ] ] @@ -197,7 +197,7 @@ let maintain (grps : SmallGroup list) ctx vi = ] grps |> List.map (fun g -> - let grpId = g.smallGroupId.ToString "N" + let grpId = flatGuid g.smallGroupId let delAction = sprintf "/small-group/%s/delete" grpId let delPrompt = s.["Are you want to delete this {0}? This action cannot be undone.", sprintf "%s (%s)" (s.["Small Group"].Value.ToLower ()) g.name].Value @@ -243,7 +243,7 @@ let members (mbrs : Member list) (emailTyps : Map) ctx ] mbrs |> List.map (fun mbr -> - let mbrId = mbr.memberId.ToString "N" + let mbrId = flatGuid mbr.memberId let delAction = sprintf "/small-group/member/%s/delete" mbrId let delPrompt = s.["Are you want to delete this {0} ({1})? This action cannot be undone.", s.["group member"], mbr.memberName].Value diff --git a/src/PrayerTracker.UI/User.fs b/src/PrayerTracker.UI/User.fs index afe2706..8cbbca6 100644 --- a/src/PrayerTracker.UI/User.fs +++ b/src/PrayerTracker.UI/User.fs @@ -5,12 +5,12 @@ open PrayerTracker.Entities open PrayerTracker.ViewModels /// View for the group assignment page -let assignGroups m (groups : Map) (curGroups : string list) ctx vi = +let assignGroups m groups curGroups ctx vi = let s = I18N.localizer.Force () let pageTitle = sprintf "%s • %A" m.userName s.["Assign Groups"] form [ _action "/user/small-groups/save"; _method "post"; _class "pt-center-columns" ] [ csrfToken ctx - input [ _type "hidden"; _name "userId"; _value (m.userId.ToString "N") ] + input [ _type "hidden"; _name "userId"; _value (flatGuid m.userId) ] input [ _type "hidden"; _name "userName"; _value m.userName ] table [ _class "pt-table" ] [ thead [] [ @@ -20,19 +20,18 @@ let assignGroups m (groups : Map) (curGroups : string list) ctx ] ] groups - |> Seq.map (fun grp -> - let inputId = sprintf "id-%s" grp.Key + |> List.map (fun (grpId, grpName) -> + let inputId = sprintf "id-%s" grpId tr [] [ td [] [ input [ yield _type "checkbox" yield _name "smallGroups" yield _id inputId - yield _value grp.Key - match curGroups |> List.contains grp.Key with true -> yield _checked | false -> () ] + yield _value grpId + match curGroups |> List.contains grpId with true -> yield _checked | false -> () ] ] - td [] [ label [ _for inputId ] [ encodedText grp.Value ] ] + td [] [ label [ _for inputId ] [ encodedText grpName ] ] ]) - |> List.ofSeq |> tbody [] ] div [ _class "pt-field-row" ] [ submit [] "save" s.["Save Group Assignments"] ] @@ -89,7 +88,7 @@ let edit (m : EditUser) ctx vi = style [ _scoped ] [ rawText "#firstName, #lastName, #password, #passwordConfirm { width: 10rem; } #emailAddress { width: 20rem; } " ] csrfToken ctx - input [ _type "hidden"; _name "userId"; _value (m.userId.ToString "N") ] + input [ _type "hidden"; _name "userId"; _value (flatGuid m.userId) ] div [ _class "pt-field-row" ] [ div [ _class "pt-field" ] [ label [ _for "firstName" ] [ encLocText s.["First Name"] ] @@ -131,7 +130,7 @@ let edit (m : EditUser) ctx vi = /// View for the user log on page -let logOn (m : UserLogOn) (groups : Map) ctx vi = +let logOn (m : UserLogOn) groups ctx vi = let s = I18N.localizer.Force () form [ _action "/user/log-on"; _method "post"; _class "pt-center-columns" ] [ style [ _scoped ] [ rawText "#emailAddress { width: 20rem; }" ] @@ -153,7 +152,7 @@ let logOn (m : UserLogOn) (groups : Map) ctx vi = label [ _for "smallGroupId" ] [ encLocText s.["Group"] ] seq { yield "", selectDefault s.["Select Group"].Value - yield! groups |> Seq.sortBy (fun x -> x.Value) |> Seq.map (fun x -> x.Key, x.Value) + yield! groups } |> selectList "smallGroupId" "" [ _required ] @@ -193,7 +192,7 @@ let maintain (users : User list) ctx vi = ] users |> List.map (fun user -> - let userId = user.userId.ToString "N" + let userId = flatGuid user.userId let delAction = sprintf "/user/%s/delete" userId let delPrompt = s.["Are you want to delete this {0}? This action cannot be undone.", (sprintf "%s (%s)" (s.["User"].Value.ToLower()) user.fullName)].Value diff --git a/src/PrayerTracker/App.fs b/src/PrayerTracker/App.fs index 7c0fd3c..2199d2c 100644 --- a/src/PrayerTracker/App.fs +++ b/src/PrayerTracker/App.fs @@ -161,10 +161,8 @@ module Configure = let log = app.ApplicationServices.GetRequiredService() (match env.IsDevelopment () with | true -> - log.AddConsole () |> ignore app.UseDeveloperExceptionPage () | false -> - log.AddConsole LogLevel.Warning |> ignore try use scope = app.ApplicationServices.GetRequiredService().CreateScope () scope.ServiceProvider.GetService().Database.Migrate () @@ -182,12 +180,10 @@ module Configure = /// The web application module App = - open System open System.IO - let exitCode = 0 - - let CreateWebHostBuilder _ = + [] + let main _ = let contentRoot = Directory.GetCurrentDirectory () WebHostBuilder() .UseContentRoot(contentRoot) @@ -196,10 +192,7 @@ module App = .UseWebRoot(Path.Combine (contentRoot, "wwwroot")) .ConfigureServices(Configure.services) .ConfigureLogging(Configure.logging) - .Configure(Action Configure.app) - - [] - let main args = - CreateWebHostBuilder(args).Build().Run() - - exitCode + .Configure(System.Action Configure.app) + .Build() + .Run () + 0 diff --git a/src/PrayerTracker/Church.fs b/src/PrayerTracker/Church.fs index 2e3301d..55520c3 100644 --- a/src/PrayerTracker/Church.fs +++ b/src/PrayerTracker/Church.fs @@ -5,6 +5,7 @@ open Giraffe open PrayerTracker open PrayerTracker.Entities open PrayerTracker.ViewModels +open PrayerTracker.Views.CommonFunctions open System open System.Threading.Tasks @@ -14,7 +15,7 @@ let private findStats (db : AppDbContext) churchId = let! grps = db.CountGroupsByChurch churchId let! reqs = db.CountRequestsByChurch churchId let! usrs = db.CountUsersByChurch churchId - return (churchId.ToString "N"), { smallGroups = grps; prayerRequests = reqs; users = usrs } + return flatGuid churchId, { smallGroups = grps; prayerRequests = reqs; users = usrs } } diff --git a/src/PrayerTracker/Extensions.fs b/src/PrayerTracker/Extensions.fs index 526a565..f8c93c7 100644 --- a/src/PrayerTracker/Extensions.fs +++ b/src/PrayerTracker/Extensions.fs @@ -2,6 +2,7 @@ module PrayerTracker.Extensions open Microsoft.AspNetCore.Http +open Microsoft.FSharpLu open Newtonsoft.Json open PrayerTracker.Entities open PrayerTracker.ViewModels @@ -19,14 +20,14 @@ type ISession with | v -> JsonConvert.DeserializeObject<'T> v member this.GetSmallGroup () = - this.GetObject Key.Session.currentGroup |> optRec + this.GetObject Key.Session.currentGroup |> Option.fromObject member this.SetSmallGroup (group : SmallGroup option) = match group with | Some g -> this.SetObject Key.Session.currentGroup g | None -> this.Remove Key.Session.currentGroup member this.GetUser () = - this.GetObject Key.Session.currentUser |> optRec + this.GetObject Key.Session.currentUser |> Option.fromObject member this.SetUser (user: User option) = match user with | Some u -> this.SetObject Key.Session.currentUser u diff --git a/src/PrayerTracker/PrayerTracker.fsproj b/src/PrayerTracker/PrayerTracker.fsproj index 7e2ac31..d448b3d 100644 --- a/src/PrayerTracker/PrayerTracker.fsproj +++ b/src/PrayerTracker/PrayerTracker.fsproj @@ -1,8 +1,8 @@  - netcoreapp2.1 - 7.1.0.0 + netcoreapp2.2 + 7.2.0.0 7.0.0.0 Bit Badger Solutions @@ -28,11 +28,11 @@ - + - - - + + + @@ -41,7 +41,7 @@ - + diff --git a/src/PrayerTracker/SmallGroup.fs b/src/PrayerTracker/SmallGroup.fs index f658709..e61eb5a 100644 --- a/src/PrayerTracker/SmallGroup.fs +++ b/src/PrayerTracker/SmallGroup.fs @@ -9,6 +9,7 @@ open PrayerTracker open PrayerTracker.Cookies open PrayerTracker.Entities open PrayerTracker.ViewModels +open PrayerTracker.Views.CommonFunctions open System open System.Threading.Tasks @@ -133,7 +134,7 @@ let logOn (groupId : SmallGroupId option) : HttpHandler = let startTicks = DateTime.Now.Ticks task { let! grps = ctx.dbContext().ProtectedGroups () - let grpId = match groupId with Some gid -> gid.ToString "N" | None -> "" + let grpId = match groupId with Some gid -> flatGuid gid | None -> "" return! { viewInfo ctx startTicks with helpLink = Help.logOn } |> Views.SmallGroup.logOn grps grpId ctx @@ -162,7 +163,7 @@ let logOnSubmit : HttpHandler = return! redirectTo false "/prayer-requests/view" next ctx | None -> addError ctx s.["Password incorrect - login unsuccessful"] - return! redirectTo false (sprintf "/small-group/log-on/%s" (m.smallGroupId.ToString "N")) next ctx + return! redirectTo false (sprintf "/small-group/log-on/%s" (flatGuid m.smallGroupId)) next ctx | Error e -> return! bindError e next ctx } diff --git a/src/PrayerTracker/User.fs b/src/PrayerTracker/User.fs index 3c83aa3..36acc78 100644 --- a/src/PrayerTracker/User.fs +++ b/src/PrayerTracker/User.fs @@ -8,6 +8,7 @@ open PrayerTracker open PrayerTracker.Cookies open PrayerTracker.Entities open PrayerTracker.ViewModels +open PrayerTracker.Views.CommonFunctions open System open System.Collections.Generic open System.Net @@ -270,7 +271,7 @@ let save : HttpHandler = |> Some } |> addUserMessage ctx - return! redirectTo false (sprintf "/user/%s/small-groups" (u.userId.ToString "N")) next ctx + return! redirectTo false (sprintf "/user/%s/small-groups" (flatGuid u.userId)) next ctx | false -> addInfo ctx s.["Successfully {0} user", s.["Updated"].Value.ToLower ()] return! redirectTo false "/users" next ctx @@ -292,7 +293,7 @@ let saveGroups : HttpHandler = match Seq.length m.smallGroups with | 0 -> addError ctx s.["You must select at least one group to assign"] - return! redirectTo false (sprintf "/user/%s/small-groups" (m.userId.ToString "N")) next ctx + return! redirectTo false (sprintf "/user/%s/small-groups" (flatGuid m.userId)) next ctx | _ -> let db = ctx.dbContext () let! user = db.TryUserByIdWithGroups m.userId @@ -330,12 +331,7 @@ let smallGroups userId : HttpHandler = match user with | Some u -> let! grps = db.GroupList () - let curGroups = - seq { - for g in u.smallGroups do - yield g.smallGroupId.ToString "N" - } - |> List.ofSeq + let curGroups = u.smallGroups |> Seq.map (fun g -> flatGuid g.smallGroupId) |> List.ofSeq return! viewInfo ctx startTicks |> Views.User.assignGroups (AssignGroups.fromUser u) grps curGroups ctx