diff --git a/Makefile b/Makefile index ca040be3..e07af982 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ VERSION=1.1 CONFIG=Debug DESTDIR=build/web/$(CONFIG) COPYUNCHANGED="false" +PREPRODHOST=lua.pschneider.fr all: deploy @@ -14,7 +15,7 @@ deploy: ddir build rm -rf $(DESTDIR)/obj mv $(DESTDIR)/Web.config $(DESTDIR)/Web.config.new -rsync: rsync-preprod rsync-local +rsync: rsync-preprod build: xbuild /p:Configuration=$(CONFIG) /t:Build Yavsc.sln @@ -24,7 +25,7 @@ clean: rm -rf $(DESTDIR) rsync-preprod: deploy - rsync -ravu build/web/$(CONFIG)/ root@lua.localdomain:/srv/httpd/luapre + rsync -ravu build/web/$(CONFIG)/ root@$(PREPRODHOST):/srv/httpd/luapre rsync-local: rsync -ravu build/web/$(CONFIG)/ root@localhost:/srv/www/yavsc diff --git a/web/Controllers/AccountController.cs b/web/Controllers/AccountController.cs index 46618769..9a56544a 100644 --- a/web/Controllers/AccountController.cs +++ b/web/Controllers/AccountController.cs @@ -46,7 +46,7 @@ namespace Yavsc.Controllers public static Profile GetProfile (string user) { - return new Profile (ProfileBase.Create (user)); + return new Profile (ProfileBase.Create (user)) ; } @@ -216,8 +216,10 @@ namespace Yavsc.Controllers [HttpGet] public ActionResult Profile(Profile model) { - ViewData ["UserName"] = Membership.GetUser ().UserName; - model = GetProfile ((string) ViewData ["UserName"]); + string username = Membership.GetUser ().UserName; + ViewData ["UserName"] = username; + model = GetProfile (username); + model.RememberMe = FormsAuthentication.GetAuthCookie ( username, true )==null; return View (model); } @@ -280,9 +282,9 @@ namespace Yavsc.Controllers HttpContext.Profile.SetPropertyValue ( "IBAN", model.IBAN); HttpContext.Profile.Save (); - ViewData ["Message"] = "Profile enregistré."; - - } + FormsAuthentication.SetAuthCookie (username, model.RememberMe); + ViewData ["Message"] = "Profile enregistré, cookie modifié."; + } // HttpContext.Profile.SetPropertyValue("Avatar",Avatar); return View (model); } diff --git a/web/Controllers/AdminController.cs b/web/Controllers/AdminController.cs index 6833a59d..eff21665 100644 --- a/web/Controllers/AdminController.cs +++ b/web/Controllers/AdminController.cs @@ -19,10 +19,11 @@ namespace Yavsc.Controllers /// public class AdminController : Controller { + [Authorize(Roles="Admin")] - public ActionResult Index(DataAccess model) + public ActionResult Index() { - return View (model); + return View (); } [Authorize(Roles="Admin")] diff --git a/web/Controllers/BlogsController.cs b/web/Controllers/BlogsController.cs index 4ae6969a..46ab5ab6 100644 --- a/web/Controllers/BlogsController.cs +++ b/web/Controllers/BlogsController.cs @@ -48,7 +48,9 @@ namespace Yavsc.Controllers if (string.IsNullOrEmpty (user)) { return BlogList (pageIndex, pageSize); } else { - MembershipUser u = Membership.GetUser (user, false); + MembershipUser u = null; + if (Membership.FindUsersByName (user) != null) + u= Membership.GetUser (user, false); if (u == null) { ModelState.AddModelError ("UserName", string.Format ("Utilisateur inconu : {0}", user)); @@ -58,6 +60,8 @@ namespace Yavsc.Controllers return UserPosts (user, pageIndex, pageSize); return UserPost (user, title); } + + } } diff --git a/web/Controllers/FormInputValue.cs b/web/Controllers/FormInputValue.cs deleted file mode 100644 index d3cb339a..00000000 --- a/web/Controllers/FormInputValue.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using Yavsc; -using SalesCatalog; -using SalesCatalog.Model; -using System.Web.Routing; -using System.Threading.Tasks; -using System.Diagnostics; -using System.Web.Http; -using System.Net.Http; -using System.Web; -using System.Linq; -using System.IO; -using System.Net; -using WorkFlowProvider; -using System.Web.Security; -using Yavsc.Model.WorkFlow; -using System.Reflection; -using System.Collections.Generic; -using Yavsc.Model.RolesAndMembers; -using Yavsc.Controllers; -using Yavsc.Formatters; -using System.Text; -using System.Web.Profile; - -namespace Yavsc.ApiControllers -{ - - public class FormInputValue: IValueProvider - { - #region IValueProvider implementation - - public T GetValue () - { - throw new NotImplementedException (); - } - - #endregion - - - } - - -} - diff --git a/web/Controllers/FrontOfficeController.cs b/web/Controllers/FrontOfficeController.cs index e366e11d..7a7e5e84 100644 --- a/web/Controllers/FrontOfficeController.cs +++ b/web/Controllers/FrontOfficeController.cs @@ -30,6 +30,11 @@ namespace Yavsc.Controllers wfmgr = new WorkFlowManager (); } + public ActionResult Index () + { + return View (); + } + [Authorize] public ActionResult Estimates () { diff --git a/web/Controllers/GoogleController.cs b/web/Controllers/GoogleController.cs index 94476329..01548060 100644 --- a/web/Controllers/GoogleController.cs +++ b/web/Controllers/GoogleController.cs @@ -22,79 +22,125 @@ namespace Yavsc.Controllers public class GoogleController : Controller { - + // datetime format : yyyy-mm-ddTHH:MM:ss + // 2015-01-01T10:00:00-07:00 // private string API_KEY="AIzaSyBV_LQHb22nGgjNvFzZwnQHjao3Q7IewRw"; private string getPeopleUri = "https://www.googleapis.com/plus/v1/people"; + private string getCalListUri = "https://www.googleapis.com/calendar/v3/users/me/calendarList"; + private string getCalEntriesUri = "https://developers.google.com/google-apps/calendar/v3/reference/events/list"; - private string CLIENT_ID="325408689282-6bekh7p3guj4k0f3301a6frf025cnrk1.apps.googleusercontent.com"; - - private string CLIENT_SECRET="MaxYcvJJCs2gDGvaELZbzwfL"; + private string CLIENT_ID = "325408689282-6bekh7p3guj4k0f3301a6frf025cnrk1.apps.googleusercontent.com"; + private string CLIENT_SECRET = "MaxYcvJJCs2gDGvaELZbzwfL"; - string [] SCOPES = { - "openid" , + string[] SCOPES = { + "openid", "profile", "email" - } ; + }; - string tokenUri = "https://accounts.google.com/o/oauth2/token"; - string authUri = "https://accounts.google.com/o/oauth2/auth"; + string tokenUri = "https://accounts.google.com/o/oauth2/token"; + string authUri = "https://accounts.google.com/o/oauth2/auth"; - public void Login(string returnUrl) + private string SetSessionSate () { - if (string.IsNullOrWhiteSpace (returnUrl)) - returnUrl = "/"; Random rand = new Random (); - string state = "security_token"+rand.Next (100000).ToString()+rand.Next (100000).ToString(); + string state = "security_token" + rand.Next (100000).ToString () + rand.Next (100000).ToString (); Session ["state"] = state; + return state; + } + + public void Login (string returnUrl) + { + if (string.IsNullOrWhiteSpace (returnUrl)) + returnUrl = "/"; Session ["returnUrl"] = returnUrl; string redirectUri = Request.Url.Scheme + "://" + Request.Url.Authority + "/Google/Auth"; + string scope = string.Join ("%20", SCOPES); - string prms = String.Format("response_type=code&client_id={0}&redirect_uri={1}&scope={2}&state={3}&access_type=offline&include_granted_scopes=false", - CLIENT_ID, redirectUri, string.Join("%20",SCOPES), state); - - WebRequest wr = WebRequest.Create(authUri+"?"+prms); - wr.Method = "GET"; - // Get the response. + string prms = String.Format ("response_type=code&client_id={0}&redirect_uri={1}&scope={2}&state={3}&include_granted_scopes=false", + CLIENT_ID, redirectUri, scope, SetSessionSate ()); - WebResponse response = wr.GetResponse(); - string resQuery = response.ResponseUri.Query; - string cont = HttpUtility.ParseQueryString(resQuery)["continue"]; - Response.Redirect (cont); + GetAuthResponse (prms); + } + private void GetAuthResponse (string prms) + { + WebRequest wr = WebRequest.Create (authUri + "?" + prms); + wr.Method = "GET"; + WebResponse response = wr.GetResponse (); + string resQuery = response.ResponseUri.Query; + string cont = HttpUtility.ParseQueryString (resQuery) ["continue"]; + Response.Redirect (cont); + } + [Authorize] + public void GetCalAuth () + { + string redirectUri = Request.Url.Scheme + "://" + Request.Url.Authority + "/Google/CalAuth"; + string scope = string.Join ("%20", SCOPES); + scope += "%20https://www.googleapis.com/auth/calendar"; + string prms = String.Format ("response_type=code&client_id={0}&redirect_uri={1}&scope={2}&state={3}&include_granted_scopes=false&access_type=offline", + CLIENT_ID, redirectUri, scope, SetSessionSate ()); + Session ["calasked"] = true; + GetAuthResponse (prms); } [HttpGet] - public ActionResult Auth() + [Authorize] + public ActionResult CalAuth () { + string redirectUri = Request.Url.Scheme + "://" + Request.Url.Authority + "/Google/CalAuth"; + AuthToken gat = GetToken (TokenPostDataFromCode(redirectUri, GetCodeFromRequest())); + if (gat == null) { + return View ("Auth"); + } + SaveToken (gat); + HttpContext.Profile.SetPropertyValue ("gcalapi", true); + string returnUrl = (string)Session ["returnUrl"]; + Session ["returnUrl"]=null; + return Redirect (returnUrl); + } - string returnUrl = (string) Session ["returnUrl"]; - string redirectUri = Request.Url.Scheme + "://" + Request.Url.Authority + "/Google/Auth"; + /// + /// Saves the token. + /// This calls the Profile.Save() method. + /// It should be called immediatly after getting the token from Google, in + /// order to save a descent value as expiration date. + /// + /// Gat. + private void SaveToken(AuthToken gat) + { + HttpContext.Profile.SetPropertyValue ("gtoken", gat.access_token); + if (gat.refresh_token!=null) + HttpContext.Profile.SetPropertyValue ("grefreshtoken", gat.refresh_token); + HttpContext.Profile.SetPropertyValue ("gtokentype", gat.token_type); + HttpContext.Profile.SetPropertyValue ("gtokenexpir", DateTime.Now.AddSeconds(gat.expires_in)); + HttpContext.Profile.Save (); + } + + private string GetCodeFromRequest() + { string code = Request.Params ["code"]; string error = Request.Params ["error"]; if (error != null) { ViewData ["Message"] = - string.Format(LocalizedText.Google_error, - LocalizedText.ResourceManager.GetString(error)); - return View(); + string.Format (LocalizedText.Google_error, + LocalizedText.ResourceManager.GetString (error)); + return null; } string state = Request.Params ["state"]; - if (state!=null && string.Compare((string)Session ["state"],state)!=0) { + if (state != null && string.Compare ((string)Session ["state"], state) != 0) { ViewData ["Message"] = - LocalizedText.ResourceManager.GetString("invalid request state"); - return View(); + LocalizedText.ResourceManager.GetString ("invalid request state"); + return null; } + return code; + } - string postdata = - string.Format( - "redirect_uri={0}&client_id={1}&client_secret={2}&code={3}&grant_type=authorization_code", - HttpUtility.UrlEncode(redirectUri), - HttpUtility.UrlEncode(CLIENT_ID), - HttpUtility.UrlEncode(CLIENT_SECRET), - HttpUtility.UrlEncode(code)); - + private AuthToken GetToken (string postdata) + { Byte[] bytes = System.Text.Encoding.UTF8.GetBytes (postdata); HttpWebRequest webreq = WebRequest.CreateHttp (tokenUri); webreq.Method = "POST"; @@ -103,45 +149,71 @@ namespace Yavsc.Controllers webreq.ContentLength = bytes.Length; using (Stream dataStream = webreq.GetRequestStream ()) { dataStream.Write (bytes, 0, bytes.Length); - }; + } + AuthToken gat =null; using (WebResponse response = webreq.GetResponse ()) { using (Stream responseStream = response.GetResponseStream ()) { using (StreamReader readStream = new StreamReader (responseStream, Encoding.UTF8)) { string responseStr = readStream.ReadToEnd (); - AuthToken gat = JsonConvert.DeserializeObject(responseStr); - Session ["GoogleAuthToken"] = gat; - SignIn regmod = new SignIn (); - HttpWebRequest webreppro = WebRequest.CreateHttp (getPeopleUri+"/me"); - webreppro.ContentType = "application/http"; - webreppro.Headers.Add (HttpRequestHeader.Authorization, gat.token_type + " " + gat.access_token); - webreppro.Method = "GET"; - using (WebResponse proresp = webreppro.GetResponse ()) { - using (Stream prresponseStream = proresp.GetResponseStream ()) { - using (StreamReader readproresp = new StreamReader (prresponseStream, Encoding.UTF8)) { - string prresponseStr = readproresp.ReadToEnd (); - People me = JsonConvert.DeserializeObject (prresponseStr); - // TODO use me.id to retreive an existing user - string accEmail = me.emails.Where (x => x.type == "account").First().value; - MembershipUserCollection mbrs = Membership.FindUsersByEmail (accEmail); - if (mbrs.Count == 1) { - // TODO check the google id - // just set this user as logged on - FormsAuthentication.SetAuthCookie (me.displayName, true); - Session ["returnUrl"] = null; - return Redirect (returnUrl); - } - // else create the account - regmod.Email = accEmail; - regmod.UserName = me.displayName; - Session ["me"] = me; - return Auth(regmod); - } - } - } + gat = JsonConvert.DeserializeObject (responseStr); } } } + return gat; + } + + private string TokenPostDataFromCode(string redirectUri, string code) + { + string postdata = + string.Format ( + "redirect_uri={0}&client_id={1}&client_secret={2}&code={3}&grant_type=authorization_code", + HttpUtility.UrlEncode (redirectUri), + HttpUtility.UrlEncode (CLIENT_ID), + HttpUtility.UrlEncode (CLIENT_SECRET), + HttpUtility.UrlEncode (code)); + return postdata; + } + + [HttpGet] + public ActionResult Auth () + { + string redirectUri = Request.Url.Scheme + "://" + Request.Url.Authority + "/Google/Auth"; + AuthToken gat = GetToken (TokenPostDataFromCode( redirectUri, GetCodeFromRequest())); + if (gat == null) { + return View (); + } + string returnUrl = (string)Session ["returnUrl"]; + + SignIn regmod = new SignIn (); + HttpWebRequest webreppro = WebRequest.CreateHttp (getPeopleUri + "/me"); + webreppro.ContentType = "application/http"; + webreppro.Headers.Add (HttpRequestHeader.Authorization, gat.token_type + " " + gat.access_token); + webreppro.Method = "GET"; + using (WebResponse proresp = webreppro.GetResponse ()) { + using (Stream prresponseStream = proresp.GetResponseStream ()) { + using (StreamReader readproresp = new StreamReader (prresponseStream, Encoding.UTF8)) { + string prresponseStr = readproresp.ReadToEnd (); + People me = JsonConvert.DeserializeObject (prresponseStr); + // TODO use me.id to retreive an existing user + string accEmail = me.emails.Where (x => x.type == "account").First ().value; + MembershipUserCollection mbrs = Membership.FindUsersByEmail (accEmail); + if (mbrs.Count == 1) { + // TODO check the google id + // just set this user as logged on + FormsAuthentication.SetAuthCookie (me.displayName, true); + Session ["returnUrl"] = null; + return Redirect (returnUrl); + } + // else create the account + regmod.Email = accEmail; + regmod.UserName = me.displayName; + Session ["me"] = me; + Session ["GoogleAuthToken"] = gat; + return Auth (regmod); + } + } + } } /// @@ -149,7 +221,7 @@ namespace Yavsc.Controllers /// /// Regmod. [HttpPost] - public ActionResult Auth(SignIn regmod) + public ActionResult Auth (SignIn regmod) { if (ModelState.IsValid) { if (Membership.GetUser (regmod.UserName) != null) { @@ -188,7 +260,6 @@ namespace Yavsc.Controllers FormsAuthentication.SetAuthCookie (regmod.UserName, true); HttpContext.Profile.Initialize (regmod.UserName, true); - HttpContext.Profile.SetPropertyValue ("gtoken", gat.access_token); HttpContext.Profile.SetPropertyValue ("Name", me.displayName); // TODO use image if (me.image != null) { @@ -197,11 +268,12 @@ namespace Yavsc.Controllers if (me.placesLived != null) { People.Place pplace = me.placesLived.Where (x => x.primary).First (); if (pplace != null) - HttpContext.Profile.SetPropertyValue ("Address", pplace.value); + HttpContext.Profile.SetPropertyValue ("CityAndState", pplace.value); } if (me.url != null) HttpContext.Profile.SetPropertyValue ("WebSite", me.url); - HttpContext.Profile.Save (); + SaveToken (gat); + // already done in SaveToken: HttpContext.Profile.Save (); return Redirect (returnUrl); } ViewData ["returnUrl"] = returnUrl; @@ -209,10 +281,111 @@ namespace Yavsc.Controllers return View (regmod); } - public void ChooseCalendar() + private string GetFreshGoogleCredential (ProfileBase pr) { - throw new NotImplementedException(); + string token = (string) pr.GetPropertyValue ("gtoken"); + string token_type = (string) pr.GetPropertyValue ("gtokentype"); + DateTime token_exp = (DateTime) pr.GetPropertyValue ("gtokenexpir"); + if (token_exp < DateTime.Now) { + string refresh_token = (string) pr.GetPropertyValue ("grefreshtoken"); + AuthToken gat = GetToken( + string.Format("grant_type=refresh_token&client_id={0}&client_secret={1}&refresh_token={2}", + CLIENT_ID, CLIENT_SECRET, refresh_token)); + token = gat.access_token; + pr.SetPropertyValue ("gtoken", token); + pr.Save (); + // assert gat.token_type == token_type + } + return token_type + " " + token; } - } -} + [Authorize] + [HttpGet] + public ActionResult ChooseCalendar (string returnUrl) + { + Session ["ChooseCalReturnUrl"] = returnUrl; + bool hasCalAuth = (bool)HttpContext.Profile.GetPropertyValue ("gcalapi"); + if (!hasCalAuth) { + Session["returnUrl"] = Request.Url.Scheme + "://" + Request.Url.Authority + "/Google/ChooseCalendar"; + return RedirectToAction ("GetCalAuth"); + } + + string cred = GetFreshGoogleCredential (HttpContext.Profile); + + HttpWebRequest webreq = WebRequest.CreateHttp (getCalListUri); + webreq.Headers.Add (HttpRequestHeader.Authorization, cred); + webreq.Method = "GET"; + webreq.ContentType = "application/http"; + using (WebResponse resp = webreq.GetResponse ()) { + using (Stream respstream = resp.GetResponseStream ()) { + using (StreamReader readresp = new StreamReader (respstream, Encoding.UTF8)) { + string responseStr = readresp.ReadToEnd (); + CalendarList res = JsonConvert.DeserializeObject (responseStr); + ViewData ["json"] = responseStr; + return View (res); + } + } + } + } + + [HttpPost] + [Authorize] + public ActionResult SetCalendar (string calchoice) + { + HttpContext.Profile.SetPropertyValue ("gcalid", calchoice); + HttpContext.Profile.Save (); + + string returnUrl = (string) Session ["ChooseCalReturnUrl"]; + if (returnUrl != null) { + Session ["ChooseCalReturnUrl"] = null; + return Redirect (returnUrl); + } + return Redirect ("/"); + } + + [Authorize] + [HttpGet] + public ActionResult DateQuery() + { + return View (new AskForADate ()); + } + + [Authorize] + [HttpPost] + public ActionResult DateQuery(AskForADate model) + { + if (ModelState.IsValid) { + if (model.MinDate < DateTime.Now) { + ModelState.AddModelError ("MinTime", "This first date must be in the future."); + return View (model); + } + if (model.MinDate > model.MaxDate) { + ModelState.AddModelError ("MinTime", "This first date must be lower than the second one."); + return View (model); + } + ProfileBase upr = ProfileBase.Create (model.UserName); + if (upr == null) { + ModelState.AddModelError ("UserName", "Non existent user"); + return View (model); + } + + + HttpWebRequest webreq = WebRequest.CreateHttp (getCalEntriesUri); + webreq.Headers.Add (HttpRequestHeader.Authorization, GetFreshGoogleCredential(upr)); + webreq.Method = "GET"; + webreq.ContentType = "application/http"; + using (WebResponse resp = webreq.GetResponse ()) { + using (Stream respstream = resp.GetResponseStream ()) { + using (StreamReader readresp = new StreamReader (respstream, Encoding.UTF8)) { + string responseStr = readresp.ReadToEnd (); + CalendarList res = JsonConvert.DeserializeObject (responseStr); + ViewData ["json"] = responseStr; + return View (res); + } + } + } + } + return View (model); + } + } +} \ No newline at end of file diff --git a/web/Controllers/IOrderInfo.cs b/web/Controllers/IOrderInfo.cs index c7cd3673..9af566d6 100644 --- a/web/Controllers/IOrderInfo.cs +++ b/web/Controllers/IOrderInfo.cs @@ -30,7 +30,7 @@ namespace Yavsc.ApiControllers DateTime Creation { get; set; } string Status { get; set; } long OrderId { get; set; } - FormInputValue [] Details { get; set; } + Object [] Details { get; set; } } } diff --git a/web/Global.asax.cs b/web/Global.asax.cs index bacbe529..32f7e0d4 100644 --- a/web/Global.asax.cs +++ b/web/Global.asax.cs @@ -25,19 +25,28 @@ namespace Yavsc "Blog", "Blog/{user}/{title}", new { controller = "Blogs", action = "Index", user=UrlParameter.Optional, title = UrlParameter.Optional } - ); + ); /* routes.MapRoute ( "Blogs", "Blogs/{action}/{user}/{title}", new { controller = "Blogs", action = "Index", user=UrlParameter.Optional, title = UrlParameter.Optional} - ); + );*/ /* + routes.MapRoute ( + "Home", + "Home/Index", + new { controller = "Blogs", action = "Index", user="paul", title = "Documentation" } + );*/ + /*routes.MapRoute ( + "Default", + "{controller}/{action}/{id}", + new { controller = "Home", action = "Index", id = UrlParameter.Optional } + );*/ routes.MapRoute ( "Default", - "{controller}/{action}/{user}", - new { controller = "Home", action = "Index", user="" } + "{controller}/{action}/{user}/{title}", + new { controller = "Blogs", action = "Index", user="paul", title = "Documentation"} ); - } protected void Application_Start () diff --git a/web/Helpers/T.cs b/web/Helpers/T.cs index 70fac62c..645c558c 100644 --- a/web/Helpers/T.cs +++ b/web/Helpers/T.cs @@ -8,14 +8,16 @@ using System.Web.Mvc.Ajax; using System.Net.Mail; using Yavsc; using System.Globalization; +using Yavsc.Model; -namespace Yavsc +namespace Yavsc.Helpers { public class T { - public static string GetString(string msgid) + public static string GetString(string msg) { - return Mono.Unix.Catalog.GetString (msgid); + string tr = LocalizedText.ResourceManager.GetString (msg.Replace (" ", "_")); + return tr==null?msg:tr; } } } diff --git a/web/Models/App.master b/web/Models/App.master index 0b23262f..e766a075 100644 --- a/web/Models/App.master +++ b/web/Models/App.master @@ -2,8 +2,9 @@ - <% Page.Title = Page.Title + " - " + YavscHelpers.SiteName; %> + <% ViewState["orgtitle"] = T.GetString(Page.Title); %> + <% Page.Title = ViewState["orgtitle"] + " - " + YavscHelpers.SiteName; %> @@ -16,7 +17,9 @@
-

