Schedule maker, stats, and minor fixes
This commit is contained in:
		
							parent
							
								
									ee3b874110
								
							
						
					
					
						commit
						26f9dfde89
					
				
							
								
								
									
										5
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Gemfile
									
									
									
									
									
								
							| @ -35,13 +35,16 @@ gem 'geocoder' | ||||
| gem 'paper_trail', '~> 3.0.5' | ||||
| gem 'sitemap_generator' | ||||
| gem 'activerecord-session_store' | ||||
| gem 'paypal-express', '0.7.1' | ||||
| gem 'paypal-express'#, '0.7.1' | ||||
| gem 'sass-json-vars' | ||||
| gem 'premailer-rails' | ||||
| gem 'redcarpet' | ||||
| gem 'sidekiq' | ||||
| gem 'letter_opener' | ||||
| gem 'launchy' | ||||
| # gem 'axlsx' | ||||
| # gem 'excelinator' | ||||
| gem 'to_spreadsheet'#, :git => 'git://github.com/glebm/to_spreadsheet.git' | ||||
| 
 | ||||
| group :test do | ||||
| 	gem 'rspec' | ||||
|  | ||||
| @ -15,24 +15,6 @@ | ||||
| 	window.forEach = function(a, f) { Array.prototype.forEach.call(a, f) }; | ||||
| 	window.forEachElement = function(s, f, p) { forEach((p || document).querySelectorAll(s), f) }; | ||||
| 	 | ||||
| 	forEachElement('.number-field,.email-field,.text-field', function(field) { | ||||
| 		var input = field.querySelector('input'); | ||||
| 		var positionLabel = function(input) {  | ||||
| 			field.classList[input.value ? 'remove' : 'add']('empty'); | ||||
| 		} | ||||
| 		positionLabel(input); | ||||
| 		input.addEventListener('keyup', function(event) { | ||||
| 			positionLabel(event.target); | ||||
| 		}); | ||||
| 		input.addEventListener('blur', function(event) { | ||||
| 			positionLabel(event.target); | ||||
| 			field.classList.remove('focused'); | ||||
| 		}); | ||||
| 		input.addEventListener('focus', function(event) { | ||||
| 			field.classList.add('focused'); | ||||
| 		}); | ||||
| 	}); | ||||
| 	 | ||||
| 	var overlay = document.getElementById('content-overlay'); | ||||
| 	if (overlay) { | ||||
| 		var body = document.querySelector('body'); | ||||
| @ -50,29 +32,35 @@ | ||||
| 		}, true); | ||||
| 		function openDlg(dlg, link) { | ||||
| 			body.setAttribute('style', 'width: ' + body.clientWidth + 'px'); | ||||
| 			dlg.querySelector('.message').innerHTML = decodeURI(link.dataset.confirmation); | ||||
| 			dlg.querySelector('.confirm').addEventListener('click', function(event) { | ||||
| 				event.preventDefault(); | ||||
| 				if (link.tagName == 'BUTTON') { | ||||
| 					var form = link.parentElement | ||||
| 					while (form && form.tagName != 'FORM') { | ||||
| 						var form = form.parentElement | ||||
| 			dlg.querySelector('.message').innerHTML = link.querySelector('.message').innerHTML | ||||
| 			if (link.dataset.infoTitle) { | ||||
| 				dlg.querySelector('.title').innerHTML = decodeURI(link.dataset.infoTitle); | ||||
| 			} | ||||
| 			confirmBtn = dlg.querySelector('.confirm'); | ||||
| 			if (confirmBtn) { | ||||
| 				confirmBtn.addEventListener('click', function(event) { | ||||
| 					event.preventDefault(); | ||||
| 					if (link.tagName == 'BUTTON') { | ||||
| 						var form = link.parentElement | ||||
| 						while (form && form.tagName != 'FORM') { | ||||
| 							var form = form.parentElement | ||||
| 						} | ||||
| 						if (form) { | ||||
| 							var input = document.createElement('input'); | ||||
| 							input.type = 'hidden'; | ||||
| 							input.name = 'button'; | ||||
| 							input.value = link.value; | ||||
| 							form.appendChild(input); | ||||
| 							form.submit(); | ||||
| 						} | ||||
| 					} else { | ||||
| 						window.location.href = link.getAttribute('href'); | ||||
| 					} | ||||
| 					if (form) { | ||||
| 						var input = document.createElement('input'); | ||||
| 						input.type = 'hidden'; | ||||
| 						input.name = 'button'; | ||||
| 						input.value = link.value; | ||||
| 						form.appendChild(input); | ||||
| 						form.submit(); | ||||
| 					} | ||||
| 				} else { | ||||
| 					window.location.href = link.getAttribute('href'); | ||||
| 				} | ||||
| 			}); | ||||
| 				}); | ||||
| 			} | ||||
| 			primaryContent.setAttribute('aria-hidden', 'true'); | ||||
| 			document.getElementById('overlay').onclick = | ||||
| 				dlg.querySelector('.delete').onclick = function() { closeDlg(dlg); }; | ||||
| 				dlg.querySelector('.close').onclick = function() { closeDlg(dlg); }; | ||||
| 			body.classList.add('has-overlay'); | ||||
| 			dlg.removeAttribute('aria-hidden'); | ||||
| 			dlg.setAttribute('role', 'alertdialog'); | ||||
| @ -99,11 +87,14 @@ | ||||
| 				return false; | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	var errorField = document.querySelector('.input-field.has-error input, .input-field.has-error textarea'); | ||||
| 	if (errorField) { | ||||
| 		errorField.focus(); | ||||
| 		var infoDlg = document.getElementById('info-dlg'); | ||||
| 		forEachElement('[data-info-text]', function(link) { | ||||
| 			link.addEventListener('click', function(event) { | ||||
| 				event.preventDefault(); | ||||
| 				openDlg(infoDlg, link); | ||||
| 				return false; | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| 	 | ||||
| 	var htmlNode = document.documentElement; | ||||
| @ -120,28 +111,73 @@ | ||||
| 			htmlNode.setAttribute('data-input', 'mouse'); | ||||
| 		} | ||||
| 	}); | ||||
| 	forEachElement('form.js-xhr', function(form) { | ||||
| 		if (form.addEventListener) { | ||||
| 			form.addEventListener('submit', function(event) { | ||||
| 				event.preventDefault(); | ||||
| 				form.classList.add('requesting'); | ||||
| 				var data = new FormData(form); | ||||
| 				var request = new XMLHttpRequest(); | ||||
| 				request.onreadystatechange = function() { | ||||
| 					if (request.readyState == 4) { | ||||
| 						form.classList.remove('requesting'); | ||||
| 						if (request.status == 200) { | ||||
| 							var response = JSON.parse(request.responseText); | ||||
| 							for (var i = 0; i < response.length; i++) { | ||||
| 								form.querySelector(response[i].selector).innerHTML = response[i].html; | ||||
| 
 | ||||
| 	var errorField = document.querySelector('.input-field.has-error input, .input-field.has-error textarea'); | ||||
| 	if (errorField) { | ||||
| 		errorField.focus(); | ||||
| 	} | ||||
| 	 | ||||
| 	window.initNodeFunctions = [ function(node) { | ||||
| 		forEachElement('.number-field,.email-field,.text-field', function(field) { | ||||
| 			var input = field.querySelector('input'); | ||||
| 			var positionLabel = function(input) {  | ||||
| 				field.classList[input.value ? 'remove' : 'add']('empty'); | ||||
| 			} | ||||
| 			positionLabel(input); | ||||
| 			input.addEventListener('keyup', function(event) { | ||||
| 				positionLabel(event.target); | ||||
| 			}); | ||||
| 			input.addEventListener('blur', function(event) { | ||||
| 				positionLabel(event.target); | ||||
| 				field.classList.remove('focused'); | ||||
| 			}); | ||||
| 			input.addEventListener('focus', function(event) { | ||||
| 				field.classList.add('focused'); | ||||
| 			}); | ||||
| 		}, node || document); | ||||
| 		forEachElement('form.js-xhr', function(form) { | ||||
| 			if (form.addEventListener) { | ||||
| 				form.addEventListener('submit', function(event) { | ||||
| 					event.preventDefault(); | ||||
| 					form.classList.add('requesting'); | ||||
| 					var data = new FormData(form); | ||||
| 					var request = new XMLHttpRequest(); | ||||
| 					request.onreadystatechange = function() { | ||||
| 						if (request.readyState == 4) { | ||||
| 							form.classList.remove('requesting'); | ||||
| 							if (request.status == 200) { | ||||
| 								var response = JSON.parse(request.responseText); | ||||
| 								for (var i = 0; i < response.length; i++) { | ||||
| 									var element; | ||||
| 									if (response[i].selector) { | ||||
| 										element = form.querySelector(response[i].selector); | ||||
| 									} | ||||
| 									if (response[i].globalSelector) { | ||||
| 										element = document.querySelector(response[i].globalSelector); | ||||
| 									} | ||||
| 
 | ||||
| 									if (response[i].html) { | ||||
| 										element.innerHTML = response[i].html; | ||||
| 										window.initNode(element); | ||||
| 									} | ||||
| 									if (response[i].className) { | ||||
| 										element.className = response[i].className; | ||||
| 									} | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 				request.open('POST', form.getAttribute('action'), true); | ||||
| 				request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); | ||||
| 				request.send(data); | ||||
| 			}, false); | ||||
| 		} | ||||
| 	}); | ||||
| 					request.open('POST', form.getAttribute('action'), true); | ||||
| 					request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); | ||||
| 					request.send(data); | ||||
| 				}, false); | ||||
| 			} | ||||
| 		}, node || document); | ||||
| 	} ]; | ||||
| 	window.initNode = function(node) { | ||||
| 		forEach(initNodeFunctions, function(fn) { | ||||
| 			fn(node); | ||||
| 		}); | ||||
| 	}; | ||||
| 	initNode(); | ||||
| })(); | ||||
|  | ||||
| @ -13,7 +13,7 @@ body { | ||||
| 	padding-bottom: 20vw; | ||||
| } | ||||
| 
 | ||||
| h1, h2, h3, h4, h5, label, button, .button, dt, th, .table-th, nav.sub-menu { | ||||
| h1, h2, h3, h4, h5, h6, label, button, .button, dt, th, .table-th, nav.sub-menu { | ||||
| 	@include font-family(secondary); | ||||
| 	font-weight: normal; | ||||
| } | ||||
| @ -31,6 +31,18 @@ h3.subtitle { | ||||
| 	font-size: 1.5em; | ||||
| } | ||||
| 
 | ||||
| h4 { | ||||
| 	font-size: 1.25em; | ||||
| } | ||||
| 
 | ||||
| h5 { | ||||
| 	font-size: 1.125em; | ||||
| } | ||||
| 
 | ||||
| h6 { | ||||
| 	font-size: 1em; | ||||
| } | ||||
| 
 | ||||
| p { | ||||
| 	font-size: 4vw; | ||||
| } | ||||
| @ -41,6 +53,7 @@ a { | ||||
| 	border-bottom: 0 solid; | ||||
| 	outline: 0; | ||||
| 	position: relative; | ||||
| 	cursor: pointer; | ||||
| 
 | ||||
| 	@include after { | ||||
| 		content: ''; | ||||
| @ -101,10 +114,25 @@ table, .table { | ||||
| 		&:last-child { | ||||
| 			padding-right: 1em; | ||||
| 		}*/ | ||||
| 
 | ||||
| 		&.status { | ||||
| 			width: 0.1rem; | ||||
| 			background-color: transparent; | ||||
| 			border: 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	th, .table-th { | ||||
| 		background-color: #F8F8F8; | ||||
| 
 | ||||
| 		&.corner { | ||||
| 			background-color: transparent; | ||||
| 			border: 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	tbody th { | ||||
| 		width: 0.1rem; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @ -382,18 +410,25 @@ input { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .hidden { | ||||
| 	display: none !important; | ||||
| } | ||||
| 
 | ||||
| .field-error { | ||||
| 	display: block; | ||||
| 	background-color: rgba($colour-2, 0.333); | ||||
| 	@include font-family(secondary); | ||||
| 	padding: 0.5em 1em; | ||||
| 	margin: 0 0.2em; | ||||
| 	text-align: center; | ||||
| 	@include default-box-shadow(top, 2); | ||||
| } | ||||
| 
 | ||||
| .input-field { | ||||
| 	.field-error { | ||||
| 		display: block; | ||||
| 		float: right; | ||||
| 		background-color: rgba($colour-2, 0.333); | ||||
| 		@include font-family(secondary); | ||||
| 		padding: 0.5em 1em; | ||||
| 		margin: 0 0.2em; | ||||
| 		text-align: center; | ||||
| 		@include _(transform, skewX(-15deg)); | ||||
| 		@include _(transform-origin, 0 100%); | ||||
| 		@include default-box-shadow(top, 2); | ||||
| 		@include _(animation, bend ease-in-out 500ms infinite alternate both); | ||||
| 	} | ||||
| 
 | ||||
| @ -463,10 +498,9 @@ input { | ||||
| } | ||||
| 
 | ||||
| .input-field-help { | ||||
| 	margin-bottom: 1em; | ||||
| 	margin-left: 1em; | ||||
| 	margin: 0.5em 1em 0; | ||||
| 	line-height: 1.3333em; | ||||
| 	font-size: 1.25em; | ||||
| 	font-size: 1.125em; | ||||
| } | ||||
| 
 | ||||
| .check-box-field, | ||||
| @ -1136,13 +1170,10 @@ ul.warnings li, | ||||
| 	margin: 1em 0 0; | ||||
| 	padding: 0; | ||||
| 	list-style: none; | ||||
| 	// border: 0.1em solid #EEE; | ||||
| 	// background-color: #F8F8F8; | ||||
| 
 | ||||
| 	a { | ||||
| 		display: block; | ||||
| 		padding: 0.5em 0.75em; | ||||
| 		//@include font-family(secondary); | ||||
| 		border: 0.1rem solid #EEE; | ||||
| 		border-top: 0; | ||||
| 		border-right: 0; | ||||
| @ -1158,10 +1189,6 @@ ul.warnings li, | ||||
| 	li { | ||||
| 		margin: 0; | ||||
| 
 | ||||
| 		&:last-child a { | ||||
| 			border: 0; | ||||
| 		} | ||||
| 
 | ||||
| 		&.current { | ||||
| 			a { | ||||
| 				color: $white; | ||||
| @ -1171,6 +1198,44 @@ ul.warnings li, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .data-set { | ||||
| 	display: table-row; | ||||
| } | ||||
| 
 | ||||
| .data-set-key, .data-set-value { | ||||
| 	display: table-cell; | ||||
| 	padding: 0.25em 0.5em; | ||||
| 	vertical-align: top; | ||||
| 	border-bottom: 0.1rem solid #EEE; | ||||
| } | ||||
| 
 | ||||
| .data-set-key { | ||||
| 	font-size: 1em; | ||||
| 	width: 1rem; | ||||
| 	white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| .data-set:last-child { | ||||
| 	.data-set-key, .data-set-value { | ||||
| 		border: 0; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .details { | ||||
| 	display: table; | ||||
| 	width: 100%; | ||||
| 
 | ||||
| 	&.inline { | ||||
| 		width: auto; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .space, .address { | ||||
| 	.data-set-key, .data-set-value { | ||||
| 		white-space: nowrap; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .admin-blocks { | ||||
| 	@include _-(display, inline-flex); | ||||
| 	@include _(flex-wrap, wrap); | ||||
| @ -1199,34 +1264,6 @@ ul.warnings li, | ||||
| 		border-bottom: 0.1rem solid #EEE; | ||||
| 	} | ||||
| 
 | ||||
| 	.details { | ||||
| 		display: table; | ||||
| 		width: 100%; | ||||
| 	} | ||||
| 
 | ||||
| 	.data-set { | ||||
| 		display: table-row; | ||||
| 	} | ||||
| 
 | ||||
| 	.data-set-key, .data-set-value { | ||||
| 		display: table-cell; | ||||
| 		padding: 0.25em 0.5em; | ||||
| 		vertical-align: top; | ||||
| 	    border-bottom: 0.1rem solid #EEE; | ||||
| 	} | ||||
| 	 | ||||
| 	.data-set:last-child { | ||||
| 		.data-set-key, .data-set-value { | ||||
| 			border: 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.space, .address { | ||||
| 		.data-set-key, .data-set-value { | ||||
| 			white-space: nowrap; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	.amenities { | ||||
| 		list-style: none; | ||||
| 		padding: 0; | ||||
| @ -1342,7 +1379,7 @@ ul.warnings li, | ||||
| 	#guests { | ||||
| 		.guests { | ||||
| 			@include _-(display, flex); | ||||
| 			@include _(align-items, flex-start); | ||||
| 			//@include _(align-items, flex-start); | ||||
| 			@include _(flex-wrap, wrap); | ||||
| 			list-style: none; | ||||
| 			padding: 0; | ||||
| @ -1366,7 +1403,7 @@ ul.warnings li, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| #admin-schedule { | ||||
| #admin-workshop_times { | ||||
| 	.workshop-blocks { | ||||
| 		td, th { | ||||
| 			vertical-align: top; | ||||
| @ -1386,11 +1423,177 @@ ul.warnings li, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .details.org-members { | ||||
| 	padding: 1em; | ||||
| 	border: 0.1rem solid #EEE; | ||||
| 	border-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| table.schedule { | ||||
| 	width: 100%; | ||||
| 	margin: 0 0 1em; | ||||
| 
 | ||||
| 	td { | ||||
| 		text-align: center; | ||||
| 
 | ||||
| 		&.empty { | ||||
| 			border-top: 0; | ||||
| 			border-bottom: 0; | ||||
| 			background-color: #F8F8F8; | ||||
| 		} | ||||
| 
 | ||||
| 		&.workshop { | ||||
| 			background-color: lighten($colour-1, 40%); | ||||
| 
 | ||||
| 			&.filled { | ||||
| 				background-color: lighten($colour-1, 25%); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		&.event { | ||||
| 			background-color: lighten($colour-2, 25%); | ||||
| 		} | ||||
| 
 | ||||
| 		&.meal { | ||||
| 			background-color: lighten($colour-3, 25%); | ||||
| 		} | ||||
| 
 | ||||
| 		.title { | ||||
| 			@include font-family(secondary); | ||||
| 		} | ||||
| 
 | ||||
| 		.status { | ||||
| 			display: inline-block; | ||||
| 			text-align: left; | ||||
| 			float: right; | ||||
| 			font-size: 0.9em; | ||||
| 			margin-top: 0.5em; | ||||
| 		} | ||||
| 
 | ||||
| 		.conflict-score { | ||||
| 			text-align: right; | ||||
| 
 | ||||
| 			.title { | ||||
| 				@include font-family(secondary); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		.errors { | ||||
| 			position: relative; | ||||
| 			border-top: 0.1rem solid #666; | ||||
| 			margin-top: 0.5em; | ||||
| 			padding-top: 0.25em; | ||||
| 
 | ||||
| 			@include before { | ||||
| 				content: '!'; | ||||
| 				display: inline-block; | ||||
| 				position: absolute; | ||||
| 				z-index: 1; | ||||
| 				top: 0; | ||||
| 				left: -1.2em; | ||||
| 				width: 1em; | ||||
| 				color: $white; | ||||
| 				@include font-family(secondary); | ||||
| 			} | ||||
| 
 | ||||
| 			@include after { | ||||
| 				content: ''; | ||||
| 				position: absolute; | ||||
| 				top: 0.1em; | ||||
| 				left: -1.333em; | ||||
| 				width: 0; | ||||
| 				height: 0; | ||||
| 				font-size: 1.25em; | ||||
| 				border: 0.5em solid; | ||||
| 				border-left-color: transparent; | ||||
| 				border-right-color: transparent; | ||||
| 				border-width: 0 0.5em 0.75em; | ||||
| 				color: darken($colour-2, 25%); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		#main .columns & form { | ||||
| 			margin-top: 0; | ||||
| 			 | ||||
| 			button { | ||||
| 				margin-top: 0.5em; | ||||
| 				float: left; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| #admin-schedule { | ||||
| 	.workshop-list { | ||||
| 		@include _-(display, flex); | ||||
| 		@include _(flex-wrap, wrap); | ||||
| 		padding: 0; | ||||
| 
 | ||||
| 		li { | ||||
| 			@include _(flex, 1); | ||||
| 			@include _(flex-basis, 48%); | ||||
| 			margin: 1%; | ||||
| 			@include _(transition, background-color 250ms ease-in-out); | ||||
| 
 | ||||
| 			.title { | ||||
| 				@include _(transition, background-color 250ms ease-in-out); | ||||
| 			} | ||||
| 
 | ||||
| 			&.booked { | ||||
| 				background-color: lighten($colour-1, 40%); | ||||
| 
 | ||||
| 				.not-booked-only { | ||||
| 					display: none; | ||||
| 				} | ||||
| 
 | ||||
| 				.data-set-key, .data-set-value { | ||||
| 					border-bottom-color: #888; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			&.not-booked { | ||||
| 
 | ||||
| 				.booked-only { | ||||
| 					display: none; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			.field-error { | ||||
| 				background-color: lighten($colour-2, 22.5%); | ||||
| 				margin: 0; | ||||
| 			} | ||||
| 
 | ||||
| 			.already-booked { | ||||
| 				overflow: hidden; | ||||
| 				max-height: 0; | ||||
| 
 | ||||
| 				&.is-true { | ||||
| 					max-height: 3em; | ||||
| 					@include _(transition, max-height 150ms ease-in-out); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			.workshop-description { | ||||
| 				max-height: none; | ||||
| 			} | ||||
| 
 | ||||
| 			.details { | ||||
| 				margin: 0 5% 1em; | ||||
| 				width: 90%; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		#main .columns & form { | ||||
| 			margin: 0; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| #main { | ||||
| 	position: relative; | ||||
| 	background-color: $white; | ||||
| 	padding-bottom: rems(2); | ||||
| 	flex: 1; | ||||
| 	@include _(flex, 1); | ||||
| 
 | ||||
| 	article { | ||||
| 		padding: rems(2.5) 0; | ||||
| @ -1816,6 +2019,8 @@ body { | ||||
| 		bottom: 0; | ||||
| 		left: 0; | ||||
| 		max-width: 50rem; | ||||
| 		max-height: 100%; | ||||
| 		overflow-y: auto; | ||||
| 		margin: auto; | ||||
| 		z-index: 1001; | ||||
| 		background-color: $white; | ||||
| @ -1841,9 +2046,34 @@ body { | ||||
| 	} | ||||
| 
 | ||||
| 	.message { | ||||
| 		font-size: 1.5em; | ||||
| 		margin-bottom: 2em; | ||||
| 	} | ||||
| 
 | ||||
| 	#info-dlg .title { | ||||
| 		background-color: $colour-1; | ||||
| 		font-size: 1.5em; | ||||
| 		text-align: left; | ||||
| 	} | ||||
| 
 | ||||
| 	#info-dlg .message { | ||||
| 		text-align: left; | ||||
| 
 | ||||
| 		p, h4 { | ||||
| 			font-size: 0.8em; | ||||
| 		} | ||||
| 
 | ||||
| 		h3 { | ||||
| 			font-size: 1em; | ||||
| 		} | ||||
| 
 | ||||
| 		h5 { | ||||
| 			font-size: 0.667em; | ||||
| 		} | ||||
| 
 | ||||
| 		h6 { | ||||
| 			font-size: 0.8em; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @include keyframes(fade-out) { | ||||
| @ -3150,7 +3380,7 @@ html[data-ontop] { | ||||
| 				} | ||||
| 
 | ||||
| 				&:last-child { | ||||
| 					@include _(border-radius, 0 0.15em 0.15em 0); | ||||
| 					@include _(border-radius, 0 0 0.15em 0.15em); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| @ -798,6 +798,39 @@ class ConferencesController < ApplicationController | ||||
| 			@page_title = 'articles.conference_registration.headings.Administration' | ||||
| 
 | ||||
| 			case @admin_step.to_sym | ||||
| 			when :stats | ||||
| 				@registrations = ConferenceRegistration.where(:conference_id => @this_conference.id) | ||||
| 				if request.format.xlsx? | ||||
| 					logger.info "Generating stats.xls" | ||||
| 					@excel_data = { | ||||
| 						columns: [:name, :email, :city, :date, :languages], | ||||
| 						column_types: {date: :date}, | ||||
| 						keys: { | ||||
| 								name: 'forms.labels.generic.name', | ||||
| 								email: 'forms.labels.generic.email', | ||||
| 								city: 'forms.labels.generic.location', | ||||
| 								date: 'articles.conference_registration.terms.Date', | ||||
| 								languages: 'articles.conference_registration.terms.Languages' | ||||
| 							}, | ||||
| 						data: [], | ||||
| 					} | ||||
| 					@registrations.each do | r | | ||||
| 						user = r.user_id ? User.where(id: r.user_id).first : nil | ||||
| 						if user.present? | ||||
| 							@excel_data[:data] << { | ||||
| 								name: user.firstname || '', | ||||
| 								email: user.email || '', | ||||
| 								date: r.created_at ? r.created_at.strftime("%F %T") : '', | ||||
| 								city: r.city || '', | ||||
| 								languages: ((r.languages || []).map { |x| view_context.language x }).join(', ').to_s | ||||
| 							} | ||||
| 						end | ||||
| 					end | ||||
| 					return respond_to do | format | | ||||
| 						# format.html | ||||
| 						format.xlsx { render xlsx: :stats, filename: "stats-#{DateTime.now.strftime('%Y-%m-%d')}" } | ||||
| 					end | ||||
| 				end | ||||
| 			when :housing | ||||
| 				# do a full analysis | ||||
| 				analyze_housing | ||||
| @ -811,6 +844,13 @@ class ConferencesController < ApplicationController | ||||
| 				@length = 1.5 | ||||
| 			when :meals | ||||
| 				@meals = Hash[@this_conference.meals.map{ |k, v| [k.to_i, v] }].sort.to_h | ||||
| 			when :workshop_times | ||||
| 				get_block_data | ||||
| 				@workshop_blocks << { | ||||
| 					'time' => nil, | ||||
| 					'length' => 1.0, | ||||
| 					'days' => [] | ||||
| 				} | ||||
| 			when :schedule | ||||
| 				get_scheule_data | ||||
| 			end | ||||
| @ -820,22 +860,8 @@ class ConferencesController < ApplicationController | ||||
| 
 | ||||
| 	end | ||||
| 
 | ||||
| 	def get_scheule_data | ||||
| 		@meals = Hash[@this_conference.meals.map{ |k, v| [k.to_i, v] }].sort.to_h | ||||
| 		@events = Event.where(:conference_id => @this_conference.id).order(start_time: :asc) | ||||
| 		@workshops = Workshop.where(:conference_id => @this_conference.id).order(start_time: :asc) | ||||
| 		@locations = {} | ||||
| 	def get_block_data | ||||
| 		@workshop_blocks = @this_conference.workshop_blocks || [] | ||||
| 		@workshop_blocks << { | ||||
| 			'time' => nil, | ||||
| 			'length' => 1.0, | ||||
| 			'days' => [] | ||||
| 		} | ||||
| 		@workshops.each do |workshop| | ||||
| 			if workshop.location_id | ||||
| 				@locations[workshop.location_id] ||= workshop.location | ||||
| 			end | ||||
| 		end | ||||
| 		@block_days = [] | ||||
| 		day = @this_conference.start_date | ||||
| 		while day <= @this_conference.end_date | ||||
| @ -844,6 +870,168 @@ class ConferencesController < ApplicationController | ||||
| 		end | ||||
| 	end | ||||
| 
 | ||||
| 	def get_scheule_data | ||||
| 		@meals = Hash[@this_conference.meals.map{ |k, v| [k.to_i, v] }].sort.to_h | ||||
| 		@events = Event.where(:conference_id => @this_conference.id).order(start_time: :asc) | ||||
| 		@workshops = Workshop.where(:conference_id => @this_conference.id).order(start_time: :asc) | ||||
| 		@locations = {} | ||||
| 
 | ||||
| 		get_block_data | ||||
| 
 | ||||
| 		@schedule = {} | ||||
| 		day_1 = @this_conference.start_date.wday | ||||
| 
 | ||||
| 		@workshop_blocks.each_with_index do | info, block | | ||||
| 			info['days'].each do | block_day | | ||||
| 				day_diff = block_day.to_i - day_1 | ||||
| 				day_diff += 7 if day_diff < 0 | ||||
| 				day = (@this_conference.start_date + day_diff.days).to_date | ||||
| 				time = info['time'].to_f | ||||
| 				@schedule[day] ||= { times: {}, locations: {} } | ||||
| 				@schedule[day][:times][time] ||= {} | ||||
| 				@schedule[day][:times][time][:type] = :workshop | ||||
| 				@schedule[day][:times][time][:length] = info['length'].to_f | ||||
| 				@schedule[day][:times][time][:item] = { block: block, workshops: {} } | ||||
| 			end | ||||
| 		end | ||||
| 
 | ||||
| 		@workshops.each do | workshop | | ||||
| 			if workshop.block.present? | ||||
| 				block = @workshop_blocks[workshop.block['block'].to_i] | ||||
| 				day_diff = workshop.block['day'].to_i - day_1 | ||||
| 				day_diff += 7 if day_diff < 0 | ||||
| 				day = (@this_conference.start_date + day_diff.days).to_date | ||||
| 
 | ||||
| 				if @schedule[day].present? && @schedule[day][:times].present? && @schedule[day][:times][block['time'].to_f].present? | ||||
| 					@schedule[day][:times][block['time'].to_f][:item][:workshops][workshop.event_location_id] = { workshop: workshop, status: { errors: [], warnings: [], conflict_score: nil } } | ||||
| 					@schedule[day][:locations][workshop.event_location_id] ||= workshop.event_location | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 
 | ||||
| 		@meals.each do | time, meal | | ||||
| 			day = meal['day'].to_date | ||||
| 			time = meal['time'].to_f | ||||
| 			@schedule[day] ||= {} | ||||
| 			@schedule[day][:times] ||= {} | ||||
| 			@schedule[day][:times][time] ||= {} | ||||
| 			@schedule[day][:times][time][:type] = :meal | ||||
| 			@schedule[day][:times][time][:length] = (meal['length'] || 1.0).to_f | ||||
| 			@schedule[day][:times][time][:item] = meal | ||||
| 		end | ||||
| 
 | ||||
| 		@events.each do | event | | ||||
| 			day = event.start_time.midnight.to_date | ||||
| 			time = event.start_time.hour.to_f + (event.start_time.min / 60.0) | ||||
| 			@schedule[day] ||= {} | ||||
| 			@schedule[day][:times] ||= {} | ||||
| 			@schedule[day][:times][time] ||= {} | ||||
| 			@schedule[day][:times][time][:type] = :event | ||||
| 			@schedule[day][:times][time][:length] = (event.end_time - event.start_time) / 3600.0 | ||||
| 			@schedule[day][:times][time][:item] = event | ||||
| 		end | ||||
| 
 | ||||
| 		@schedule = @schedule.sort.to_h | ||||
| 		@schedule.each do | day, data | | ||||
| 			@schedule[day][:times] = data[:times].sort.to_h | ||||
| 		end | ||||
| 
 | ||||
| 		@schedule.each do | day, data | | ||||
| 			last_event = nil | ||||
| 			data[:times].each do | time, time_data | | ||||
| 				if last_event.present? | ||||
| 					@schedule[day][:times][last_event][:next_event] = time | ||||
| 				end | ||||
| 				last_event = time | ||||
| 			end | ||||
| 		end | ||||
| 
 | ||||
| 		@schedule.deep_dup.each do | day, data | | ||||
| 			data[:times].each do | time, time_data | | ||||
| 				if time_data[:next_event].present? || time_data[:length] > 0.5 | ||||
| 					span = 0.5 | ||||
| 					length = time_data[:next_event].present? ? time_data[:next_event] - time : time_data[:length] | ||||
| 					while span < length | ||||
| 						@schedule[day][:times][time + span] ||= { | ||||
| 							type: (span >= time_data[:length] ? :empty : :nil), | ||||
| 							length: 0.5 | ||||
| 						} | ||||
| 						span += 0.5 | ||||
| 					end | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 
 | ||||
| 		@schedule = @schedule.sort.to_h | ||||
| 		@schedule.each do | day, data | | ||||
| 			@schedule[day][:times] = data[:times].sort.to_h | ||||
| 
 | ||||
| 			data[:times].each do | time, time_data | | ||||
| 				if time_data[:type] == :workshop && time_data[:item].present? && time_data[:item][:workshops].present? | ||||
| 					ids = time_data[:item][:workshops].keys | ||||
| 					(0..ids.length).each do | i | | ||||
| 						if time_data[:item][:workshops][ids[i]].present? | ||||
| 							workshop_i = time_data[:item][:workshops][ids[i]][:workshop] | ||||
| 							conflicts = {} | ||||
| 							 | ||||
| 							(i+1..ids.length).each do | j | | ||||
| 								workshop_j = time_data[:item][:workshops][ids[j]].present? ? time_data[:item][:workshops][ids[j]][:workshop] : nil | ||||
| 								if workshop_i.present? && workshop_j.present? | ||||
| 									workshop_i.active_facilitators.each do | facilitator_i | | ||||
| 										workshop_j.active_facilitators.each do | facilitator_j | | ||||
| 											if facilitator_i.id == facilitator_j.id | ||||
| 												@schedule[day][:times][time][:status] ||= {} | ||||
| 												@schedule[day][:times][time][:item][:workshops][ids[j]][:status][:errors] << { | ||||
| 														name: :common_facilitator, | ||||
| 														facilitator: facilitator_i, | ||||
| 														workshop: workshop_i, | ||||
| 														i18nVars: { | ||||
| 															facilitator_name: facilitator_i.name, | ||||
| 															workshop_title: workshop_i.title | ||||
| 														} | ||||
| 													} | ||||
| 											end | ||||
| 										end | ||||
| 									end | ||||
| 								end | ||||
| 							end | ||||
| 
 | ||||
| 							(0..ids.length).each do | j | | ||||
| 								workshop_j = time_data[:item][:workshops][ids[j]].present? ? time_data[:item][:workshops][ids[j]][:workshop] : nil | ||||
| 								if workshop_i.present? && workshop_j.present? && workshop_i.id != workshop_j.id | ||||
| 									workshop_i.interested.each do | interested_i | | ||||
| 										workshop_j.interested.each do | interested_j | | ||||
| 											conflicts[interested_i.id] ||= true | ||||
| 										end | ||||
| 									end | ||||
| 								end | ||||
| 							end | ||||
| 
 | ||||
| 							needs = JSON.parse(workshop_i.needs || '[]').map &:to_sym | ||||
| 							amenities = JSON.parse(workshop_i.event_location.amenities || '[]').map &:to_sym | ||||
| 
 | ||||
| 							needs.each do | need | | ||||
| 								@schedule[day][:times][time][:item][:workshops][ids[i]][:status][:errors] << { | ||||
| 										name: :need_not_available, | ||||
| 										need: need, | ||||
| 										location: workshop_i.event_location, | ||||
| 										workshop: workshop_i, | ||||
| 										i18nVars: { | ||||
| 											need: need.to_s, | ||||
| 											location: workshop_i.event_location.title, | ||||
| 											workshop_title: workshop_i.title | ||||
| 										} | ||||
| 									} unless amenities.include? need | ||||
| 							end | ||||
| 
 | ||||
| 							@schedule[day][:times][time][:item][:workshops][ids[i]][:status][:conflict_score] = workshop_i.interested.present? ? (conflicts.length / workshop_i.interested.size) : 0 | ||||
| 						end | ||||
| 					end | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 	end | ||||
| 
 | ||||
| 	def get_housing_data | ||||
| 		@hosts = {} | ||||
| 		@guests = {} | ||||
| @ -948,13 +1136,24 @@ class ConferencesController < ApplicationController | ||||
| 
 | ||||
| 		case params[:admin_step] | ||||
| 		when 'edit' | ||||
| 			@this_conference.info = LinguaFranca::ActiveRecord::UntranslatedValue.new(params[:info]) unless @this_conference.info! == params[:info] | ||||
| 			case params[:button] | ||||
| 			when 'save' | ||||
| 				@this_conference.info = LinguaFranca::ActiveRecord::UntranslatedValue.new(params[:info]) unless @this_conference.info! == params[:info] | ||||
| 
 | ||||
| 			params[:info_translations].each do | locale, value | | ||||
| 				@this_conference.set_column_for_locale(:info, locale, value, current_user.id) unless value = @this_conference._info(locale) | ||||
| 				params[:info_translations].each do | locale, value | | ||||
| 					@this_conference.set_column_for_locale(:info, locale, value, current_user.id) unless value = @this_conference._info(locale) | ||||
| 				end | ||||
| 				@this_conference.save | ||||
| 				return redirect_to register_step_path(@this_conference.slug, :administration) | ||||
| 			when 'add_member' | ||||
| 				org = nil | ||||
| 				@this_conference.organizations.each do | organization | | ||||
| 					org = organization if organization.id == params[:org_id].to_i | ||||
| 				end | ||||
| 				org.users << (User.get params[:email]) | ||||
| 				org.save | ||||
| 				return redirect_to administration_step_path(@this_conference.slug, :edit) | ||||
| 			end | ||||
| 			@this_conference.save | ||||
| 			return redirect_to register_step_path(@this_conference.slug, :administration) | ||||
| 		when 'housing' | ||||
| 			space = params[:button].split(':')[0] | ||||
| 			host_id = params[:button].split(':')[1].to_i | ||||
| @ -1091,7 +1290,7 @@ class ConferencesController < ApplicationController | ||||
| 
 | ||||
| 				return redirect_to administration_step_path(@this_conference.slug, :events) | ||||
| 			end | ||||
| 		when 'schedule' | ||||
| 		when 'workshop_times' | ||||
| 			case params[:button] | ||||
| 			when 'save_block' | ||||
| 				@this_conference.workshop_blocks ||= [] | ||||
| @ -1101,7 +1300,67 @@ class ConferencesController < ApplicationController | ||||
| 					'days' => params[:days].keys | ||||
| 				} | ||||
| 				@this_conference.save | ||||
| 				return redirect_to administration_step_path(@this_conference.slug, :schedule) | ||||
| 				return redirect_to administration_step_path(@this_conference.slug, :workshop_times) | ||||
| 			end | ||||
| 		when 'schedule' | ||||
| 			success = false | ||||
| 			 | ||||
| 			case params[:button] | ||||
| 			when 'schedule_workshop' | ||||
| 				workshop = Workshop.find_by!(conference_id: @this_conference.id, id: params[:id]) | ||||
| 				booked = false | ||||
| 				workshop.event_location_id = params[:event_location] | ||||
| 				block_data = params[:workshop_block].split(':') | ||||
| 				workshop.block = { | ||||
| 					day: block_data[0].to_i, | ||||
| 					block: block_data[1].to_i | ||||
| 				} | ||||
| 
 | ||||
| 				# make sure this spot isn't already taken | ||||
| 				Workshop.where(:conference_id => @this_conference.id).each do | w | | ||||
| 					if request.xhr? | ||||
| 						if w.block.present? && | ||||
| 								w.id != workshop.id && | ||||
| 								w.block['day'] == workshop.block['day'] && | ||||
| 								w.block['block'] == workshop.block['block'] && | ||||
| 								w.event_location_id == workshop.event_location_id | ||||
| 							return render json: [ { | ||||
| 									selector: '.already-booked', | ||||
| 									className: 'already-booked is-true' | ||||
| 								} ] | ||||
| 						end | ||||
| 					else | ||||
| 						return redirect_to administration_step_path(@this_conference.slug, :schedule) | ||||
| 					end | ||||
| 				end | ||||
| 				 | ||||
| 				workshop.save! | ||||
| 				success = true | ||||
| 			when 'deschedule_workshop' | ||||
| 				workshop = Workshop.find_by!(conference_id: @this_conference.id, id: params[:id]) | ||||
| 				workshop.event_location_id = nil | ||||
| 				workshop.block = nil | ||||
| 				workshop.save! | ||||
| 				success = true | ||||
| 			end | ||||
| 
 | ||||
| 			if success | ||||
| 				if request.xhr? | ||||
| 					get_scheule_data | ||||
| 					schedule = render_to_string partial: 'conferences/admin/schedule' | ||||
| 					return render json: [ { | ||||
| 							globalSelector: '#schedule-preview', | ||||
| 							html: schedule | ||||
| 						}, { | ||||
| 							globalSelector: "#workshop-#{workshop.id}", | ||||
| 							className: workshop.block.present? ? 'booked' : 'not-booked' | ||||
| 						}, { | ||||
| 							globalSelector: "#workshop-#{workshop.id} .already-booked", | ||||
| 							className: 'already-booked' | ||||
| 						} ] | ||||
| 				else | ||||
| 					return redirect_to administration_step_path(@this_conference.slug, :schedule) | ||||
| 				end | ||||
| 			end | ||||
| 		end | ||||
| 		do_404 | ||||
|  | ||||
| @ -708,12 +708,16 @@ module ApplicationHelper | ||||
| 	end | ||||
| 
 | ||||
| 	def data_set(header_type, header_key, attributes = {}, &block) | ||||
| 		raw_data_set(header_type, _(header_key), attributes, &block) | ||||
| 	end | ||||
| 
 | ||||
| 	def raw_data_set(header_type, header, attributes = {}, &block) | ||||
| 		attributes[:class] = attributes[:class].split(' ') if attributes[:class].is_a?(String) | ||||
| 		attributes[:class] = [attributes[:class].to_s] if attributes[:class].is_a?(Symbol) | ||||
| 		attributes[:class] ||= [] | ||||
| 		attributes[:class] << 'data-set' | ||||
| 		content_tag(:div, attributes) do | ||||
| 			content_tag(header_type, _(header_key), class: 'data-set-key') + | ||||
| 			content_tag(header_type, header, class: 'data-set-key') + | ||||
| 			content_tag(:div, class: 'data-set-value', &block) | ||||
| 		end | ||||
| 	end | ||||
| @ -774,6 +778,16 @@ module ApplicationHelper | ||||
| 		selectfield :time_span, value, lengths, args | ||||
| 	end | ||||
| 
 | ||||
| 	def block_select(value = nil, args = {}) | ||||
| 		blocks = {} | ||||
| 		@workshop_blocks.each_with_index do | info, block | | ||||
| 			info['days'].each do | day | | ||||
| 				blocks[(day.to_i * 10) + block] = [ "#{(I18n.t 'date.day_names')[day.to_i]} Block #{block + 1}", "#{day}:#{block}" ] | ||||
| 			end | ||||
| 		end | ||||
| 		selectfield :workshop_block, value, blocks.sort.to_h.values, args | ||||
| 	end | ||||
| 
 | ||||
| 	def location_select(value = nil, args = {}) | ||||
| 		locations = [] | ||||
| 		if @this_conference.event_locations.present? | ||||
| @ -844,7 +858,7 @@ module ApplicationHelper | ||||
| 	end | ||||
| 
 | ||||
| 	def admin_steps | ||||
| 		[:edit, :stats, :broadcast, :housing, :locations, :meals, :events, :schedule] | ||||
| 		[:edit, :stats, :broadcast, :housing, :locations, :meals, :events, :workshop_times, :schedule] | ||||
| 	end | ||||
| 
 | ||||
| 	def valid_admin_steps | ||||
| @ -903,15 +917,29 @@ module ApplicationHelper | ||||
| 	def link_with_confirmation(link_text, confirmation_text, path, args = {}) | ||||
| 		@confirmation_dlg ||= true | ||||
| 		args[:data] ||= {} | ||||
| 		args[:data][:confirmation] = CGI::escapeHTML(confirmation_text) | ||||
| 		link_to link_text, path, args | ||||
| 		args[:data][:confirmation] = true | ||||
| 		link_to path, args do | ||||
| 			(link_text.to_s + content_tag(:template, confirmation_text, class: 'message')).html_safe | ||||
| 		end | ||||
| 	end | ||||
| 
 | ||||
| 	def link_info_dlg(link_text, info_text, info_title, args = {}) | ||||
| 		@info_dlg ||= true | ||||
| 		args[:data] ||= {} | ||||
| 		args[:data]['info-title'] = info_title | ||||
| 		args[:data]['info-text'] = true | ||||
| 		content_tag(:a, args) do | ||||
| 			(link_text.to_s + content_tag(:template, info_text, class: 'message')).html_safe | ||||
| 		end | ||||
| 	end | ||||
| 
 | ||||
| 	def button_with_confirmation(button_name, confirmation_text, args = {}) | ||||
| 		@confirmation_dlg ||= true | ||||
| 		args[:data] ||= {} | ||||
| 		args[:data][:confirmation] = CGI::escapeHTML(confirmation_text) | ||||
| 		button_tag button_name, args | ||||
| 		args[:data][:confirmation] = true | ||||
| 		button_tag args do | ||||
| 			(button_name.to_s + content_tag(:template, confirmation_text, class: 'message')).html_safe | ||||
| 		end | ||||
| 	end | ||||
| 
 | ||||
| 	def richtext(text, reduce_headings = 2) | ||||
| @ -924,6 +952,10 @@ module ApplicationHelper | ||||
| 			html_safe | ||||
| 	end | ||||
| 
 | ||||
| 	def truncate(text) | ||||
| 		strip_tags(text.gsub('>', '> ')).gsub(/^(.{40,60})\s.*$/m, '\1…').html_safe | ||||
| 	end | ||||
| 
 | ||||
| 	def textarea(name, value, options = {}) | ||||
| 		id = name.to_s.gsub('[', '_').gsub(']', '') | ||||
| 		label_id = "#{id}-label" | ||||
| @ -1003,6 +1035,10 @@ module ApplicationHelper | ||||
| 		textfield(name, value, options.merge({type: :number})) | ||||
| 	end | ||||
| 
 | ||||
| 	def emailfield(name, value, options = {}) | ||||
| 		textfield(name, value, options.merge({type: :email})) | ||||
| 	end | ||||
| 
 | ||||
| 	def textfield(name, value, options = {}) | ||||
| 		html = '' | ||||
| 		id = name.to_s.gsub('[', '_').gsub(']', '') | ||||
|  | ||||
| @ -36,4 +36,18 @@ class User < ActiveRecord::Base | ||||
| 		return "#{name} <#{email}>" | ||||
| 	end | ||||
| 
 | ||||
| 	def self.get(email) | ||||
| 		user = where(email: email).first | ||||
| 
 | ||||
| 		unless user | ||||
| 			# not really a good UX so we should fix this later | ||||
| 			#do_404 | ||||
| 			#return | ||||
| 			user = new(email: email) | ||||
| 			user.save! | ||||
| 		end | ||||
| 
 | ||||
| 		return user | ||||
| 	end | ||||
| 
 | ||||
| end | ||||
|  | ||||
| @ -2,6 +2,7 @@ class Workshop < ActiveRecord::Base | ||||
|     translates :info, :title | ||||
| 
 | ||||
|     belongs_to :conference | ||||
|     belongs_to :event_location | ||||
| 
 | ||||
|     has_many :workshop_facilitators, :dependent => :destroy | ||||
|     has_many :users, :through => :workshop_facilitators | ||||
| @ -83,14 +84,19 @@ class Workshop < ActiveRecord::Base | ||||
|     end | ||||
| 
 | ||||
|     def interested_count | ||||
|         return 0 unless id | ||||
|         interested.size | ||||
|     end | ||||
| 
 | ||||
|     def interested | ||||
|         return [] unless id | ||||
|         return @interested if @interested.present? | ||||
| 
 | ||||
|         collaborators = [] | ||||
|         workshop_facilitators.each do |f| | ||||
|             collaborators << f.user_id unless f.role.to_sym == :requested || f.user_id.nil? | ||||
|         end | ||||
|         return 10 unless collaborators.present? | ||||
|         interested = WorkshopInterest.where("workshop_id=#{id} AND user_id NOT IN (#{collaborators.join ','})") || [] | ||||
|         interested ? interested.size : 0 | ||||
|         @interested = WorkshopInterest.where("workshop_id=#{id} AND user_id NOT IN (#{collaborators.join ','})") || [] | ||||
|     end | ||||
| 
 | ||||
|     def can_translate?(user, lang) | ||||
|  | ||||
| @ -5,3 +5,15 @@ | ||||
| 			= textarea "info_translations[#{locale.to_s}]", @this_conference._info(locale), label: 'translate.pages.Locale_Translation', vars: { language: _("languages.#{locale}") }, lang: locale, edit_on: :focus | ||||
| 	.actions.right | ||||
| 		= button_tag :save, value: :save | ||||
| %h4=_'articles.admin.edit.headings.host_organizations' | ||||
| - @this_conference.organizations.each do | organization | | ||||
| 	%p=organization.name | ||||
| 	%h5=_'articles.admin.edit.headings.members' | ||||
| 	.details.org-members.inline | ||||
| 		- organization.users.each do | user | | ||||
| 			= raw_data_set(:h6, user.name) do | ||||
| 				= user.email | ||||
| 	= form_tag administration_update_path(@this_conference.slug, :edit), class: 'mini-flex-form' do | ||||
| 		= hidden_field_tag :org_id, organization.id | ||||
| 		= emailfield :email, nil, required: true | ||||
| 		= button_tag :add_member, value: :add_member, class: :small | ||||
|  | ||||
| @ -19,7 +19,7 @@ | ||||
| 			- if registration.user.present? | ||||
| 				%li.guest{id: "guest-#{id}", data: { id: id, 'affected-hosts': @hosts_affected_by_guests[id].join(',') }} | ||||
| 					%h4= registration.user.name | ||||
| 					.city.on-top-only=registration.city | ||||
| 					.city=registration.city | ||||
| 					.email.on-top-only=registration.user.email | ||||
| 					= button_tag :set_host, type: :button, class: [:small, 'set-host', 'not-on-top'] | ||||
| 
 | ||||
|  | ||||
| @ -1,18 +1,98 @@ | ||||
| .table.workshop-blocks | ||||
| 	.table-tr.header | ||||
| 		.table-th=_'forms.labels.generic.block_number' | ||||
| 		.table-th=_'forms.labels.generic.time' | ||||
| 		.table-th=_'forms.labels.generic.length' | ||||
| 		.table-th=_'forms.labels.generic.days' | ||||
| 		.table-th.form | ||||
| 	- @workshop_blocks.each_with_index do | info, block | | ||||
| 		- is_new = info['time'].blank? | ||||
| 		= form_tag administration_update_path(@this_conference.slug, :schedule), class: ['table-tr', is_new ? 'new' : 'saved'] do | ||||
| 			.table-th.center.big= is_new ? '' : (block + 1) | ||||
| 			.table-td=time_select info['time'], small: true, label: false | ||||
| 			.table-td=length_select info['length'], {small: true, label: false}, 0.5, 2 | ||||
| 			.table-td=checkboxes :days, @block_days, info['days'].map(&:to_i), 'date.day_names', vertical: true, small: true | ||||
| 			.table-td.form | ||||
| 				= hidden_field_tag :workshop_block, block | ||||
| 				= button_tag :delete_block, value: :delete_block, class: [:small, :delete] if block == @workshop_blocks.length - 2 | ||||
| 				= button_tag (is_new ? :add_block : :update_block), value: :save_block, class: [:small, :add] | ||||
| #schedule-preview | ||||
| 	- @schedule.each do | day, data | | ||||
| 		%h4=date(day, :weekday) | ||||
| 		%table.schedule{class: data[:locations].present? ? 'has-locations' : 'no-locations'} | ||||
| 			- if data[:locations].present? | ||||
| 				%thead | ||||
| 					%tr | ||||
| 						%th.corner | ||||
| 						- data[:locations].each do | id, location | | ||||
| 							%th=location.title | ||||
| 						%th.status | ||||
| 			%tbody | ||||
| 				- data[:times].each do | time, time_data | | ||||
| 					%tr | ||||
| 						- rowspan = (time_data[:length] * 2).to_i | ||||
| 						%th=time(time) | ||||
| 						- if time_data[:type] == :workshop | ||||
| 							- if time_data[:item][:workshops].present? | ||||
| 								- data[:locations].each do | id, location | | ||||
| 									- if time_data[:item][:workshops][id].present? | ||||
| 										- workshop = time_data[:item][:workshops][id][:workshop] | ||||
| 										- status = time_data[:item][:workshops][id][:status] | ||||
| 									- else | ||||
| 										- workshop = status = nil | ||||
| 									%td{class: [time_data[:type], workshop.present? ? :filled : nil], rowspan: rowspan} | ||||
| 										- if workshop.present? | ||||
| 											= form_tag administration_update_path(@this_conference.slug, :schedule), class: 'js-xhr' do | ||||
| 												.title=workshop.title | ||||
| 												.status | ||||
| 													.conflict-score | ||||
| 														%span.title Conflict Score:  | ||||
| 														%span.value="#{status[:conflict_score] * 100.0}%" | ||||
| 													- if status[:errors].present? | ||||
| 														.errors | ||||
| 															- status[:errors].each do | error | | ||||
| 																.error=_"errors.messages.schedule.#{error[:name].to_s}", vars: error[:i18nVars] | ||||
| 												= hidden_field_tag :id, workshop.id | ||||
| 												= button_tag :deschedule, value: :deschedule_workshop, class: [:delete, :small] | ||||
| 										- else | ||||
| 											.title="Block #{time_data[:item][:block] + 1}" | ||||
| 							- else | ||||
| 								%td{class: time_data[:type], rowspan: rowspan, colspan: data[:locations].present? ? data[:locations].size : 1} | ||||
| 									.title="Block #{time_data[:item][:block] + 1}" | ||||
| 							%td.status{rowspan: rowspan} | ||||
| 								- if time_data[:status].present? && time_data[:status][:errors].present? | ||||
| 									%ul.errors | ||||
| 										- time_data[:status][:errors].each do | error | | ||||
| 											%li=error.to_json.to_s | ||||
| 						- elsif time_data[:type] != :nil | ||||
| 							%td{class: time_data[:type], rowspan: rowspan, colspan: data[:locations].present? ? data[:locations].size : 1} | ||||
| 								- case time_data[:type] | ||||
| 									- when :meal | ||||
| 										.title= time_data[:item]['title'] | ||||
| 										.location= EventLocation.find(time_data[:item]['location'].to_i).title | ||||
| 									- when :event | ||||
| 										.title= time_data[:item].title | ||||
| 										.location= time_data[:item].event_location.title | ||||
| 							%td.status{rowspan: rowspan} | ||||
| - unless request.xhr? | ||||
| 	%ul.workshop-list | ||||
| 		- @workshops.each do | workshop | | ||||
| 			%li{id: "workshop-#{workshop.id}", class: workshop.block.present? ? 'booked' : 'not-booked'} | ||||
| 				%h4.title= workshop.title | ||||
| 				= form_tag administration_update_path(@this_conference.slug, :schedule), class: 'js-xhr' do | ||||
| 					.already-booked | ||||
| 						.field-error='This block is already booked' | ||||
| 					.workshop-description | ||||
| 						.details | ||||
| 							= data_set(:h5, 'articles.workshops.headings.interested_count') do | ||||
| 								= workshop.interested_count | ||||
| 							= data_set(:h5, 'articles.workshops.headings.facilitators') do | ||||
| 								- facilitators = [] | ||||
| 								- workshop.active_facilitators.each do | facilitator | | ||||
| 									- facilitators << facilitator.name | ||||
| 								= facilitators.join ', ' | ||||
| 							= data_set(:h5, 'articles.workshops.headings.needs') do | ||||
| 								- needs = [] | ||||
| 								- JSON.parse(workshop.needs || '[]').each do | need | | ||||
| 									- needs << (_"workshop.options.needs.#{need}") | ||||
| 								= _!(needs.join ', ') | ||||
| 							= data_set(:h5, 'articles.workshops.headings.theme') do | ||||
| 								= workshop.theme.present? ? (_"workshop.options.theme.#{workshop.theme}") : '' | ||||
| 							= data_set(:h5, 'articles.workshops.headings.space') do | ||||
| 								= workshop.space.present? ? (_"workshop.options.space.#{workshop.space}") : '' | ||||
| 							= data_set(:h5, 'forms.labels.generic.info') do | ||||
| 								= link_info_dlg truncate(workshop.info), (richtext workshop.info.html_safe), workshop.title | ||||
| 							- if workshop.notes.present? && strip_tags(workshop.notes).length > 0 | ||||
| 								= data_set(:h5, 'forms.labels.generic.notes') do | ||||
| 									= link_info_dlg truncate(workshop.notes), (richtext workshop.notes.html_safe), workshop.title | ||||
| 
 | ||||
| 						= hidden_field_tag :id, workshop.id | ||||
| 						.flex-inputs | ||||
| 							= location_select workshop.event_location_id, small: true, stretch: true | ||||
| 							= block_select workshop.block.present? ? "#{workshop.block['day']}:#{workshop.block['block']}" : nil, small: true | ||||
| 						.actions.next-prev | ||||
| 							= button_tag :deschedule, value: :deschedule_workshop, class: [:delete, 'booked-only'] | ||||
| 							= button_tag :reschedule, value: :schedule_workshop, class: [:secondary, 'booked-only'] | ||||
| 							= button_tag :schedule_workshop, value: :schedule_workshop, class: 'not-booked-only' | ||||
|  | ||||
| @ -0,0 +1,5 @@ | ||||
| .details | ||||
| 	= data_set(:h4, 'articles.admin.stats.headings.registrations') do | ||||
| 		= @registrations.size | ||||
| .actions | ||||
| 	= link_to (_'links.download.Excel'), administration_step_path(@this_conference.slug, :stats, :format => :xlsx), class: [:button, :download] | ||||
							
								
								
									
										18
									
								
								app/views/conferences/admin/_workshop_times.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/views/conferences/admin/_workshop_times.html.haml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| .table.workshop-blocks | ||||
| 	.table-tr.header | ||||
| 		.table-th=_'forms.labels.generic.block_number' | ||||
| 		.table-th=_'forms.labels.generic.time' | ||||
| 		.table-th=_'forms.labels.generic.length' | ||||
| 		.table-th=_'forms.labels.generic.days' | ||||
| 		.table-th.form | ||||
| 	- @workshop_blocks.each_with_index do | info, block | | ||||
| 		- is_new = info['time'].blank? | ||||
| 		= form_tag administration_update_path(@this_conference.slug, :workshop_times), class: ['table-tr', is_new ? 'new' : 'saved'] do | ||||
| 			.table-th.center.big= is_new ? '' : (block + 1) | ||||
| 			.table-td=time_select info['time'], small: true, label: false | ||||
| 			.table-td=length_select info['length'], {small: true, label: false}, 0.5, 2 | ||||
| 			.table-td=checkboxes :days, @block_days, info['days'].map(&:to_i), 'date.day_names', vertical: true, small: true | ||||
| 			.table-td.form | ||||
| 				= hidden_field_tag :workshop_block, block | ||||
| 				= button_tag :delete_block, value: :delete_block, class: [:small, :delete] if block == @workshop_blocks.length - 2 | ||||
| 				= button_tag (is_new ? :add_block : :update_block), value: :save_block, class: [:small, :add] | ||||
							
								
								
									
										18
									
								
								app/views/conferences/stats.xlsx.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/views/conferences/stats.xlsx.haml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| - key = @excel_data[:key] || 'excel.columns' | ||||
| %table | ||||
|   %thead | ||||
|     %tr | ||||
|       - @excel_data[:columns].each do |column| | ||||
|         %th=_(@excel_data[:keys][column].present? ? @excel_data[:keys][column] : "#{key}.#{column.to_s}") | ||||
|   %tbody | ||||
|     - @excel_data[:data].each do |row| | ||||
|       %tr | ||||
|         - @excel_data[:columns].each do |column| | ||||
|           %td{class: @excel_data[:column_types][column].present? ? @excel_data[:column_types][column].to_s : nil}=_!row[column] | ||||
| 
 | ||||
| - format_xls 'table' do | ||||
|   - workbook use_autowidth: true | ||||
|   - format bg_color: '333333' | ||||
|   - format 'td', font_name: 'Calibri', bg_color: 'ffffff', fg_color: '333333' | ||||
|   - format 'th', font_name: 'Calibri', b: true, bg_color: '333333', fg_color: 'ffffff' | ||||
|   - format 'td.date', num_fmt: 22, font_name: 'Courier New', sz: 10 | ||||
| @ -2,8 +2,7 @@ | ||||
| %html{ lang: I18n.locale.to_s } | ||||
| 	%head | ||||
| 		%meta{ charset: 'utf-8' } | ||||
| 		%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0' } | ||||
| 		-#%title= (yield :title) + (content_for?(:title) ? (_!' | ') : '') + (_!'Bike!Bike!') | ||||
| 		%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' } | ||||
| 		- title = yield :title | ||||
| 		%title=_!('Bike!Bike!' + (content_for?(:title) ? " - #{title}" : '')) | ||||
| 		%meta{ name: 'description', content: (yield_or_default :description, I18n.t('page_descriptions.home')) } | ||||
| @ -14,13 +13,11 @@ | ||||
| 		- @alt_lang_urls.each do |locale, url| | ||||
| 			%link{ rel: :alternate, hreflang: locale, href: url } | ||||
| 		- if content_for?(:og_image) | ||||
| 			- og_image = yield :og_image | ||||
| 			- og_image = request.base_url + og_image | ||||
| 			%meta{property: 'og:title', content: title} | ||||
| 			%meta{property: 'og:type', content: "website"} | ||||
| 			%meta{property: 'og:image', content: (yield :og_image)} | ||||
| 		-#%link{ href: asset_path('apple-touch-icon.png'), rel: 'apple-touch-icon' } | ||||
| 		-#%link{ href: asset_path('apple-touch-icon-72x72.png'), rel: 'apple-touch-icon', sizes: '72x72' } | ||||
| 		-#%link{ href: asset_path('apple-touch-icon-114x114.png'), rel: 'apple-touch-icon', sizes: '114x114' } | ||||
| 		-#%link{ href: asset_path('apple-touch-icon-144x144.png'), rel: 'apple-touch-icon', sizes: '144x144' } | ||||
| 			%meta{property: 'og:type', content: 'website'} | ||||
| 			%meta{property: 'og:image', content: og_image} | ||||
| 		= yield :head | ||||
| 
 | ||||
| 	%body{ class: page_style } | ||||
| @ -45,16 +42,24 @@ | ||||
| 					= yield | ||||
| 			#footer | ||||
| 				%footer= render 'shared/footer' | ||||
| 		- if @confirmation_dlg.present?	 | ||||
| 		- if @confirmation_dlg.present?	|| @info_dlg.present? | ||||
| 			#content-overlay | ||||
| 				#overlay | ||||
| 				.dlg#confirmation-dlg | ||||
| 					.dlg-content | ||||
| 						%h2.title=_'modals.confirm' | ||||
| 						.dlg-inner | ||||
| 							%p.message='' | ||||
| 							%a.button.confirm=_'modals.yes_button' | ||||
| 							%button.delete=_'modals.no_button' | ||||
| 				- if @confirmation_dlg.present? | ||||
| 					.dlg#confirmation-dlg | ||||
| 						.dlg-content | ||||
| 							%h2.title=_'modals.confirm' | ||||
| 							.dlg-inner | ||||
| 								%p.message='' | ||||
| 								%a.button.confirm=_'modals.yes_button' | ||||
| 								%button.delete.close=_'modals.no_button' | ||||
| 				- if @info_dlg.present? | ||||
| 					.dlg#info-dlg | ||||
| 						.dlg-content | ||||
| 							%h2.title=_'modals.info' | ||||
| 							.dlg-inner | ||||
| 								%p.message='' | ||||
| 								%button.close=_'modals.done_button' | ||||
| 		 | ||||
| 		= yield :footer_scripts if content_for?(:footer_scripts) | ||||
| 		= inline_scripts | ||||
|  | ||||
| @ -58,5 +58,5 @@ BikeBike::Application.configure do | ||||
| 	# to deliver to the browser instead of email | ||||
| 	config.action_mailer.delivery_method = :letter_opener | ||||
| 
 | ||||
| 	Paypal.sandbox! | ||||
| 	# Paypal.sandbox! | ||||
| end | ||||
|  | ||||
| @ -5533,6 +5533,7 @@ en: | ||||
|         needs: Needs | ||||
|         space: Space | ||||
|         theme: Theme | ||||
|         interested_count: Interested | ||||
|         Delete_Workshop: Delete Workshop | ||||
|         notes: Notes | ||||
|         Workshops: Workshops | ||||
|  | ||||
							
								
								
									
										5
									
								
								db/migrate/20160703044620_add_block_to_workshops.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20160703044620_add_block_to_workshops.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| class AddBlockToWorkshops < ActiveRecord::Migration | ||||
|   def change | ||||
|     add_column :workshops, :block, :json | ||||
|   end | ||||
| end | ||||
| @ -11,7 +11,7 @@ | ||||
| # | ||||
| # It's strongly recommended that you check this file into your version control system. | ||||
| 
 | ||||
| ActiveRecord::Schema.define(version: 20160630233219) do | ||||
| ActiveRecord::Schema.define(version: 20160703044620) do | ||||
| 
 | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "plpgsql" | ||||
| @ -415,6 +415,7 @@ ActiveRecord::Schema.define(version: 20160630233219) do | ||||
|     t.string   "locale" | ||||
|     t.integer  "event_location_id" | ||||
|     t.boolean  "needs_facilitators" | ||||
|     t.json     "block" | ||||
|   end | ||||
| 
 | ||||
| end | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user