<%= Page.Title %>

+

<%=ViewState["orgtitle"]%> - +"><%= YavscHelpers.SiteName %> +

<% if (ViewData["Error"]!=null) { %> @@ -29,32 +32,36 @@ <%= Html.Encode(ViewData["Message"]) %> <% } %> +
+ + +
diff --git a/web/Scripts/GruntFile.js b/web/Scripts/GruntFile.js new file mode 100644 index 00000000..ca3e653b --- /dev/null +++ b/web/Scripts/GruntFile.js @@ -0,0 +1,29 @@ +module.exports = function(grunt) { + + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + meta: { + banner : '/*!\n' + + ' * <%= pkg.title %> v<%= pkg.version %> - <%= pkg.description %>\n' + + ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %> - <%= pkg.homepage %>\n' + + ' * License: <%= pkg.license %>\n' + + ' */\n\n' + }, + uglify: { + options : { + banner : '<%= meta.banner %>', + report: 'gzip' + }, + dist: { + files: { + 'jquery.timepicker.min.js': ['jquery.timepicker.js'] + } + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-uglify'); + + grunt.registerTask('default', ['uglify']); + +}; diff --git a/web/Scripts/jquery.mousewheel.js b/web/Scripts/jquery.mousewheel.js new file mode 100755 index 00000000..6756fa61 --- /dev/null +++ b/web/Scripts/jquery.mousewheel.js @@ -0,0 +1,221 @@ +/*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh) + * Licensed under the MIT License (LICENSE.txt). + * + * Version: 3.1.12 + * + * Requires: jQuery 1.2.2+ + */ + +(function (factory) { + if ( typeof define === 'function' && define.amd ) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS style for Browserify + module.exports = factory; + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + + var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'], + toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ? + ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'], + slice = Array.prototype.slice, + nullLowestDeltaTimeout, lowestDelta; + + if ( $.event.fixHooks ) { + for ( var i = toFix.length; i; ) { + $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks; + } + } + + var special = $.event.special.mousewheel = { + version: '3.1.12', + + setup: function() { + if ( this.addEventListener ) { + for ( var i = toBind.length; i; ) { + this.addEventListener( toBind[--i], handler, false ); + } + } else { + this.onmousewheel = handler; + } + // Store the line height and page height for this particular element + $.data(this, 'mousewheel-line-height', special.getLineHeight(this)); + $.data(this, 'mousewheel-page-height', special.getPageHeight(this)); + }, + + teardown: function() { + if ( this.removeEventListener ) { + for ( var i = toBind.length; i; ) { + this.removeEventListener( toBind[--i], handler, false ); + } + } else { + this.onmousewheel = null; + } + // Clean up the data we added to the element + $.removeData(this, 'mousewheel-line-height'); + $.removeData(this, 'mousewheel-page-height'); + }, + + getLineHeight: function(elem) { + var $elem = $(elem), + $parent = $elem['offsetParent' in $.fn ? 'offsetParent' : 'parent'](); + if (!$parent.length) { + $parent = $('body'); + } + return parseInt($parent.css('fontSize'), 10) || parseInt($elem.css('fontSize'), 10) || 16; + }, + + getPageHeight: function(elem) { + return $(elem).height(); + }, + + settings: { + adjustOldDeltas: true, // see shouldAdjustOldDeltas() below + normalizeOffset: true // calls getBoundingClientRect for each event + } + }; + + $.fn.extend({ + mousewheel: function(fn) { + return fn ? this.bind('mousewheel', fn) : this.trigger('mousewheel'); + }, + + unmousewheel: function(fn) { + return this.unbind('mousewheel', fn); + } + }); + + + function handler(event) { + var orgEvent = event || window.event, + args = slice.call(arguments, 1), + delta = 0, + deltaX = 0, + deltaY = 0, + absDelta = 0, + offsetX = 0, + offsetY = 0; + event = $.event.fix(orgEvent); + event.type = 'mousewheel'; + + // Old school scrollwheel delta + if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } + if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } + if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } + if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; } + + // Firefox < 17 horizontal scrolling related to DOMMouseScroll event + if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { + deltaX = deltaY * -1; + deltaY = 0; + } + + // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy + delta = deltaY === 0 ? deltaX : deltaY; + + // New school wheel delta (wheel event) + if ( 'deltaY' in orgEvent ) { + deltaY = orgEvent.deltaY * -1; + delta = deltaY; + } + if ( 'deltaX' in orgEvent ) { + deltaX = orgEvent.deltaX; + if ( deltaY === 0 ) { delta = deltaX * -1; } + } + + // No change actually happened, no reason to go any further + if ( deltaY === 0 && deltaX === 0 ) { return; } + + // Need to convert lines and pages to pixels if we aren't already in pixels + // There are three delta modes: + // * deltaMode 0 is by pixels, nothing to do + // * deltaMode 1 is by lines + // * deltaMode 2 is by pages + if ( orgEvent.deltaMode === 1 ) { + var lineHeight = $.data(this, 'mousewheel-line-height'); + delta *= lineHeight; + deltaY *= lineHeight; + deltaX *= lineHeight; + } else if ( orgEvent.deltaMode === 2 ) { + var pageHeight = $.data(this, 'mousewheel-page-height'); + delta *= pageHeight; + deltaY *= pageHeight; + deltaX *= pageHeight; + } + + // Store lowest absolute delta to normalize the delta values + absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) ); + + if ( !lowestDelta || absDelta < lowestDelta ) { + lowestDelta = absDelta; + + // Adjust older deltas if necessary + if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) { + lowestDelta /= 40; + } + } + + // Adjust older deltas if necessary + if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) { + // Divide all the things by 40! + delta /= 40; + deltaX /= 40; + deltaY /= 40; + } + + // Get a whole, normalized value for the deltas + delta = Math[ delta >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta); + deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta); + deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta); + + // Normalise offsetX and offsetY properties + if ( special.settings.normalizeOffset && this.getBoundingClientRect ) { + var boundingRect = this.getBoundingClientRect(); + offsetX = event.clientX - boundingRect.left; + offsetY = event.clientY - boundingRect.top; + } + + // Add information to the event object + event.deltaX = deltaX; + event.deltaY = deltaY; + event.deltaFactor = lowestDelta; + event.offsetX = offsetX; + event.offsetY = offsetY; + // Go ahead and set deltaMode to 0 since we converted to pixels + // Although this is a little odd since we overwrite the deltaX/Y + // properties with normalized deltas. + event.deltaMode = 0; + + // Add event and delta to the front of the arguments + args.unshift(event, delta, deltaX, deltaY); + + // Clearout lowestDelta after sometime to better + // handle multiple device types that give different + // a different lowestDelta + // Ex: trackpad = 3 and mouse wheel = 120 + if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); } + nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200); + + return ($.event.dispatch || $.event.handle).apply(this, args); + } + + function nullLowestDelta() { + lowestDelta = null; + } + + function shouldAdjustOldDeltas(orgEvent, absDelta) { + // If this is an older event and the delta is divisable by 120, + // then we are assuming that the browser is treating this as an + // older mouse wheel event and that we should divide the deltas + // by 40 to try and get a more usable deltaFactor. + // Side note, this actually impacts the reported scroll distance + // in older browsers and can cause scrolling to be slower than native. + // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false. + return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0; + } + +})); diff --git a/web/Scripts/jquery.timepicker.js b/web/Scripts/jquery.timepicker.js new file mode 100644 index 00000000..8f4a900f --- /dev/null +++ b/web/Scripts/jquery.timepicker.js @@ -0,0 +1,1151 @@ +/************************ +jquery-timepicker v1.4.13 +http://jonthornton.github.com/jquery-timepicker/ + +requires jQuery 1.7+ +************************/ + + +(function (factory) { + if (typeof exports === "object" && exports && + typeof module === "object" && module && module.exports === exports) { + // Browserify. Attach to jQuery module. + factory(require("jquery")); + } else if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + var _baseDate = _generateBaseDate(); + var _ONE_DAY = 86400; + var _lang = { + am: 'am', + pm: 'pm', + AM: 'AM', + PM: 'PM', + decimal: '.', + mins: 'mins', + hr: 'hr', + hrs: 'hrs' + }; + + var methods = + { + init: function(options) + { + return this.each(function() + { + var self = $(this); + + // pick up settings from data attributes + var attributeOptions = []; + for (var key in $.fn.timepicker.defaults) { + if (self.data(key)) { + attributeOptions[key] = self.data(key); + } + } + + var settings = $.extend({}, $.fn.timepicker.defaults, attributeOptions, options); + + if (settings.lang) { + _lang = $.extend(_lang, settings.lang); + } + + settings = _parseSettings(settings); + self.data('timepicker-settings', settings); + self.addClass('ui-timepicker-input'); + + if (settings.useSelect) { + _render(self); + } else { + self.prop('autocomplete', 'off'); + self.on('click.timepicker focus.timepicker', methods.show); + self.on('change.timepicker', _formatValue); + self.on('keydown.timepicker', _keydownhandler); + self.on('keyup.timepicker', _keyuphandler); + + _formatValue.call(self.get(0)); + } + }); + }, + + show: function(e) + { + var self = $(this); + var settings = self.data('timepicker-settings'); + + if (e) { + if (!settings.showOnFocus) { + return true; + } + + e.preventDefault(); + } + + if (settings.useSelect) { + self.data('timepicker-list').focus(); + return; + } + + if (_hideKeyboard(self)) { + // block the keyboard on mobile devices + self.blur(); + } + + var list = self.data('timepicker-list'); + + // check if input is readonly + if (self.prop('readonly')) { + return; + } + + // check if list needs to be rendered + if (!list || list.length === 0 || typeof settings.durationTime === 'function') { + _render(self); + list = self.data('timepicker-list'); + } + + if (_isVisible(list)) { + return; + } + + // make sure other pickers are hidden + methods.hide(); + + // position the dropdown relative to the input + list.show(); + var listOffset = {}; + + if (settings.orientation == 'rtl') { + // right-align the dropdown + listOffset.left = self.offset().left + self.outerWidth() - list.outerWidth() + parseInt(list.css('marginLeft').replace('px', ''), 10); + } else { + // left-align the dropdown + listOffset.left = self.offset().left + parseInt(list.css('marginLeft').replace('px', ''), 10); + } + + if ((self.offset().top + self.outerHeight(true) + list.outerHeight()) > $(window).height() + $(window).scrollTop()) { + // position the dropdown on top + list.addClass('ui-timepicker-positioned-top'); + listOffset.top = self.offset().top - list.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10); + } else { + // put it under the input + list.removeClass('ui-timepicker-positioned-top'); + listOffset.top = self.offset().top + self.outerHeight() + parseInt(list.css('marginTop').replace('px', ''), 10); + } + + list.offset(listOffset); + + // position scrolling + var selected = list.find('.ui-timepicker-selected'); + + if (!selected.length) { + if (_getTimeValue(self)) { + selected = _findRow(self, list, _time2int(_getTimeValue(self))); + } else if (settings.scrollDefault) { + selected = _findRow(self, list, settings.scrollDefault); + } + } + + if (selected && selected.length) { + var topOffset = list.scrollTop() + selected.position().top - selected.outerHeight(); + list.scrollTop(topOffset); + } else { + list.scrollTop(0); + } + + // attach close handlers + $(document).on('touchstart.ui-timepicker mousedown.ui-timepicker', _closeHandler); + if (settings.closeOnWindowScroll) { + $(document).on('scroll.ui-timepicker', _closeHandler); + } + + self.trigger('showTimepicker'); + + return this; + }, + + hide: function(e) + { + var self = $(this); + var settings = self.data('timepicker-settings'); + + if (settings && settings.useSelect) { + self.blur(); + } + + $('.ui-timepicker-wrapper').each(function() { + var list = $(this); + if (!_isVisible(list)) { + return; + } + + var self = list.data('timepicker-input'); + var settings = self.data('timepicker-settings'); + + if (settings && settings.selectOnBlur) { + _selectValue(self); + } + + list.hide(); + self.trigger('hideTimepicker'); + }); + + return this; + }, + + option: function(key, value) + { + return this.each(function(){ + var self = $(this); + var settings = self.data('timepicker-settings'); + var list = self.data('timepicker-list'); + + if (typeof key == 'object') { + settings = $.extend(settings, key); + + } else if (typeof key == 'string' && typeof value != 'undefined') { + settings[key] = value; + + } else if (typeof key == 'string') { + return settings[key]; + } + + settings = _parseSettings(settings); + + self.data('timepicker-settings', settings); + + if (list) { + list.remove(); + self.data('timepicker-list', false); + } + + if (settings.useSelect) { + _render(self); + } + }); + }, + + getSecondsFromMidnight: function() + { + return _time2int(_getTimeValue(this)); + }, + + getTime: function(relative_date) + { + var self = this; + + var time_string = _getTimeValue(self); + if (!time_string) { + return null; + } + + if (!relative_date) { + relative_date = new Date(); + } + var offset = _time2int(time_string); + + // construct a Date with today's date, and offset's time + var time = new Date(relative_date); + time.setHours(offset / 3600); + time.setMinutes(offset % 3600 / 60); + time.setSeconds(offset % 60); + time.setMilliseconds(0); + + return time; + }, + + setTime: function(value) + { + var self = this; + var settings = self.data('timepicker-settings'); + + if (settings.forceRoundTime) { + var prettyTime = _roundAndFormatTime(value, settings) + } else { + var prettyTime = _int2time(_time2int(value), settings.timeFormat); + } + + _setTimeValue(self, prettyTime); + if (self.data('timepicker-list')) { + _setSelected(self, self.data('timepicker-list')); + } + + return this; + }, + + remove: function() + { + var self = this; + + // check if this element is a timepicker + if (!self.hasClass('ui-timepicker-input')) { + return; + } + + var settings = self.data('timepicker-settings'); + self.removeAttr('autocomplete', 'off'); + self.removeClass('ui-timepicker-input'); + self.removeData('timepicker-settings'); + self.off('.timepicker'); + + // timepicker-list won't be present unless the user has interacted with this timepicker + if (self.data('timepicker-list')) { + self.data('timepicker-list').remove(); + } + + if (settings.useSelect) { + self.show(); + } + + self.removeData('timepicker-list'); + + return this; + } + }; + + // private methods + + function _isVisible(elem) + { + var el = elem[0]; + return el.offsetWidth > 0 && el.offsetHeight > 0; + } + + function _parseSettings(settings) + { + if (settings.minTime) { + settings.minTime = _time2int(settings.minTime); + } + + if (settings.maxTime) { + settings.maxTime = _time2int(settings.maxTime); + } + + if (settings.durationTime && typeof settings.durationTime !== 'function') { + settings.durationTime = _time2int(settings.durationTime); + } + + if (settings.scrollDefault == 'now') { + settings.scrollDefault = _time2int(new Date()); + } else if (settings.scrollDefault) { + settings.scrollDefault = _time2int(settings.scrollDefault); + } else if (settings.minTime) { + settings.scrollDefault = settings.minTime; + } + + if (settings.scrollDefault) { + settings.scrollDefault = _roundTime(settings.scrollDefault, settings); + } + + if ($.type(settings.timeFormat) === "string" && settings.timeFormat.match(/[gh]/)) { + settings._twelveHourTime = true; + } + + if (settings.disableTimeRanges.length > 0) { + // convert string times to integers + for (var i in settings.disableTimeRanges) { + settings.disableTimeRanges[i] = [ + _time2int(settings.disableTimeRanges[i][0]), + _time2int(settings.disableTimeRanges[i][1]) + ]; + } + + // sort by starting time + settings.disableTimeRanges = settings.disableTimeRanges.sort(function(a, b){ + return a[0] - b[0]; + }); + + // merge any overlapping ranges + for (var i = settings.disableTimeRanges.length-1; i > 0; i--) { + if (settings.disableTimeRanges[i][0] <= settings.disableTimeRanges[i-1][1]) { + settings.disableTimeRanges[i-1] = [ + Math.min(settings.disableTimeRanges[i][0], settings.disableTimeRanges[i-1][0]), + Math.max(settings.disableTimeRanges[i][1], settings.disableTimeRanges[i-1][1]) + ]; + settings.disableTimeRanges.splice(i, 1); + } + } + } + + return settings; + } + + function _render(self) + { + var settings = self.data('timepicker-settings'); + var list = self.data('timepicker-list'); + + if (list && list.length) { + list.remove(); + self.data('timepicker-list', false); + } + + if (settings.useSelect) { + list = $('",{"class":"ui-timepicker-select"});var f=d}else{d=a("
    ",{"class":"ui-timepicker-list"});var f=a("
    ",{"class":"ui-timepicker-wrapper",tabindex:-1});f.css({display:"none",position:"absolute"}).append(d)}if(c.noneOption)if(c.noneOption===!0&&(c.noneOption=c.useSelect?"Time...":"None"),a.isArray(c.noneOption)){for(var h in c.noneOption)if(parseInt(h,10)==h){var i=e(c.noneOption[h],c.useSelect);d.append(i)}}else{var i=e(c.noneOption,c.useSelect);d.append(i)}c.className&&f.addClass(c.className),null===c.minTime&&null===c.durationTime||!c.showDuration||(f.addClass("ui-timepicker-with-duration"),f.addClass("ui-timepicker-step-"+c.step));var k=c.minTime;"function"==typeof c.durationTime?k=u(c.durationTime()):null!==c.durationTime&&(k=c.durationTime);var m=null!==c.minTime?c.minTime:0,n=null!==c.maxTime?c.maxTime:m+w-1;m>=n&&(n+=w),n===w-1&&"string"===a.type(c.timeFormat)&&-1!==c.timeFormat.indexOf("H")&&(n=w);for(var p=c.disableTimeRanges,q=0,v=p.length,h=m;n>=h;h+=60*c.step){var x=h,z=t(x,c.timeFormat);if(c.useSelect){var A=a("