From 908f0b3ade9fc1991cf0fe5d8e67cac8f56f78c7 Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Thu, 2 Nov 2017 16:59:03 +0100 Subject: [PATCH 01/14] Initialize `package.json` with `yarn` --- package.json | 17 +++++++++++++++++ yarn.lock | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 package.json create mode 100644 yarn.lock diff --git a/package.json b/package.json new file mode 100644 index 0000000..9e7859e --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "django-clock", + "version": "2.0.0", + "main": "index.js", + "repository": "git@github.com:mimischi/django-clock", + "author": "Michael Gecht ", + "license": "MIT", + "dependencies": { + "bootstrap": "^3.3.7", + "bootstrap-select": "^1.12.4", + "datatables": "^1.10.13", + "eonasdan-bootstrap-datetimepicker": "^4.17.47", + "font-awesome": "^4.7.0", + "jquery": "^3.2.1", + "moment": "^2.19.1" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..8a80bfb --- /dev/null +++ b/yarn.lock @@ -0,0 +1,46 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +bootstrap-select@^1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/bootstrap-select/-/bootstrap-select-1.12.4.tgz#7f15d3c0ce978868d9c09c70f96624f55fa02ee1" + dependencies: + jquery ">=1.8" + +bootstrap@^3.3, bootstrap@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71" + +datatables@^1.10.13: + version "1.10.13" + resolved "https://registry.yarnpkg.com/datatables/-/datatables-1.10.13.tgz#9bb2dec6f7dcf02049a00e4f0e7d3fe009c39346" + dependencies: + jquery ">=1.7" + +eonasdan-bootstrap-datetimepicker@^4.17.47: + version "4.17.47" + resolved "https://registry.yarnpkg.com/eonasdan-bootstrap-datetimepicker/-/eonasdan-bootstrap-datetimepicker-4.17.47.tgz#7a49970044065276e7965efd16f822735219e735" + dependencies: + bootstrap "^3.3" + jquery "^1.8.3 || ^2.0 || ^3.0" + moment "^2.10" + moment-timezone "^0.4.0" + +font-awesome@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" + +jquery@>=1.7, jquery@>=1.8, "jquery@^1.8.3 || ^2.0 || ^3.0", jquery@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" + +moment-timezone@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.4.1.tgz#81f598c3ad5e22cdad796b67ecd8d88d0f5baa06" + dependencies: + moment ">= 2.6.0" + +"moment@>= 2.6.0", moment@^2.10, moment@^2.19.1: + version "2.19.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167" From f525953e67f6d57b832d7e8b9223b16bb9acbc50 Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Thu, 2 Nov 2017 20:12:32 +0100 Subject: [PATCH 02/14] =?UTF-8?q?Add=20webpack!=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move to a functional local development with `webpack`. --- Pipfile | 1 + Pipfile.lock | 9 +- assets/css/clock.css | 449 +++ assets/images/clock.svg | 18 + assets/js/clock.js | 7 + assets/js/index.js | 18 + assets/js/tageditor/css/jquery.tag-editor.css | 159 + assets/js/tageditor/js/jquery.caret.min.js | 2 + .../js/tageditor/js/jquery.tag-editor.min.js | 3 + clock/templates/base.html | 39 +- clock/templates/contract/list.html | 4 + clock/templates/pages/backend/index.html | 4 +- clock/templates/shift/edit.html | 2 + clock/templates/shift/month_archive_view.html | 4 + config/settings/common.py | 44 +- config/settings/local.py | 4 +- package.json | 28 +- webpack.common.js | 61 + webpack.dev.js | 9 + webpack.prod.js | 28 + yarn.lock | 3103 ++++++++++++++++- 21 files changed, 3934 insertions(+), 62 deletions(-) create mode 100755 assets/css/clock.css create mode 100755 assets/images/clock.svg create mode 100755 assets/js/clock.js create mode 100644 assets/js/index.js create mode 100755 assets/js/tageditor/css/jquery.tag-editor.css create mode 100755 assets/js/tageditor/js/jquery.caret.min.js create mode 100755 assets/js/tageditor/js/jquery.tag-editor.min.js create mode 100644 webpack.common.js create mode 100644 webpack.dev.js create mode 100644 webpack.prod.js diff --git a/Pipfile b/Pipfile index 7f3e307..7fa121e 100644 --- a/Pipfile +++ b/Pipfile @@ -26,6 +26,7 @@ raven = "*" gunicorn = "*" gevent = "*" django-anymail = "*" +django-webpack-loader = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 4fd7559..2bd4a14 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a81da1d3cf587acfe5328b38133084194dc25aabd14d7d6dbc20f03ac4471c65" + "sha256": "7faf04ffac0162e8f7c70603054b35fe598084e43fdc07876dccabd69b4eeb5a" }, "host-environment-markers": { "implementation_name": "cpython", @@ -111,6 +111,13 @@ ], "version": "==0.22.1" }, + "django-webpack-loader": { + "hashes": [ + "sha256:0a8536e36a30d719018cd4c5da6e9d2377771134e713c14e617bb484b4f0acce", + "sha256:7094bcd8cc40c9824e5b482ce8ff9e8ae09e1982e5d08e078e5fe2411fb40a03" + ], + "version": "==0.5.0" + }, "gevent": { "hashes": [ "sha256:9b492bb1a043540abb6e54fdb5537531e24962ca49c09f3b47dc4f9c37f6297c", diff --git a/assets/css/clock.css b/assets/css/clock.css new file mode 100755 index 0000000..c042299 --- /dev/null +++ b/assets/css/clock.css @@ -0,0 +1,449 @@ +/* General design +------------------------- */ + +html, body { + height: 100%; + /* The html and body elements cannot have any padding or margin. */ + font-size: 14px; + font: "Helvetica Neue", Verdana, Arial, Helvetica, sans-serif !important; + color: #333; + + padding: 0; +} + +header { + padding: 20px 0 0; + margin-bottom: 0; +} + +/* Wrapper for page content to push down footer */ +#wrap { + min-height: 100%; + height: auto !important; + height: 100%; + /* Negative indent footer by it's height */ + margin: 0 auto -80px; + /* Pad bottom by footer height */ + padding: 0 0 110px; +} + +.container > .row h2 { + margin-top: 0; + margin-bottom: 20px; +} + +/* Set the fixed height of the footer here */ +#footer { + background-color: #f5f5f5; + height: 80px; + vertical-align: middle; +} + +/* Vertically center footer content */ + +#footer > .container { + padding-top: 30px; +} + +.error-page { + margin-top: 30px; +} + +.error-page img { + height: 150px; +} + +.btn-app { + border-radius: 3px; + position: relative; + padding: 15px 5px; + margin: 0 0 10px 10px; + min-width: 80px; + height: 60px; + text-align: center; + color: #666; + border: 1px solid #ddd; + background-color: #f4f4f4; + font-size: 12px; +} + +.btn-app .fa { + font-size: 20px; + display: block; +} + +.verticaltext { + transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + + top: 5px; + left: 100px; + position: absolute; + color: #ed217c; +} + +/* Navbar +------------------------- */ + +/* Adjust top margin of navbar on every other page than the landing page */ + +body > header > .navbar { + margin: 20px 0; +} + +.navbar { + font-size: 16px; +} + +.navbar .btn { + margin: 5px 10px 0 10px; + padding: 10px; + height: 40px; +} + +/* Set the height to 100%, so we can use an SVG logo! */ +.navbar-brand img { + height: 100%; +} + +/* Fancy menu hamburger animation. Adapted from: http://julienmelissas.com/animated-x-icon-for-the-bootstrap-navbar-toggle/ +------------------------- */ + +.navbar-toggle { + border: none; + background: transparent !important; +} + +.navbar-toggle:hover { + background: transparent !important; +} + +.navbar-toggle .icon-bar { + width: 22px; + transition: all 0.2s; +} + +.navbar-toggle .top-bar { + -ms-transform: rorate(45deg); + -ms-transform-origin: 10% 10%; + -webkit-transform: rotate(45deg); + -webkit-transform-origin: 10% 10%; + transform: rotate(45deg); + transform-origin: 10% 10%; +} + +.navbar-toggle .middle-bar { + opacity: 0; +} + +.navbar-toggle .bottom-bar { + -ms-transform: rorate(-45deg); + -ms-transform-origin: 10% 90%; + -webkit-transform: rotate(-45deg); + -webkit-transform-origin: 10% 90%; + transform: rotate(-45deg); + transform-origin: 10% 90%; +} + +.navbar-toggle.collapsed .top-bar { + -ms-transform: rorate(0); + -webkit-transform: rotate(0); + transform: rotate(0); +} + +.navbar-toggle.collapsed .middle-bar { + opacity: 1; +} + +.navbar-toggle.collapsed .bottom-bar { + -ms-transform: rorate(0); + -webkit-transform: rotate(0); + transform: rotate(0); +} + +/* Eyecatcher +------------------------- */ + +.eyecatcher { + position: relative; + overflow: auto; + /* background-image: -webkit-linear-gradient(left, rgb(228, 227, 221) 25%, rgb(0, 97, 143) 100%); */ + /* background-image: linear-gradient(to right, rgb(228, 227, 221) 25%, rgb(0, 97, 143) 100%); */ + text-align: center; +} + +.eyecatcher p { + font-size: 20px; + font-weight: 200; + margin-bottom: 30px; +} + +.eyecatcher h1 { + font-size: 26px; + margin-bottom: 20px; +} + +.eyecatcher .screen { + height: 100px; + background: url('../images/clock.svg') no-repeat; + background-size: 100% 85%; + position: relative; + /*margin-top: 50px;*/ +} + +/* Featurettes +------------------------- */ + +.featurette-divider { + margin: 30px 0; + /* Space out the Bootstrap
more */ +} + +/* Let's create some spacing between the first hidden divider and the text*/ + +.featurette-divider:first-of-type { + margin: 0 0 10px; +} + +/* +HEX HEX - ABRAKADABRA - !! +This condition looks for every h2.featurette-heading that is NOT in the first +occurence of div .row .featurette and removes their top margin! +*/ + +div.container > div.row.featurette:not(:first-of-type) > div.col-md-7 > h2.featurette-heading { + margin-top: 0; +} + +/* Thin out the marketing headings */ + +.featurette-heading { + font-weight: 300; + line-height: 1; + letter-spacing: -1px; + font-size: 25px; + margin-bottom: 20px; +} + +div.row.featurette p.lead { + font-size: 18px; +} + +div.row.featurette img { + border: 1px solid lightgrey; + -webkit-border-radius: 1em; + -moz-border-radius: 1em; + border-radius: 1em; +} + +/* Login & Signup page +-------------------------------------------------- */ + +.form-halfpage { + max-width: 400px; +} + +.form-halfpage .form-halfpage-heading, .form-halfpage .checkbox { + margin-bottom: 30px; +} + +.form-halfpage .checkbox { + font-weight: normal; +} + +.form-halfpage .form-control { + position: relative; + height: auto; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 10px; + font-size: 16px; +} + +.form-halfpage .form-control:focus { + z-index: 2; +} + +.form-halfpage input[type="text"] { + margin-bottom: -1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.form-halfpage input[type="email"] { + margin-bottom: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.form-halfpage input[type="password"] { + margin-bottom: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.form-halfpage input[type="password"]:last-of-type { + margin-bottom: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.form-halfpage > p.pull-right { + margin-top: 5px; +} + +.registrationInfo { + margin-top: 30px; +} + +.registrationInfo h4 { + font-weight: 600; +} + +.below-form { + padding-top: 20px; +} + +/* Account settings +-------------------------------------------------- */ + +#add-mail { + margin-top: 20px; +} + +#change-mail { + +} + +/* DataTables modifications +-------------------------------------------------- */ + +#clockTable > tbody > tr { + cursor: pointer; + text-align: center; +} + +#clockTable > tbody > tr:active, +#clockTable > tbody > tr:focus, +#clockTable > tbody > tr:hover { + background-color: rgb(17, 146, 201); + color: white; +} + +/* Only change color of the plus/minus sign, if the parent tr is focussed/active/hovered over! */ +#clockTable > tbody > tr:active td.control:before, +#clockTable > tbody > tr:focus td.control:before, +#clockTable > tbody > tr:hover td.control:before { + color: rgb(255, 255, 255); +} + +#clockTable > tbody td { + vertical-align: middle; +} + +#clockTable > tbody td.select-checkbox { + /*margin-left: 5px;*/ + /*margin-right: 5px;*/ + width: 30px; +} + +tr.child > td.child > ul > li { + color: rgb(0, 0, 0); + text-align: left; +} + +@media screen and (max-width: 640px) { + div.dt-buttons { + text-align: inherit; + } + +} + +@media screen and (max-width: 767px) { + div.dataTables_wrapper div.dataTables_filter { + text-align: right; + } +} + +@media screen and (max-width: 991px) { + /* Align the pseudo-element checkbox to be in the middle of the cell! */ + #clockTable > tbody td::before { + margin-top: 5px; + } +} + +/* Shift Datepicker +-------------------------------------------------- */ + +.objectList-topbar .form-group { + margin-bottom: 0; +} + +/* Add margin to the bottom of the top menu, except for the last item! */ +.objectList-topbar .col-sm-12:not(:last-of-type) { + margin-bottom: 10px; +} + +.second-button { + margin: 0 10px; +} + +/* RESPONSIVE CSS +-------------------------------------------------- */ + +@media (min-width: 768px) { + /* Lastly, apply responsive CSS fixes as necessary */ + #footer { + padding-left: 20px; + padding-right: 20px; + } + + .eyecatcher h1 { + font-size: 38px; + } + + .eyecatcher p { + font-size: 22px; + } + + .eyecatcher .screen { + height: 300px; + margin-top: 30px; + } + + .eyecatcher .btn { + margin-right: 0; + } + + .featurette-heading { + font-size: 40px; + } + + .registrationInfo { + margin-top: 20px; + } +} + +@media (min-width: 992px) { + /* Some spacing between first featurette and the top! */ + div.row.featurette:first-of-type { + margin-top: 30px; + } + + div.container > div.row.featurette:not(:first-of-type) > div.col-md-7 > h2.featurette-heading { + margin-top: 120px; + } + + /* Get rid of the ugly bottom margin */ + div.row.featurette > div > p.lead { + margin-bottom: 0; + } +} diff --git a/assets/images/clock.svg b/assets/images/clock.svg new file mode 100755 index 0000000..90c49a8 --- /dev/null +++ b/assets/images/clock.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/assets/js/clock.js b/assets/js/clock.js new file mode 100755 index 0000000..3b77d3b --- /dev/null +++ b/assets/js/clock.js @@ -0,0 +1,7 @@ +$(function() { +// Taken from pinax/js/theme.js + $("#account_logout").click(function(e) { + e.preventDefault(); + $("#accountLogOutForm").submit(); + }); +}); diff --git a/assets/js/index.js b/assets/js/index.js new file mode 100644 index 0000000..c91e3b0 --- /dev/null +++ b/assets/js/index.js @@ -0,0 +1,18 @@ +import "expose-loader?$!jquery"; +import "expose-loader?moment!moment"; +import "bootstrap/dist/css/bootstrap.css"; +import "font-awesome/css/font-awesome.css"; +import "bootstrap"; +import "bootstrap-select/js/bootstrap-select"; +import "bootstrap-select/dist/css/bootstrap-select.css" +import "datetimepicker"; +import "eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.css"; +import "datatables.net"; +import dt from "datatables.net-bs"; +import 'datatables.net-bs/css/dataTables.bootstrap.css'; +import "holderjs"; +import "./tageditor/js/jquery.tag-editor.min.js"; +import "./tageditor/css/jquery.tag-editor.css"; + +import "../css/clock.css"; +import "./clock.js"; diff --git a/assets/js/tageditor/css/jquery.tag-editor.css b/assets/js/tageditor/css/jquery.tag-editor.css new file mode 100755 index 0000000..ad4ec47 --- /dev/null +++ b/assets/js/tageditor/css/jquery.tag-editor.css @@ -0,0 +1,159 @@ +/* surrounding tag container */ +.tag-editor { + list-style-type: none; + padding: 6px 0; + margin: 0; + overflow: hidden; + border: 1px solid #ccc; + cursor: text; + font: normal 14px sans-serif; + color: #555; + background: #fff; + line-height: 20px; + border-radius: 4px; +} + +/* core styles usually need no change */ +.tag-editor li { + display: block; + float: left; + overflow: hidden; + margin: 3px 0; +} + +.tag-editor div { + float: left; + padding: 0 4px; +} + +.tag-editor .placeholder { + padding: 0 8px; + color: #bbb; +} + +.tag-editor .tag-editor-spacer { + padding: 0; + width: 8px; + overflow: hidden; + color: transparent; + background: none; +} + +.tag-editor input { + vertical-align: inherit; + border: 0; + outline: none; + padding: 0; + margin: 0; + cursor: text; + font-family: inherit; + font-weight: inherit; + font-size: inherit; + font-style: inherit; + box-shadow: none; + background: none; + color: #444; +} + +/* hide original input field or textarea visually to allow tab navigation */ +.tag-editor-hidden-src { + position: absolute !important; + left: -99999px; +} + +/* hide IE10 "clear field" X */ +.tag-editor ::-ms-clear { + display: none; +} + +/* tag style */ +.tag-editor .tag-editor-tag { + padding-left: 5px; + color: #46799b; + background: #e0eaf1; + white-space: nowrap; + overflow: hidden; + cursor: pointer; + border-radius: 2px 0 0 2px; +} + +/* delete icon */ +.tag-editor .tag-editor-delete { + background: #e0eaf1; + cursor: pointer; + border-radius: 0 2px 2px 0; + padding-left: 3px; + padding-right: 4px; +} + +.tag-editor .tag-editor-delete i { + line-height: 18px; + display: inline-block; +} + +.tag-editor .tag-editor-delete i:before { + font-size: 16px; + color: #8ba7ba; + content: "×"; + font-style: normal; +} + +.tag-editor .tag-editor-delete:hover i:before { + color: #d65454; +} + +.tag-editor .tag-editor-tag.active + .tag-editor-delete, .tag-editor .tag-editor-tag.active + .tag-editor-delete i { + visibility: hidden; + cursor: text; +} + +.tag-editor .tag-editor-tag.active { + background: none !important; +} + +/* jQuery UI autocomplete - code.jquery.com/ui/1.10.2/themes/smoothness/jquery-ui.css */ +.ui-autocomplete { + position: absolute; + top: 0; + left: 0; + cursor: default; + font-size: 14px; +} + +.ui-front { + z-index: 9999; +} + +.ui-menu { + list-style: none; + padding: 1px; + margin: 0; + display: block; + outline: none; +} + +.ui-menu .ui-menu-item a { + text-decoration: none; + display: block; + padding: 2px .4em; + line-height: 1.4; + min-height: 0; /* support: IE7 */ +} + +.ui-widget-content { + border: 1px solid #bbb; + background: #fff; + color: #555; +} + +.ui-widget-content a { + color: #46799b; +} + +.ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { + background: #e0eaf1; +} + +.ui-helper-hidden-accessible { + display: none; +} diff --git a/assets/js/tageditor/js/jquery.caret.min.js b/assets/js/tageditor/js/jquery.caret.min.js new file mode 100755 index 0000000..0f9ef48 --- /dev/null +++ b/assets/js/tageditor/js/jquery.caret.min.js @@ -0,0 +1,2 @@ +// http://code.accursoft.com/caret - 1.3.3 +!function(e){e.fn.caret=function(e){var t=this[0],n="true"===t.contentEditable;if(0==arguments.length){if(window.getSelection){if(n){t.focus();var o=window.getSelection().getRangeAt(0),r=o.cloneRange();return r.selectNodeContents(t),r.setEnd(o.endContainer,o.endOffset),r.toString().length}return t.selectionStart}if(document.selection){if(t.focus(),n){var o=document.selection.createRange(),r=document.body.createTextRange();return r.moveToElementText(t),r.setEndPoint("EndToEnd",o),r.text.length}var e=0,c=t.createTextRange(),r=document.selection.createRange().duplicate(),a=r.getBookmark();for(c.moveToBookmark(a);0!==c.moveStart("character",-1);)e++;return e}return t.selectionStart?t.selectionStart:0}if(-1==e&&(e=this[n?"text":"val"]().length),window.getSelection)n?(t.focus(),window.getSelection().collapse(t.firstChild,e)):t.setSelectionRange(e,e);else if(document.body.createTextRange)if(n){var c=document.body.createTextRange();c.moveToElementText(t),c.moveStart("character",e),c.collapse(!0),c.select()}else{var c=t.createTextRange();c.move("character",e),c.select()}return n||t.focus(),e}}(jQuery); diff --git a/assets/js/tageditor/js/jquery.tag-editor.min.js b/assets/js/tageditor/js/jquery.tag-editor.min.js new file mode 100755 index 0000000..1d083d2 --- /dev/null +++ b/assets/js/tageditor/js/jquery.tag-editor.min.js @@ -0,0 +1,3 @@ +// jQuery tagEditor v1.0.20 +// https://github.com/Pixabay/jQuery-tagEditor +!function(t){t.fn.tagEditorInput=function(){var e=" ",i=t(this),a=parseInt(i.css("fontSize")),r=t("").css({position:"absolute",top:-9999,left:-9999,width:"auto",fontSize:i.css("fontSize"),fontFamily:i.css("fontFamily"),fontWeight:i.css("fontWeight"),letterSpacing:i.css("letterSpacing"),whiteSpace:"nowrap"}),l=function(){if(e!==(e=i.val())){r.text(e);var t=r.width()+a;20>t&&(t=20),t!=i.width()&&i.width(t)}};return r.insertAfter(i),i.bind("keyup keydown focus",l)},t.fn.tagEditor=function(e,a,r){function l(t){return t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}var n,o=t.extend({},t.fn.tagEditor.defaults,e),c=this;if(o.dregex=new RegExp("["+o.delimiter.replace("-","-")+"]","g"),"string"==typeof e){var s=[];return c.each(function(){var i=t(this),l=i.data("options"),n=i.next(".tag-editor");if("getTags"==e)s.push({field:i[0],editor:n,tags:n.data("tags")});else if("addTag"==e){if(l.maxTags&&n.data("tags").length>=l.maxTags)return!1;t('
  •  '+l.delimiter[0]+'
  • ').appendTo(n).find(".tag-editor-tag").html('').addClass("active").find("input").val(a).blur(),r?t(".placeholder",n).remove():n.click()}else"removeTag"==e?(t(".tag-editor-tag",n).filter(function(){return t(this).text()==a}).closest("li").find(".tag-editor-delete").click(),r||n.click()):"destroy"==e&&i.removeClass("tag-editor-hidden-src").removeData("options").off("focus.tag-editor").next(".tag-editor").remove()}),"getTags"==e?s:this}return window.getSelection&&t(document).off("keydown.tag-editor").on("keydown.tag-editor",function(e){if(8==e.which||46==e.which||e.ctrlKey&&88==e.which){try{var a=getSelection(),r="BODY"==document.activeElement.tagName?t(a.getRangeAt(0).startContainer.parentNode).closest(".tag-editor"):0}catch(e){r=0}if(a.rangeCount>0&&r&&r.length){var l=[],n=a.toString().split(r.prev().data("options").dregex);for(i=0;i
    '+o.placeholder+"
    ")}function i(i){var a=c.toString();c=t(".tag-editor-tag:not(.deleted)",s).map(function(e,i){var a=t.trim(t(this).hasClass("active")?t(this).find("input").val():t(i).text());return a?a:void 0}).get(),s.data("tags",c),r.val(c.join(o.delimiter[0])),i||a!=c.toString()&&o.onChange(r,s,c),e()}function a(e){for(var a,n=e.closest("li"),d=e.val().replace(/ +/," ").split(o.dregex),g=e.data("old_tag"),f=c.slice(0),h=!1,u=0;u
     '+o.delimiter[0]+'
    '+l(v)+'
    '),o.maxTags&&f.length>=o.maxTags)){h=!0;break}e.attr("maxlength",o.maxLength).removeData("old_tag").val(""),h?e.blur():e.focus(),i()}var r=t(this),c=[],s=t("
      ').insertAfter(r);r.addClass("tag-editor-hidden-src").data("options",o).on("focus.tag-editor",function(){s.click()}),s.append('
    •  
    • ');var d='
    •  '+o.delimiter[0]+'
    • ';s.click(function(e,i){var a,r,l=99999;if(!window.getSelection||""==getSelection())return o.maxTags&&s.data("tags").length>=o.maxTags?(s.find("input").blur(),!1):(n=!0,t("input:focus",s).blur(),n?(n=!0,t(".placeholder",s).remove(),i&&i.length?r="before":t(".tag-editor-tag",s).each(function(){var n=t(this),o=n.offset(),c=o.left,s=o.top;e.pageY>=s&&e.pageY<=s+n.height()&&(e.pageXa&&(l=a,i=n))}),"before"==r?t(d).insertBefore(i.closest("li")).find(".tag-editor-tag").click():"after"==r?t(d).insertAfter(i.closest("li")).find(".tag-editor-tag").click():t(d).appendTo(s).find(".tag-editor-tag").click(),!1):!1)}),s.on("click",".tag-editor-delete",function(){if(t(this).prev().hasClass("active"))return t(this).closest("li").find("input").caret(-1),!1;var a=t(this).closest("li"),l=a.find(".tag-editor-tag");return o.beforeTagDelete(r,s,c,l.text())===!1?!1:(l.addClass("deleted").animate({width:0},o.animateDelete,function(){a.remove(),e()}),i(),!1)}),o.clickDelete&&s.on("mousedown",".tag-editor-tag",function(a){if(a.ctrlKey||a.which>1){var l=t(this).closest("li"),n=l.find(".tag-editor-tag");return o.beforeTagDelete(r,s,c,n.text())===!1?!1:(n.addClass("deleted").animate({width:0},o.animateDelete,function(){l.remove(),e()}),i(),!1)}}),s.on("click",".tag-editor-tag",function(e){if(o.clickDelete&&(e.ctrlKey||e.which>1))return!1;if(!t(this).hasClass("active")){var i=t(this).text(),a=Math.abs((t(this).offset().left-e.pageX)/t(this).width()),r=parseInt(i.length*a),n=t(this).html('').addClass("active").find("input");if(n.data("old_tag",i).tagEditorInput().focus().caret(r),o.autocomplete){var c=t.extend({},o.autocomplete),d="select"in c?o.autocomplete.select:"";c.select=function(e,i){d&&d(e,i),setTimeout(function(){s.trigger("click",[t(".active",s).find("input").closest("li").next("li").find(".tag-editor-tag")])},20)},n.autocomplete(c)}}return!1}),s.on("blur","input",function(d){d.stopPropagation();var g=t(this),f=g.data("old_tag"),h=t.trim(g.val().replace(/ +/," ").replace(o.dregex,o.delimiter[0]));if(h){if(h.indexOf(o.delimiter[0])>=0)return void a(g);if(h!=f)if(o.forceLowercase&&(h=h.toLowerCase()),cb_val=o.beforeTagSave(r,s,c,f,h),h=cb_val||h,cb_val===!1){if(f)return g.val(f).focus(),n=!1,void i();try{g.closest("li").remove()}catch(d){}f&&i()}else o.removeDuplicates&&t(".tag-editor-tag:not(.active)",s).each(function(){t(this).text()==h&&t(this).closest("li").remove()})}else{if(f&&o.beforeTagDelete(r,s,c,f)===!1)return g.val(f).focus(),n=!1,void i();try{g.closest("li").remove()}catch(d){}f&&i()}g.parent().html(l(h)).removeClass("active"),h!=f&&i(),e()});var g;s.on("paste","input",function(){t(this).removeAttr("maxlength"),g=t(this),setTimeout(function(){a(g)},30)});var f;s.on("keypress","input",function(e){o.delimiter.indexOf(String.fromCharCode(e.which))>=0&&(f=t(this),setTimeout(function(){a(f)},20))}),s.on("keydown","input",function(e){var i=t(this);if((37==e.which||!o.autocomplete&&38==e.which)&&!i.caret()||8==e.which&&!i.val()){var a=i.closest("li").prev("li").find(".tag-editor-tag");return a.length?a.click().find("input").caret(-1):!i.val()||o.maxTags&&s.data("tags").length>=o.maxTags||t(d).insertBefore(i.closest("li")).find(".tag-editor-tag").click(),!1}if((39==e.which||!o.autocomplete&&40==e.which)&&i.caret()==i.val().length){var l=i.closest("li").next("li").find(".tag-editor-tag");return l.length?l.click().find("input").caret(0):i.val()&&s.click(),!1}if(9==e.which){if(e.shiftKey){var a=i.closest("li").prev("li").find(".tag-editor-tag");if(a.length)a.click().find("input").caret(0);else{if(!i.val()||o.maxTags&&s.data("tags").length>=o.maxTags)return r.attr("disabled","disabled"),void setTimeout(function(){r.removeAttr("disabled")},30);t(d).insertBefore(i.closest("li")).find(".tag-editor-tag").click()}return!1}var l=i.closest("li").next("li").find(".tag-editor-tag");if(l.length)l.click().find("input").caret(0);else{if(!i.val())return;s.click()}return!1}if(!(46!=e.which||t.trim(i.val())&&i.caret()!=i.val().length)){var l=i.closest("li").next("li").find(".tag-editor-tag");return l.length?l.click().find("input").caret(0):i.val()&&s.click(),!1}if(13==e.which)return s.trigger("click",[i.closest("li").next("li").find(".tag-editor-tag")]),o.maxTags&&s.data("tags").length>=o.maxTags&&s.find("input").blur(),!1;if(36!=e.which||i.caret()){if(35==e.which&&i.caret()==i.val().length)s.find(".tag-editor-tag").last().click();else if(27==e.which)return i.val(i.data("old_tag")?i.data("old_tag"):"").blur(),!1}else s.find(".tag-editor-tag").first().click()});for(var h=o.initialTags.length?o.initialTags:r.val().split(o.dregex),u=0;u=o.maxTags);u++){var v=t.trim(h[u].replace(/ +/," "));v&&(o.forceLowercase&&(v=v.toLowerCase()),c.push(v),s.append('
    •  '+o.delimiter[0]+'
      '+l(v)+'
    • '))}i(!0),o.sortable&&t.fn.sortable&&s.sortable({distance:5,cancel:".tag-editor-spacer, input",helper:"clone",update:function(){i()}})})},t.fn.tagEditor.defaults={initialTags:[],maxTags:0,maxLength:50,delimiter:",;",placeholder:"",forceLowercase:!0,removeDuplicates:!0,clickDelete:!1,animateDelete:175,sortable:!0,autocomplete:null,onChange:function(){},beforeTagSave:function(){},beforeTagDelete:function(){}}}(jQuery); \ No newline at end of file diff --git a/clock/templates/base.html b/clock/templates/base.html index afe031f..89c0fbb 100755 --- a/clock/templates/base.html +++ b/clock/templates/base.html @@ -1,4 +1,4 @@ -{% load staticfiles i18n bootstrap3 django_bootstrap_breadcrumbs base_extras %} +{% load render_bundle from webpack_loader %}{% load staticfiles i18n bootstrap3 django_bootstrap_breadcrumbs base_extras %} @@ -26,26 +26,9 @@ - {% block title %}Clock | {% endblock title %}{% block extra_title %} - Goethe-Universität{% endblock extra_title %} - - - - - - - - - - - {% block extra_head %} - {% endblock extra_head %} - - + {% block title %}Clock | {% endblock title %}{% block extra_title %}Goethe-Universität{% endblock extra_title %} + {% render_bundle 'main' %} + {% block extra_head %}{% endblock extra_head %} @@ -83,20 +66,6 @@

      Last visited: {{ request.session.last_visited }}

      - - - - - - - - - - - {% block extra_js %}{% endblock extra_js %} diff --git a/clock/templates/contract/list.html b/clock/templates/contract/list.html index 3ea8104..647e600 100755 --- a/clock/templates/contract/list.html +++ b/clock/templates/contract/list.html @@ -2,7 +2,9 @@ {% load staticfiles i18n django_bootstrap_breadcrumbs crispy_forms_tags %} {% block extra_head %}{{ block.super }} +{% comment %} +{% endcomment %} {% endblock extra_head %} {% block container %} @@ -40,7 +42,9 @@

      {% trans 'Contracts' %}

      {% endblock container %} {% block extra_js %} + {% comment %} + {% endcomment %} +{% endcomment %} {% endblock extra_head %} {% block container %} @@ -132,7 +134,7 @@

      {% trans 'Last five finished shifts' %}

      }()); {% endif %} - + {% comment %}{% endcomment %} {% if last_shifts %} @@ -15,6 +16,7 @@ href="{% static 'libraries/eonasdan-bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css' %}"/> +{% endcomment %} {% endblock extra_head %} {% block breadcrumbs %}{{ block.super }} diff --git a/clock/templates/shift/month_archive_view.html b/clock/templates/shift/month_archive_view.html index 9b17087..600b475 100755 --- a/clock/templates/shift/month_archive_view.html +++ b/clock/templates/shift/month_archive_view.html @@ -2,6 +2,7 @@ {% load staticfiles i18n django_bootstrap_breadcrumbs format_duration base_extras %} {% get_current_language as LANGUAGE_CODE %} {% block extra_head %}{{ block.super }} +{% comment %} @@ -12,6 +13,7 @@ +{% endcomment %} {% endblock extra_head %} {% block breadcrumbs %}{{ block.super }} @@ -123,7 +125,9 @@

      {% trans 'Shifts in' %} {{ month|date:"F Y" }}

      {% endblock container %} {% block extra_js %} + {% comment %} + {% endcomment %} - - - -{% endcomment %} -{% endblock extra_head %} +{% block extra_head %}{{ block.super }}{% endblock extra_head %} {% block container %}
      @@ -134,8 +127,7 @@

      {% trans 'Last five finished shifts' %}

      }()); {% endif %} - {% comment %}{% endcomment %} - {% if last_shifts %} + {% if last_shifts %} - - - - - - - -{% endcomment %} -{% endblock extra_head %} +{% block extra_head %}{{ block.super }}{% endblock extra_head %} {% block breadcrumbs %}{{ block.super }} {% if shift %} @@ -39,31 +26,43 @@

      {% trans 'New shift' %}

      {% crispy form %}
      + {% endblock %} {% block extra_js %} - {% if not shift %} - {% endif %} {% endblock extra_js %} diff --git a/clock/templates/shift/month_archive_view.html b/clock/templates/shift/month_archive_view.html index 600b475..7b2b9fc 100755 --- a/clock/templates/shift/month_archive_view.html +++ b/clock/templates/shift/month_archive_view.html @@ -1,20 +1,7 @@ {% extends 'shift/base.html' %} {% load staticfiles i18n django_bootstrap_breadcrumbs format_duration base_extras %} {% get_current_language as LANGUAGE_CODE %} -{% block extra_head %}{{ block.super }} -{% comment %} - - - - - - - -{% endcomment %} -{% endblock extra_head %} +{% block extra_head %}{{ block.super }}{% endblock extra_head %} {% block breadcrumbs %}{{ block.super }} {% breadcrumb month|date:"F Y" "shift_list" %} @@ -125,14 +112,10 @@

      {% trans 'Shifts in' %} {{ month|date:"F Y" }}

      {% endblock container %} {% block extra_js %} - {% comment %} - - {% endcomment %} - -{% endblock extra_head %} - {% block breadcrumbs %}{{ block.super }}{% endblock breadcrumbs %} {% block content_account %} diff --git a/clock/templates/shift/edit.html b/clock/templates/shift/edit.html index 6bdabdd..eb4622f 100755 --- a/clock/templates/shift/edit.html +++ b/clock/templates/shift/edit.html @@ -35,33 +35,44 @@

      {% trans 'New shift' %}

      diff --git a/clock/templates/shift/month_archive_view.html b/clock/templates/shift/month_archive_view.html index 7b2b9fc..3513c8d 100755 --- a/clock/templates/shift/month_archive_view.html +++ b/clock/templates/shift/month_archive_view.html @@ -214,13 +214,7 @@

      {% trans 'Shifts in' %} {{ month|date:"F Y" }}

      style: 'os', selector: 'td.select-checkbox' }, - buttons: [], - {% get_current_language as LANGUAGE_CODE %} - {% if LANGUAGE_CODE == 'de' %} - language: { - url: "{% static 'libraries/datatables/locale/de.json' %}" - } - {% endif %} + buttons: [] }); }); diff --git a/config/settings/common.py b/config/settings/common.py index a1c9766..f9f9135 100755 --- a/config/settings/common.py +++ b/config/settings/common.py @@ -295,7 +295,7 @@ 'DEFAULT': { 'CACHE': False, 'BUNDLE_DIR_NAME': 'bundles/', - 'STATS_FILE': str(ROOT_DIR.path('webpack-stats.json')), + 'STATS_FILE': str(ROOT_DIR.path('webpack-stats-local.json')), 'POLL_INTERVAL': 0.1, 'TIMEOUT': None, 'IGNORE': ['.+\.hot-update.js', '.+\.map'] diff --git a/docker/local/Dockerfile.web b/docker/local/Dockerfile.web index 385dbfe..74e1fc2 100644 --- a/docker/local/Dockerfile.web +++ b/docker/local/Dockerfile.web @@ -1,14 +1,12 @@ FROM python:3.6.3-alpine3.6 ENV PYTHONBUFFERED 1 -ARG VERBOSE # Add new user to run the whole thing as non-root RUN addgroup -S app \ && adduser -G app -u 1000 -h /app -D app # Copy all files and change the working directory -COPY --chown=app:app . /app -WORKDIR /app +COPY --chown=app:app Pipfile Pipfile.lock /app/ # Install build dependencies for PostgreSQL. While we're at it, also install # pipenv and all python requirements. Then purge unneeded build dependencies. @@ -19,17 +17,21 @@ RUN apk update \ zlib-dev \ jpeg-dev \ libxslt-dev \ - libxml2-dev \ - && apk add --no-cache postgresql postgresql-dev jpeg gettext \ - && pip install pipenv \ - && LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pipenv install --dev --system $VERBOSE" \ - && apk del .build-deps + libxml2-dev +RUN apk add --no-cache postgresql postgresql-dev jpeg gettext +RUN pip install pipenv + +WORKDIR /app +RUN LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pipenv install --dev --system" +RUN apk del .build-deps # Change to user +COPY --chown=app:app . /app USER app # Let Django collect all staticfiles and compile localizations RUN python /app/manage.py collectstatic --noinput \ && python /app/manage.py compilemessages + ENTRYPOINT ["/app/docker/local/entrypoint.sh"] diff --git a/webpack.dev.js b/webpack.dev.js index d5029c8..a5f7cc8 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -9,7 +9,7 @@ module.exports = merge(common, { contentBase: './assets/bundles/' }, plugins: [ - new BundleTracker({filename: './webpack-stats.json'}), + new BundleTracker({filename: './webpack-stats-local.json'}), new CleanWebpackPlugin(['./assets/bundles/']), ] }); diff --git a/webpack.prod.js b/webpack.prod.js index 0828671..c18a06c 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -8,7 +8,7 @@ const common = require('./webpack.common.js'); module.exports = merge(common, { devtool: 'source-map', output: { - path: path.resolve('./assets/assets/'), + path: path.resolve('./assets/dist/'), filename: "[name]-[hash].js", publicPath: '/static/dist/' }, From e2eae8f44ef6554165576fdc2ae0f068fdad463a Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Fri, 10 Nov 2017 12:41:36 +0100 Subject: [PATCH 06/14] Fix crashes on Alpine Linux. --- manage.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/manage.py b/manage.py index df1be3a..2606120 100755 --- a/manage.py +++ b/manage.py @@ -1,6 +1,11 @@ -#!/usr/bin/env python import os import sys +import threading + +# This is a workaround for Alpine Linux (musl libc) quirk: +# https://github.com/docker-library/python/issues/211 +# Thanks to https://github.com/fadawar for the snippet! +threading.stack_size(2 * 1024 * 1024) if __name__ == '__main__': os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.local') @@ -12,7 +17,7 @@ # issue is really that Django is missing to avoid masking other # exceptions on Python 2. try: - import django # noqa + import django # noqa except ImportError: raise ImportError( "Couldn't import Django. Are you sure it's installed and " From 20c61551fd50680aebf0a22cc2678065cd7d0742 Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Fri, 10 Nov 2017 12:44:43 +0100 Subject: [PATCH 07/14] Update local Dockerfile --- docker-compose.yml | 2 +- docker/local/Dockerfile.assets | 11 ++------- docker/local/Dockerfile.web | 44 +++++++++++++++------------------- docker/local/start.sh | 1 - 4 files changed, 22 insertions(+), 36 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f2c9880..b8d6410 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,7 +23,7 @@ services: build: context: . dockerfile: ./docker/local/Dockerfile.web - command: /app/docker/local/start.sh + command: /start.sh environment: POSTGRES_HOST: db POSTGRES_PORT: 5432 diff --git a/docker/local/Dockerfile.assets b/docker/local/Dockerfile.assets index ab48788..4e49653 100644 --- a/docker/local/Dockerfile.assets +++ b/docker/local/Dockerfile.assets @@ -1,13 +1,6 @@ FROM node:8.9-alpine -RUN deluser --remove-home node \ - && addgroup -S app \ - && adduser -G app -u 1000 -h /app -D app - -USER app - -COPY --chown=app:app . ./app/ +COPY package.json yarn.lock /app/ WORKDIR /app -RUN yarn install --dev \ - && yarn build +RUN yarn install --dev diff --git a/docker/local/Dockerfile.web b/docker/local/Dockerfile.web index 74e1fc2..d9f11ce 100644 --- a/docker/local/Dockerfile.web +++ b/docker/local/Dockerfile.web @@ -1,37 +1,31 @@ -FROM python:3.6.3-alpine3.6 -ENV PYTHONBUFFERED 1 +FROM python:alpine3.6 +# Tell python not to produce any `__pycache__` and `*.pyc` files +ENV PYTHONBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 -# Add new user to run the whole thing as non-root -RUN addgroup -S app \ - && adduser -G app -u 1000 -h /app -D app - -# Copy all files and change the working directory -COPY --chown=app:app Pipfile Pipfile.lock /app/ - -# Install build dependencies for PostgreSQL. While we're at it, also install -# pipenv and all python requirements. Then purge unneeded build dependencies. +# Install all dependencies needed to install our python requirements RUN apk update \ - && apk add --no-cache --virtual .build-deps \ + && apk add --no-cache \ gcc \ musl-dev \ zlib-dev \ jpeg-dev \ libxslt-dev \ - libxml2-dev -RUN apk add --no-cache postgresql postgresql-dev jpeg gettext + libxml2-dev \ + postgresql \ + postgresql-dev \ + jpeg \ + gettext + +# Copy Pipfile, install pipenv and then install all python dependencies +COPY Pipfile Pipfile.lock / RUN pip install pipenv - -WORKDIR /app RUN LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pipenv install --dev --system" -RUN apk del .build-deps - -# Change to user -COPY --chown=app:app . /app -USER app -# Let Django collect all staticfiles and compile localizations -RUN python /app/manage.py collectstatic --noinput \ - && python /app/manage.py compilemessages +# Copy the entrypoint.sh and start.sh and make them executable +COPY docker/local/entrypoint.sh docker/local/start.sh / +RUN chmod +x /entrypoint.sh /start.sh +# All subsequent commands will be run from the /app folder +WORKDIR /app -ENTRYPOINT ["/app/docker/local/entrypoint.sh"] +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/local/start.sh b/docker/local/start.sh index b0ea90b..2eda341 100755 --- a/docker/local/start.sh +++ b/docker/local/start.sh @@ -1,5 +1,4 @@ #!/bin/sh -set -e while true; do echo "Re-starting Django runserver_plus!" From b4fcd72ccef6f3088db5c2caa93abebbf3e68cbf Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Fri, 10 Nov 2017 14:51:43 +0100 Subject: [PATCH 08/14] Finalize production deploy --- Pipfile | 8 ++--- Pipfile.lock | 20 ++++++------ config/favicon_urls.py | 10 +++--- config/settings/common.py | 2 +- config/urls.py | 13 +++----- docker/dokku/Dockerfile | 42 ++++++++++++++----------- docker/dokku/{app.BAK.json => app.json} | 0 docker/dokku/entrypoint.sh | 11 +++++++ package.json | 2 +- webpack.common.js | 10 +++--- webpack.prod.js | 13 +++++--- yarn.lock | 6 +++- 12 files changed, 78 insertions(+), 59 deletions(-) rename docker/dokku/{app.BAK.json => app.json} (100%) create mode 100755 docker/dokku/entrypoint.sh diff --git a/Pipfile b/Pipfile index b30596d..7187cfc 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,7 @@ name = "pypi" [packages] -django = "*" +Django = "*" django-environ = "*" whitenoise = "*" django-braces = "*" @@ -31,11 +31,11 @@ django-webpack-loader = "*" [dev-packages] coverage = "*" -django-coverage-plugin = "*" +django_coverage_plugin = "*" django-extensions = "*" -werkzeug = "*" +Werkzeug = "*" django-test-plus = "*" -factory-boy = "*" +factory_boy = "*" django-debug-toolbar = "*" ipython = "*" ipdb = "*" diff --git a/Pipfile.lock b/Pipfile.lock index e6dcc99..9db41a8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b437255091676e817bc17ca5af7d4a1b0d46c8c64a5be2f80d993863818590d8" + "sha256": "228b5b793962ad51a9a590c67e9a3a1e3cbc2a7b06efb7d73bc06a7fea2732fe" }, "host-environment-markers": { "implementation_name": "cpython", @@ -700,10 +700,10 @@ }, "jinja2": { "hashes": [ - "sha256:2231bace0dfd8d2bf1e5d7e41239c06c9e0ded46e70cc1094a0aa64b0afeb054", - "sha256:ddaa01a212cd6d641401cb01b605f4a4d9f37bfc93043d7f760ec70fb99ff9ff" + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" ], - "version": "==2.9.6" + "version": "==2.10" }, "lockfile": { "hashes": [ @@ -762,11 +762,11 @@ }, "pexpect": { "hashes": [ - "sha256:f853b52afaf3b064d29854771e2db509ef80392509bde2dd7a6ecf2dfc3f0018", - "sha256:3d132465a75b57aa818341c6521392a06cc660feb3988d7f1074f39bd23c9a92" + "sha256:2b50dd8caa5007b10b0afcf075095814780b104b4a5cf9d8fbdc8bbc754e5ca4", + "sha256:00ab0872f80f5db740499e7a1283a7c3b97bea542d72df84d83dea17d0afd2d9" ], "markers": "sys_platform != 'win32'", - "version": "==4.2.1" + "version": "==4.3.0" }, "pickleshare": { "hashes": [ @@ -907,10 +907,10 @@ }, "text-unidecode": { "hashes": [ - "sha256:b7a515d2b14d476b35f7701ceac9872737cbb7a8b2cd07431f3cd73837b97a18", - "sha256:1943a4bfb52f2b5068c8da25cdcfcf28c64c41d891dff70c662927c18ac58052" + "sha256:02efd86b9c0f489f858d8cead62e94d3760dab444054b258734716f7602330a3", + "sha256:d0afd5e8a7ac69bfb1372e1bbfa3c63c22e3db8ae1284690e96b45c4430d08d0" ], - "version": "==1.0" + "version": "==1.1" }, "traitlets": { "hashes": [ diff --git a/config/favicon_urls.py b/config/favicon_urls.py index c18277c..f50e79d 100755 --- a/config/favicon_urls.py +++ b/config/favicon_urls.py @@ -3,11 +3,11 @@ from django.views.generic import RedirectView favicon_urlpatters = [ - url(r'^android-chrome-36x36.png', - RedirectView.as_view( - url=staticfiles_storage.url( - 'common/favicons/android-chrome-36x36.png'), - permanent=False)), + # url(r'^android-chrome-36x36.png', + # RedirectView.as_view( + # url=staticfiles_storage.url( + # 'common/favicons/android-chrome-36x36.png'), + # permanent=False)), url(r'^android-chrome-48x48.png', RedirectView.as_view( url=staticfiles_storage.url( diff --git a/config/settings/common.py b/config/settings/common.py index f9f9135..1ef874a 100755 --- a/config/settings/common.py +++ b/config/settings/common.py @@ -177,7 +177,7 @@ # STATIC FILE CONFIGURATION # ------------------------------------------------------------------------------ # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root -STATIC_ROOT = str(ROOT_DIR('staticfiles')) +STATIC_ROOT = str(ROOT_DIR.path('staticfiles')) # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url STATIC_URL = '/static/' diff --git a/config/urls.py b/config/urls.py index 1b07879..d057840 100755 --- a/config/urls.py +++ b/config/urls.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +# from config.favicon_urls import favicon_urlpatters from django.conf import settings from django.conf.urls import include, url from django.conf.urls.static import static @@ -10,7 +11,6 @@ from django.views import defaults as default_views import clock.profiles.views -from config.favicon_urls import favicon_urlpatters urlpatterns = [ url(r'^', include("clock.pages.urls"), name='pages'), @@ -37,22 +37,19 @@ ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # Add all needed favicon redirects to comply with todays OS/browser standards -urlpatterns += favicon_urlpatters +# urlpatterns += favicon_urlpatters if settings.DEBUG: # This allows the error pages to be debugged during development, just visit # these url in browser to see how these error pages look like. urlpatterns += [ - url( - r'^400/$', + url(r'^400/$', default_views.bad_request, kwargs={'exception': Exception("Bad Request!")}), - url( - r'^403/$', + url(r'^403/$', default_views.permission_denied, kwargs={'exception': Exception("Permission Denied")}), - url( - r'^404/$', + url(r'^404/$', default_views.page_not_found, kwargs={'exception': Exception("Page not Found")}), url(r'^500/$', default_views.server_error), diff --git a/docker/dokku/Dockerfile b/docker/dokku/Dockerfile index bdb8cf2..88f25f9 100644 --- a/docker/dokku/Dockerfile +++ b/docker/dokku/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.6.3-alpine3.6 +FROM python:alpine3.6 ARG DJANGO_SETTINGS_MODULE ARG DJANGO_ADMIN_URL ARG DJANGO_SECRET_KEY @@ -10,37 +10,41 @@ ENV PYTHONBUFFERED=1 DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE} DJANGO_ADM RUN addgroup -S app \ && adduser -G app -h /app -D app -# Copy app files and change working directory. -COPY --chown=app:app . /app -WORKDIR /app - -# Install build dependencies for PostgreSQL. While we're at it, also install -# pipenv and all python requirements. Then remove unneeded build dependencies. +# Install build dependencies RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \ && apk update \ && apk add bash \ && apk add \ - && apk add --no-cache --virtual .build-deps \ + && apk add --no-cache \ gcc \ musl-dev \ zlib-dev \ jpeg-dev \ libxslt-dev \ - libxml2-dev -RUN apk add --no-cache postgresql postgresql-dev jpeg gettext yarn@edge + libxml2-dev \ + postgresql \ + postgresql-dev \ + jpeg \ + gettext \ + yarn@edge +# Copy Pipfile and install python dependencies +COPY --chown=app:app Pipfile Pipfile.lock / RUN pip install pipenv RUN LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pipenv install --system --deploy" -RUN apk del .build-deps -# Change to user and copy code +# Copy dokku specific files to the project root +COPY --chown=app:app docker/dokku/* /app/ +COPY --chown=app:app docker/dokku/entrypoint.sh / +RUN chmod +x entrypoint.sh + USER app +WORKDIR /app -# Copy dokku specific files to the project root -COPY docker/dokku/* /app/ +COPY --chown=app:app package.json yarn.lock /app/ +RUN yarn install --dev + +# Copy all related app files +COPY --chown=app:app . /app -# Let Django collect all staticfiles -RUN python manage.py compilemessages \ - && yarn install --dev \ - && yarn prod \ - && python manage.py collectstatic +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/dokku/app.BAK.json b/docker/dokku/app.json similarity index 100% rename from docker/dokku/app.BAK.json rename to docker/dokku/app.json diff --git a/docker/dokku/entrypoint.sh b/docker/dokku/entrypoint.sh new file mode 100755 index 0000000..be17645 --- /dev/null +++ b/docker/dokku/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +# while ! pg_isready -h $POSTGRES_HOST -p $POSTGRES_PORT; do +# >&2 echo "Postgres is unavailable - sleeping" +# sleep 1 +# done + +# >&2 echo "Postgres is up - continuing" + +exec "$@" diff --git a/package.json b/package.json index 766289b..b039460 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "jquery": "^1.1.13", "less": "^2.7.3", "less-loader": "^4.0.5", - "moment": "^2.19.1", + "moment": "2.18.1", "nodemon": "^1.12.1", "style-loader": "^0.19.0", "uglifyjs-webpack-plugin": "^1.0.1", diff --git a/webpack.common.js b/webpack.common.js index 238fb8e..669e63a 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -48,11 +48,11 @@ module.exports = { }, plugins: [ new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /de|en/), - // // new BundleAnalyzerPlugin({ - // // analyzerMode: "server", - // // analyzerHost: "127.0.0.1", - // // analyzerPort: "8888" - // // }) + // new BundleAnalyzerPlugin({ + // analyzerMode: "server", + // analyzerHost: "127.0.0.1", + // analyzerPort: "8888" + // }) ], resolve: { alias: { diff --git a/webpack.prod.js b/webpack.prod.js index c18a06c..827b3b5 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -28,11 +28,14 @@ module.exports = merge(common, { // keeps hashes consistent between compilations new webpack.optimize.OccurrenceOrderPlugin(), + /* Currently disabled, because it somehow causes problems with momentjs locales in production. + Further the code seems to be minimized anyhow.. + */ // minifies your code - new webpack.optimize.UglifyJsPlugin({ - compressor: { - warnings: false - } - }) + // new webpack.optimize.UglifyJsPlugin({ + // compressor: { + // warnings: false + // } + // }) ] }); diff --git a/yarn.lock b/yarn.lock index 35f6ae6..6387d07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2156,7 +2156,11 @@ moment-timezone@^0.4.0: dependencies: moment ">= 2.6.0" -"moment@>= 2.6.0", moment@^2.10, moment@^2.19.1: +moment@2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" + +"moment@>= 2.6.0", moment@^2.10: version "2.19.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167" From cfca190c33b7e50ebd48b7f5c737aa6e9c0fd0e8 Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Fri, 10 Nov 2017 17:39:02 +0100 Subject: [PATCH 09/14] Fix datetimepicker and set timezone to Europe/Berlin --- clock/shifts/forms.py | 19 +++++++++- clock/shifts/views.py | 27 ++++++++++++-- clock/templates/shift/edit.html | 37 ++++++++----------- .../shift/fields/datetimepicker_field.html | 15 ++++++++ docker/dokku/Dockerfile | 7 +++- docker/local/Dockerfile.web | 7 +++- 6 files changed, 83 insertions(+), 29 deletions(-) create mode 100644 clock/templates/shift/fields/datetimepicker_field.html diff --git a/clock/shifts/forms.py b/clock/shifts/forms.py index 769bc28..24b9a22 100755 --- a/clock/shifts/forms.py +++ b/clock/shifts/forms.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Submit +from crispy_forms.layout import HTML, Field, Fieldset, Layout, Submit from django import forms from django.conf import settings from django.core.exceptions import ValidationError @@ -58,7 +58,11 @@ def __init__(self, *args, **kwargs): self.view = kwargs.pop('view') self.user = self.request.user super(ShiftForm, self).__init__(*args, **kwargs) - # self.fields['shift_started'].widget = forms.HiddenInput() + + # Hide the actual input fields + self.fields['shift_started'].widget = forms.HiddenInput() + self.fields['shift_finished'].widget = forms.HiddenInput() + self.fields['pause_duration'].widget = forms.HiddenInput() # Retrieve all contracts that belong to the user self.fields['contract'].queryset = Contract.objects.filter( @@ -90,6 +94,17 @@ def __init__(self, *args, **kwargs): self.helper = FormHelper(self) self.helper.form_action = '.' self.helper.form_method = 'post' + self.helper.layout = Layout( + Field( + 'shift_started', + template='shift/fields/datetimepicker_field.html'), + Field( + 'shift_finished', + template='shift/fields/datetimepicker_field.html'), + Field( + 'pause_duration', + template='shift/fields/datetimepicker_field.html'), + Field('contract'), Field('key'), Field('tags'), Field('note')) self.helper.layout.append( FormActions( HTML(cancel_html_inject), diff --git a/clock/shifts/views.py b/clock/shifts/views.py index 2b0ae56..2591b17 100755 --- a/clock/shifts/views.py +++ b/clock/shifts/views.py @@ -1,10 +1,12 @@ from datetime import date, datetime +from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.shortcuts import redirect from django.utils import timezone from django.utils.decorators import method_decorator +from django.utils.timezone import activate from django.utils.translation import ugettext_lazy as _ from django.views.decorators.http import require_POST from django.views.generic.dates import ( @@ -15,6 +17,7 @@ ) from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.list import ListView +from pytz import timezone as p_timezone from clock.pages.mixins import UserObjectOwnerMixin from clock.shifts.forms import QuickActionForm, ShiftForm @@ -128,18 +131,18 @@ def get_form_kwargs(self): return kwargs @property - def base_date(self): + def start_datetime(self): try: d = datetime( int(self.request.session['last_kwargs']['year']), int(self.request.session['last_kwargs']['month']), 1, - hour=8).strftime("%Y-%m-%d %H:%M") + hour=8).strftime("%Y-%m-%dT%H:%M") if self.request.session['last_kwargs'][ 'month'] == datetime.now().strftime("%m"): - d = datetime.now().strftime("%Y-%m-%d") + d = datetime.now().strftime("%Y-%m-%dT%H:%M") except KeyError: - d = datetime.now().strftime("%Y-%m-%d") + d = datetime.now().strftime("%Y-%m-%dT%H:%M") return d @@ -174,6 +177,22 @@ def get_form_kwargs(self): kwargs.update(k) return kwargs + def get_shift(self): + # Use the current timezone when retrieving datetime objects + tz = p_timezone(settings.TIME_ZONE) + + moment_format = "%Y-%m-%dT%H:%M" + obj = self.get_object() + dates = { + 'started': + obj.shift_started.astimezone(tz).strftime(moment_format), + 'finished': + obj.shift_finished.astimezone(tz).strftime(moment_format), + 'paused': (datetime(1970, 1, 1) + + obj.pause_duration).strftime(moment_format), + } + return dates + @method_decorator(login_required, name="dispatch") class ShiftManualDelete(DeleteView, UserObjectOwnerMixin): diff --git a/clock/templates/shift/edit.html b/clock/templates/shift/edit.html index eb4622f..baf1bac 100755 --- a/clock/templates/shift/edit.html +++ b/clock/templates/shift/edit.html @@ -31,9 +31,8 @@

      {% trans 'New shift' %}

      {% block extra_js %} - {% endblock extra_js %} diff --git a/clock/templates/shift/fields/datetimepicker_field.html b/clock/templates/shift/fields/datetimepicker_field.html new file mode 100644 index 0000000..adb8085 --- /dev/null +++ b/clock/templates/shift/fields/datetimepicker_field.html @@ -0,0 +1,15 @@ +{% load crispy_forms_field %} + +<{% if tag %}{{ tag }}{% else %}div{% endif %} id="div_{{ field.auto_id }}" class="form-group{% if wrapper_class %} {{ wrapper_class }}{% endif %}{% if form_show_errors%}{% if field.errors %} has-error{% endif %}{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %}"> + {% if field.label and form_show_labels %} + + {% endif %} + +
      + {% crispy_field field %} + {% include 'bootstrap3/layout/help_text_and_errors.html' %} +
      + + diff --git a/docker/dokku/Dockerfile b/docker/dokku/Dockerfile index 88f25f9..84615ef 100644 --- a/docker/dokku/Dockerfile +++ b/docker/dokku/Dockerfile @@ -26,7 +26,12 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/community" >> /etc/apk/re postgresql-dev \ jpeg \ gettext \ - yarn@edge + yarn@edge \ + tzdata + +# Set the correct timezone +RUN cp /usr/share/zoneinfo/Europe/Berlin /etc/localtime \ + && echo "Europe/Berlin" > /etc/timezone # Copy Pipfile and install python dependencies COPY --chown=app:app Pipfile Pipfile.lock / diff --git a/docker/local/Dockerfile.web b/docker/local/Dockerfile.web index d9f11ce..1c54c97 100644 --- a/docker/local/Dockerfile.web +++ b/docker/local/Dockerfile.web @@ -14,7 +14,12 @@ RUN apk update \ postgresql \ postgresql-dev \ jpeg \ - gettext + gettext \ + tzdata + +# Set the correct timezone +RUN cp /usr/share/zoneinfo/Europe/Berlin /etc/localtime \ + && echo "Europe/Berlin" > /etc/timezone # Copy Pipfile, install pipenv and then install all python dependencies COPY Pipfile Pipfile.lock / From 7b29142ee47f41f0adcff11a7e7086e1134e8456 Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Sat, 11 Nov 2017 13:27:12 +0100 Subject: [PATCH 10/14] Fix django-debug-toolbar when running in a Docker container --- config/settings/local.py | 4 ++-- docker/dokku/Dockerfile | 2 +- docker/dokku/app.json | 2 +- docker/dokku/deploy.sh | 7 +++++++ docker/local/Dockerfile.web | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 docker/dokku/deploy.sh diff --git a/config/settings/local.py b/config/settings/local.py index 396a7a2..93dded6 100755 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -54,8 +54,8 @@ INTERNAL_IPS = ['127.0.0.1', '192.168.99.100', '192.168.99.101'] -# tricks to have debug toolbar when developing with docker -if os.environ.get('USE_DOCKER') == 'yes': +# Fix django-debug-toolbar when running Django in a Docker container +if env('INSIDE_DOCKER', default=False): ip = socket.gethostbyname(socket.gethostname()) INTERNAL_IPS += [ip[:-1] + "1"] diff --git a/docker/dokku/Dockerfile b/docker/dokku/Dockerfile index 84615ef..0639822 100644 --- a/docker/dokku/Dockerfile +++ b/docker/dokku/Dockerfile @@ -41,7 +41,7 @@ RUN LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pipenv install --system --deploy" # Copy dokku specific files to the project root COPY --chown=app:app docker/dokku/* /app/ COPY --chown=app:app docker/dokku/entrypoint.sh / -RUN chmod +x entrypoint.sh +RUN chmod +x entrypoint.sh /app/deploy.sh USER app WORKDIR /app diff --git a/docker/dokku/app.json b/docker/dokku/app.json index 4dc5c44..ad2ad27 100644 --- a/docker/dokku/app.json +++ b/docker/dokku/app.json @@ -1,7 +1,7 @@ { "scripts": { "dokku": { - "predeploy": "python /app/manage.py migrate --noinput && yarn prod && python /app/manage.py collectstatic --noinput" + "predeploy": "sh /app/deploy.sh" } } } diff --git a/docker/dokku/deploy.sh b/docker/dokku/deploy.sh new file mode 100644 index 0000000..38cd619 --- /dev/null +++ b/docker/dokku/deploy.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -e + +python /app/manage.py migrate --noinput +python /app/manage.py compilemessages +yarn prod +python /app/manage.py collectstatic --noinput diff --git a/docker/local/Dockerfile.web b/docker/local/Dockerfile.web index 1c54c97..8cd3c40 100644 --- a/docker/local/Dockerfile.web +++ b/docker/local/Dockerfile.web @@ -1,6 +1,6 @@ FROM python:alpine3.6 # Tell python not to produce any `__pycache__` and `*.pyc` files -ENV PYTHONBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 INSIDE_DOCKER=1 # Install all dependencies needed to install our python requirements RUN apk update \ From 1fe90516bc6b1bacd0eff428260a5fc20ee204ff Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Sat, 11 Nov 2017 14:24:43 +0100 Subject: [PATCH 11/14] Update strings and translations --- clock/contracts/fields.py | 44 ++-- clock/contracts/forms.py | 30 +-- clock/contracts/urls.py | 16 +- clock/contracts/utils.py | 5 +- clock/contracts/views.py | 5 +- clock/exports/mixins.py | 3 +- clock/exports/printing.py | 93 +++++--- clock/exports/urls.py | 6 +- clock/exports/views.py | 17 +- clock/pages/middleware.py | 8 +- clock/pages/mixins.py | 4 +- clock/pages/utils.py | 14 +- clock/pages/views.py | 8 +- clock/profiles/forms.py | 13 +- clock/profiles/middleware.py | 10 +- clock/profiles/views.py | 31 +-- clock/shifts/forms.py | 31 ++- clock/shifts/models.py | 73 +++--- clock/shifts/utils.py | 25 ++- clock/shifts/views.py | 20 +- clock/templates/account/email_confirm.html | 6 +- clock/templates/account/email_confirmed.html | 6 +- .../account/password_reset_done.html | 3 +- .../templates/account/verification_sent.html | 4 +- .../account/verified_email_required.html | 9 +- locale/de/LC_MESSAGES/django.po | 210 +++++++++++------- 26 files changed, 381 insertions(+), 313 deletions(-) diff --git a/clock/contracts/fields.py b/clock/contracts/fields.py index 913ca3e..6577891 100755 --- a/clock/contracts/fields.py +++ b/clock/contracts/fields.py @@ -4,6 +4,8 @@ from django.db.models.fields import IntegerField from django.forms.fields import CharField from django.utils.translation import ugettext_lazy as _ + + """ All credit for WorkingHoursFieldForm and WorkingHoursField go to http://charlesleifer.com/blog/writing-custom-field-django/ @@ -13,8 +15,8 @@ class WorkingHoursFieldForm(CharField): - """ - Implementation of a CharField to handle validation of data from WorkingHoursField. + """Implementation of a CharField to handle validation of data from + WorkingHoursField. """ def __init__(self, label=_('Work hours'), *args, **kwargs): @@ -23,13 +25,17 @@ def __init__(self, label=_('Work hours'), *args, **kwargs): label=label, *args, **kwargs) def clean(self, value): - """ - Checks the supplied data from the users. Accepts work hours in the format of "HH:MM" (e.g. 14:35) - Needs to check two cases: + """Checks the supplied data from the users. Accepts work hours in the format + of "HH:MM" (e.g. 14:35) Needs to check two cases: + a) Did the user supply the correct format (HH:MM)? - b) If this fails, then we'll try to assume the user only entered the hours (HH) and we'll add - the minutes by ourselves. This will also check if the entered value is actually an int! - Furthermore check if the total work time is bigger than zero and smaller than 80 hours (288.000 seconds) + + b) If this fails, then we'll try to assume the user only entered + the hours (HH) and we'll add the minutes by ourselves. This will + also check if the entered value is actually an int! + + Furthermore check if the total work time is bigger than zero and + smaller than 80 hours (288.000 seconds) """ value = super(CharField, self).clean(value) @@ -44,8 +50,8 @@ def clean(self, value): raise ValidationError( _('Working hours entered must be in format HH:MM')) - # If the value is in the correct format, check if the total working hours - # exceed 80 hours per month (this equals 288.000 seconds) + # If the value is in the correct format, check if the total working + # hours exceed 80 hours per month (this equals 288.000 seconds) total_seconds = hours * 3600 + minutes * 60 if total_seconds > 80 * 3600: raise ValidationError( @@ -58,11 +64,11 @@ def clean(self, value): class WorkingHoursField(IntegerField): - """ - Creates a custom field so we can store our working hours in contracts. + """Creates a custom field so we can store our working hours in contracts. Working hours are stored as an integer in minutes inside the database. - This field accepts input in the format HH.MM and will display it the same way. + This field accepts input in the format HH.MM and will display it the same + way. """ # Get values from database and return them as HH.MM @@ -75,18 +81,18 @@ def from_db_value(self, value, expression, connection, context): def to_python(self, value): if value is None: return value - if isinstance(value, (int, long)): + if isinstance(value, int): return value # Split into two values and return the duration in minutes! - if isinstance(value, basestring): + if isinstance(value, str): try: hours, minutes = map(int, value.split(':')) except ValueError: raise ValidationError( _('Working hours entered must be in format HH:MM')) - # If the user entered a value like '30.3' we will convert it into '30.30'. - # Otherwise it would be interpreted as '30.03'. + # If the user entered a value like '30.3' we will convert it into + # '30.30'. Otherwise it would be interpreted as '30.03'. if len(value.split(':')[1]) == 1 and minutes < 10: minutes *= 10 return (hours * 60) + minutes @@ -102,8 +108,8 @@ def get_db_prep_value(self, value, connection, prepared=False): def formfield(self, form_class=WorkingHoursFieldForm, **kwargs): defaults = { 'help_text': - _('Please specify your working hours in the format HH:MM \ - (eg. 12:15 - meaning 12 hours and 15 minutes)') + _('Please specify your working hours in the format HH:MM ' + '(eg. 12:15 - meaning 12 hours and 15 minutes)') } defaults.update(kwargs) return form_class(**defaults) diff --git a/clock/contracts/forms.py b/clock/contracts/forms.py index a564fd8..b43bc9a 100755 --- a/clock/contracts/forms.py +++ b/clock/contracts/forms.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper -from crispy_forms.layout import Submit, HTML +from crispy_forms.layout import HTML, Submit from django import forms from django.core.urlresolvers import reverse_lazy from django.utils.translation import ugettext_lazy as _ @@ -12,18 +12,10 @@ class ContractForm(forms.ModelForm): class Meta: model = Contract - fields = ('department', 'department_short', 'hours', ) - # This could be used to select working hours with a widget. Right now it does not support values above 24 hours - # widgets = { - # 'hours': DateTimePicker( - # options={ - # "format": "HH.mm", - # "stepping": 10, - # "toolbarPlacement": "top", - # "maxDate": 80, - # } - # ), - # } + fields = ( + 'department', + 'department_short', + 'hours', ) def __init__(self, *args, **kwargs): super(ContractForm, self).__init__(*args, **kwargs) @@ -38,17 +30,13 @@ def __init__(self, *args, **kwargs): add_input_text = _('Create new contract') elif self.initial['view'] == 'contract_update': add_input_text = _('Update contract') - delete_html_inject = u' \ - %(delete_translation)s' % { - 'delete_url': + delete_html_inject = '{}'.format( reverse_lazy( 'contract:delete', kwargs={'pk': self.instance.pk}), - 'delete_translation': - _('Delete') - } + 'btn btn-danger pull-right second-button', _('Delete')) - cancel_html_inject = '%(cancel_translation)s' % \ - {'cancel_url': reverse_lazy('contract:list'), 'cancel_translation': _('Cancel')} + cancel_html_inject = '{}'.format( + reverse_lazy('contract:list'), 'btn btn-default', _('Cancel')) self.helper = FormHelper(self) self.helper.form_action = '.' diff --git a/clock/contracts/urls.py b/clock/contracts/urls.py index 192c262..e7375f1 100755 --- a/clock/contracts/urls.py +++ b/clock/contracts/urls.py @@ -1,16 +1,12 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals -from datetime import datetime - from django.conf.urls import url -from clock.contracts.views import ContractListView, ContractAddView, \ - ContractUpdateView, ContractDeleteView - -# Data to display the current year-month inside the shift_list -currentDate = datetime.now() -currentYear = currentDate.strftime("%Y") -currentMonth = currentDate.strftime("%m") +from clock.contracts.views import ( + ContractAddView, + ContractDeleteView, + ContractListView, + ContractUpdateView, +) urlpatterns = [ # Contract URLs diff --git a/clock/contracts/utils.py b/clock/contracts/utils.py index de368f2..4e420df 100755 --- a/clock/contracts/utils.py +++ b/clock/contracts/utils.py @@ -1,11 +1,10 @@ from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ def convert_work_hours(work_hours): try: hours, minutes = map(int, work_hours.split(':')) except ValueError: - raise ValidationError( - 'Could not split the value during one internal function (convert_work_hours).' - ) + raise ValidationError(_('Could not split the value you provided.')) return (hours * 60) + minutes diff --git a/clock/contracts/views.py b/clock/contracts/views.py index 4c9db0b..e1a9dbe 100755 --- a/clock/contracts/views.py +++ b/clock/contracts/views.py @@ -52,9 +52,8 @@ class ContractUpdateView(UpdateView, UserObjectOwnerMixin): success_url = reverse_lazy('contract:list') def get_initial(self): - """ - Sets initial data for the ModelForm, so we can use the user object and know which view created this form - (CreateView in this case) + """Sets initial data for the ModelForm, so we can use the user object and know + which view created this form (CreateView in this case) """ return { 'user': self.request.user, diff --git a/clock/exports/mixins.py b/clock/exports/mixins.py index 92e5e26..ec7c5d5 100755 --- a/clock/exports/mixins.py +++ b/clock/exports/mixins.py @@ -7,7 +7,8 @@ class PdfResponseMixin(object): def render_to_response(self, context, **response_kwargs): - filename = "Stundenzettel_" + context['month'].strftime('%Y%m') + ".pdf" + filename = "Stundenzettel_{}_.pdf".format( + context['month'].strftime('%Y%m')) response = HttpResponse(content_type='application/pdf') response['Content-Disposition'] = 'attachment; filename=' + filename diff --git a/clock/exports/printing.py b/clock/exports/printing.py index 6749be9..a2f3234 100755 --- a/clock/exports/printing.py +++ b/clock/exports/printing.py @@ -6,16 +6,26 @@ from django.utils import timezone from reportlab.lib import colors from reportlab.lib.enums import TA_CENTER -from reportlab.lib.pagesizes import letter, A4 -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.pagesizes import A4, letter +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import inch, mm from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont -from reportlab.platypus import SimpleDocTemplate, Flowable, Paragraph, Table, TableStyle, Spacer, Frame, KeepInFrame +from reportlab.platypus import ( + Flowable, + Frame, + KeepInFrame, + Paragraph, + SimpleDocTemplate, + Spacer, + Table, + TableStyle, +) from clock.pages.templatetags.format_duration import format_dttd -# Register custom fonts. Path is hardcoded so we're using the internal fonts from /static/ +# Register custom fonts. Path is hardcoded so we're using the internal fonts +# from /static/ pdfmetrics.registerFont( TTFont('OpenSans-Regular', os.path.join( @@ -144,8 +154,8 @@ def _header_footer(canvas, doc): key_text.drawOn(canvas, 200 + 125 + 5, doc.bottomMargin + 2.5) note_text = Paragraph( - "* Tragen Sie in diese Spalte eines der folgenden Kürzel ein, wenn es für diesen " - "Kalendertag zutrifft", styles['BottomText']) + '* Tragen Sie in diese Spalte eines der folgenden Kürzel ein, ' + 'wenn es für diesen Kalendertag zutrifft', styles['BottomText']) w, h = note_text.wrap(doc.width, doc.bottomMargin) note_text.drawOn(canvas, doc.leftMargin + 5, doc.bottomMargin + 45) @@ -202,17 +212,18 @@ def print_shifts(self): # Add text on top of the first page elements.append( Paragraph( - u'Vorlage zur Dokumentation der täglichen Arbeitszeit nach § 17 MiLoG

      ' - u'Wichtig:', styles['TitleBoldCentered'])) + 'Vorlage zur Dokumentation der täglichen Arbeitszeit nach ' + '§ 17 MiLoG

      Wichtig:', styles[ + 'TitleBoldCentered'])) elements.append(Spacer(1, 4)) elements.append( Paragraph( - u'Die Aufzeichnungen sind mindestens wöchentlich zu führen, denn es besteht ' - u'die Verpflichtung "Beginn, Ende und Dauer ' - u'der täglichen Arbeitszeit spätestens bis zum Ablauf des siebten auf den Tag der ' - u'Arbeitsleistung folgenden Kalendertages aufzuzeichnen und diese Aufzeichnungen ' - u'mindestens zwei Jahre beginnend ab dem für die Aufzeichnung maßgelblichen ' - u'Zeitpunkt aufzubewahren.“', styles['TitleCentered'])) + 'Die Aufzeichnungen sind mindestens wöchentlich zu führen, denn es besteht ' + 'die Verpflichtung "Beginn, Ende und Dauer ' + 'der täglichen Arbeitszeit spätestens bis zum Ablauf des siebten auf den Tag der ' + 'Arbeitsleistung folgenden Kalendertages aufzuzeichnen und diese Aufzeichnungen ' + 'mindestens zwei Jahre beginnend ab dem für die Aufzeichnung maßgelblichen ' + 'Zeitpunkt aufzubewahren.“', styles['TitleCentered'])) elements.append(Spacer(1, 6)) # Add boxes above the table @@ -221,15 +232,19 @@ def print_shifts(self): width=doc.width, height=20, text_label="FB / Institut / Abteilung", - text_box=self.context['department']), Spacer(1, 4), BoxyLine( - width=doc.width, - height=20, - text_label="Name des Mitarbeiters", - text_box=self.context['fullname']), Spacer(1, 4), BoxyLine( - width=100 + 155, - height=20, - text_label="Pers. Nr. (falls vorhanden)", - text_box=""), + text_box=self.context['department']), + Spacer(1, 4), + BoxyLine( + width=doc.width, + height=20, + text_label="Name des Mitarbeiters", + text_box=self.context['fullname']), + Spacer(1, 4), + BoxyLine( + width=100 + 155, + height=20, + text_label="Pers. Nr. (falls vorhanden)", + text_box=""), BoxyLine( x=196, y=+5, @@ -259,19 +274,21 @@ def print_shifts(self): i = 0 for i, shift in enumerate(shifts): # Not sure why, but timezone.localtime() is not working here. - # Instead timezone.template_localtime() is, so we're using it.. dum-dee-doo.. + # Instead timezone.template_localtime() is, so we're using it b1_date = timezone.template_localtime( - shift.shift_started).strftime('%d.%m.%Y') # e.g. 24.12.2016 + shift.shift_started).strftime('%d.%m.%Y') # e.g. 24.12.2016 b2_start = timezone.template_localtime( - shift.shift_started).strftime("%H:%M") # e.g. 08:15 - b3_pause = shift.pause_start_end # e.g. 08:15 - 15:55 + shift.shift_started).strftime("%H:%M") # e.g. 08:15 + b3_pause = shift.pause_start_end # e.g. 08:15 - 15:55 b4_end = timezone.template_localtime( - shift.shift_finished).strftime("%H:%M") # e.g. 15:55 - b5_total = format_dttd(shift.shift_duration, "%H:%M") # e.g. 07:40 - b6_cmnt = shift.key # e.g. "K" or "U" - - # We want every cell content to be an own paragraph, so we can give it a certain style. - # As always there is probably some other smart solution, but this works. + shift.shift_finished).strftime("%H:%M") # e.g. 15:55 + b5_total = format_dttd(shift.shift_duration, + "%H:%M") # e.g. 07:40 + b6_cmnt = shift.key # e.g. "K" or "U" + + # We want every cell content to be an own paragraph, so we can give + # it a certain style. As always there is probably some other smart + # solution, but this works. body_row = [] body_cells = [ b1_date, b2_start, b3_pause, b4_end, b5_total, b6_cmnt @@ -280,7 +297,8 @@ def print_shifts(self): body_row.append(Paragraph(cell, styles['NormalCenteredText'])) table_data.append(body_row) - # Append new empty lines while we haven't reached the full page capacity! + # Append new empty lines while we haven't reached the full page + # capacity! if i < 18: f = i while f < 19: @@ -293,7 +311,8 @@ def print_shifts(self): self.context['total_shift_duration'], "%H:%M") table_data.append(['', '', '', 'Summe:', total_shift_duration, '']) - # Create the table. Column width are set to fit the current data correctly. + # Create the table. Column width are set to fit the current data + # correctly. shift_table = Table( table_data, colWidths=(22.5 * mm, 22.5 * mm, 27.5 * mm, 22.5 * mm, 45 * mm, @@ -303,9 +322,9 @@ def print_shifts(self): ('INNERGRID', (0, 0), (-1, -2), 0.25, colors.black), ('BOX', (0, 0), (-1, -2), 0.25, colors.black), ('INNERGRID', (3, -1), (4, -1), 0.25, colors.black - ), # Custom grid for last row + ), # Custom grid for last row ('BOX', (3, -1), (4, -1), 0.25, colors.black - ), # Custom border for last row + ), # Custom border for last row ('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), diff --git a/clock/exports/urls.py b/clock/exports/urls.py index 30a3652..45e4324 100755 --- a/clock/exports/urls.py +++ b/clock/exports/urls.py @@ -3,7 +3,11 @@ from django.conf.urls import url -from clock.exports.views import ExportContractMonthAPI, ExportMonth, ExportMonthAPI +from clock.exports.views import ( + ExportContractMonthAPI, + ExportMonth, + ExportMonthAPI, +) urlpatterns = [ # Export URLs diff --git a/clock/exports/views.py b/clock/exports/views.py index c0513c9..9e25e55 100755 --- a/clock/exports/views.py +++ b/clock/exports/views.py @@ -1,12 +1,9 @@ # -*- coding: utf-8 -*- -from datetime import datetime, timedelta +from datetime import timedelta from braces.views import JSONResponseMixin from django.contrib.auth.decorators import login_required -from django.http import HttpResponseBadRequest -from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator -from django.utils.translation import ugettext_lazy as _ from django.views.generic.dates import MonthArchiveView from clock.contracts.models import Contract @@ -66,12 +63,12 @@ def get(self, request, *args, **kwargs): context_dict = ['No shifts available for this given query.'] else: context_dict = [{ - u"employee": shift.employee.username, - u"contract": shift.contract_or_none, - u"shift_started": shift.shift_started, - u"shift_finished": shift.shift_finished, - u"pause_duration": shift.pause_duration, - u"shift_duration": shift.shift_duration + "employee": shift.employee.username, + "contract": shift.contract_or_none, + "shift_started": shift.shift_started, + "shift_finished": shift.shift_finished, + "pause_duration": shift.pause_duration, + "shift_duration": shift.shift_duration } for shift in self.object] return self.render_json_response(context_dict) diff --git a/clock/pages/middleware.py b/clock/pages/middleware.py index e8a6a61..0f14311 100755 --- a/clock/pages/middleware.py +++ b/clock/pages/middleware.py @@ -2,7 +2,7 @@ try: from django.utils.deprecation import MiddlewareMixin -except ImportError: # Django < 1.10 +except ImportError: # Django < 1.10 # Works perfectly for everyone using MIDDLEWARE_CLASSES MiddlewareMixin = object @@ -19,7 +19,8 @@ def process_view(self, request, view_func, view_args, view_kwargs): # Added a check whether we're visiting a UpdateView right now. This # will now redirect to the old ListView, as otherwise the kwargs # would be overwritten and we'd be redirected to the default one. - if request.session['currently_visiting'] != request_path and 'delete' not in request_view_name: + if request.session['currently_visiting'] != request_path and ( + 'delete' not in request_view_name): request.session['last_visited'] = request.session[ 'currently_visiting'] request.session['last_kwargs'] = request.session[ @@ -31,7 +32,8 @@ def process_view(self, request, view_func, view_args, view_kwargs): except KeyError: pass - # We're sometimes calling the view with init-kwargs, instead of passing them as GET parameters + # We're sometimes calling the view with init-kwargs, instead of passing + # them as GET parameters if not request_kwargs: try: request_kwargs = view_func.__dict__['view_initkwargs'] diff --git a/clock/pages/mixins.py b/clock/pages/mixins.py index d1dbbf1..0e36308 100755 --- a/clock/pages/mixins.py +++ b/clock/pages/mixins.py @@ -3,8 +3,8 @@ class UserObjectOwnerMixin(SingleObjectMixin): - """ - Overrides SingleObjectMixin and checks if the user actually created the requested object. + """Overrides SingleObjectMixin and checks if the user actually created the + requested object. """ def get_object(self, queryset=None): diff --git a/clock/pages/utils.py b/clock/pages/utils.py index 66c160b..a16189a 100755 --- a/clock/pages/utils.py +++ b/clock/pages/utils.py @@ -7,11 +7,16 @@ def round_time(dt=None, obj=None, date_delta=timedelta(minutes=5), to='average'): - """ - Round a datetime object to a multiple of a timedelta + """Round a datetime object to a multiple of a timedelta + dt : datetime.datetime object, default now. - dateDelta : timedelta object, we round to a multiple of this, default 5 minute. - from: http://stackoverflow.com/questions/3463930/how-to-round-the-minute-of-a-datetime-object-python + + dateDelta : timedelta object, we round to a multiple of this, default 5 + minute. + + from: + http://stackoverflow.com/questions/3463930/how-to-round-the-minute-of-a-datetime-object-python + """ round_to = date_delta.total_seconds() @@ -27,7 +32,6 @@ def round_time(dt=None, seconds = (dt - tzmin).seconds if to == 'up': - # // is a floor division, not a comment on following line (like in javascript): rounding = (seconds + round_to) // round_to * round_to elif to == 'down': rounding = seconds // round_to * round_to diff --git a/clock/pages/views.py b/clock/pages/views.py index 48b7074..2bd2208 100755 --- a/clock/pages/views.py +++ b/clock/pages/views.py @@ -4,8 +4,12 @@ from clock.contracts.models import Contract from clock.shifts.forms import QuickActionForm -from clock.shifts.utils import get_all_contracts, get_current_shift, \ - get_default_contract, get_last_shifts, get_all_shifts +from clock.shifts.utils import ( + get_all_contracts, + get_current_shift, + get_default_contract, + get_last_shifts, +) def home(request): diff --git a/clock/profiles/forms.py b/clock/profiles/forms.py index 2800e79..55ba111 100755 --- a/clock/profiles/forms.py +++ b/clock/profiles/forms.py @@ -11,7 +11,9 @@ class UpdateUserForm(forms.ModelForm): class Meta: model = User - fields = ('first_name', 'last_name', ) + fields = ( + 'first_name', + 'last_name', ) def __init__(self, *args, **kwargs): super(UpdateUserForm, self).__init__(*args, **kwargs) @@ -24,12 +26,13 @@ def __init__(self, *args, **kwargs): def clean(self): super(UpdateUserForm, self).clean() + first = self.cleaned_data['first_name'] + last = self.cleaned_data['last_name'] - if self.cleaned_data['first_name'] and not self.cleaned_data['last_name'] \ - or not self.cleaned_data['first_name'] and self.cleaned_data['last_name']: + if (first and not last) or (not first and last): raise ValidationError( - _('When specifying your real name, you must give both your first and last name.' - )) + _('When specifying your real name, you must give both your ' + 'first and last name.')) class DeleteUserForm(forms.Form): diff --git a/clock/profiles/middleware.py b/clock/profiles/middleware.py index 852a32e..e9e4964 100755 --- a/clock/profiles/middleware.py +++ b/clock/profiles/middleware.py @@ -6,10 +6,12 @@ class LocaleMiddlewareExtended(LocaleMiddleware): - """ - This middleware extends Djangos normal LocaleMiddleware and looks for the language preferred by the user. - Normally only the current session is searched for the preferred language, but the user may want to define it in his - profile. This solves the problem and therefore keeps the set language across logouts/different devices. + """This middleware extends Djangos normal LocaleMiddleware and looks for the + language preferred by the user. + + Normally only the current session is searched for the preferred language, + but the user may want to define it in his profile. This solves the problem + and therefore keeps the set language across logouts/different devices. """ def get_language_for_user(self, request): diff --git a/clock/profiles/views.py b/clock/profiles/views.py index f7a7c0e..e8b04e2 100755 --- a/clock/profiles/views.py +++ b/clock/profiles/views.py @@ -3,17 +3,17 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from django.core.urlresolvers import translate_url, reverse_lazy -from django.shortcuts import render, redirect +from django.core.urlresolvers import reverse_lazy, translate_url +from django.shortcuts import redirect, render from django.utils.http import is_safe_url -from django.utils.translation import LANGUAGE_SESSION_KEY, check_for_language from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import LANGUAGE_SESSION_KEY, check_for_language from django.views.generic import TemplateView from django.views.generic.edit import UpdateView -from clock.users.models import User -from clock.profiles.forms import UpdateUserForm, DeleteUserForm +from clock.profiles.forms import DeleteUserForm, UpdateUserForm from clock.profiles.models import UserProfile +from clock.users.models import User LANGUAGE_QUERY_PARAMETER = 'language' @@ -35,13 +35,15 @@ def get_object(self): @login_required() def update_language(request): - """ - This is a very lazy solution overwriting django.views.i18n.set_lang. We're basically just adding another option - to save the selected language into the database, besides the session. This way we can retrieve the language - preference across different devices and after a longer period of time, when the user was not logged in. - - It's possible, that there is a nifty trick on how to make this whole thing shorter and not having to copy the whole - function. But this seems to work so far. + """This is a very lazy solution overwriting django.views.i18n.set_lang. We're + basically just adding another option to save the selected language into the + database, besides the session. This way we can retrieve the language + preference across different devices and after a longer period of time, when + the user was not logged in. + + It's possible, that there is a nifty trick on how to make this whole thing + shorter and not having to copy the whole function. But this seems to work + so far. """ next = request.POST.get('next', request.GET.get('next')) if not is_safe_url(url=next, host=request.get_host()): @@ -78,8 +80,9 @@ def delete_user(request): template_name = 'profiles/delete.html' context = { 'text': - _('

      Are you sure you want to delete this profile? Please type in your username ' - '%s to confirm.

      ') % request.user + _('

      Are you sure you want to delete this profile? Please type in ' + 'your username {} to confirm.

      ').format( + request.user) } if request.method == 'POST': diff --git a/clock/shifts/forms.py b/clock/shifts/forms.py index 24b9a22..bfd59a7 100755 --- a/clock/shifts/forms.py +++ b/clock/shifts/forms.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper -from crispy_forms.layout import HTML, Field, Fieldset, Layout, Submit +from crispy_forms.layout import HTML, Field, Layout, Submit from django import forms from django.conf import settings from django.core.exceptions import ValidationError @@ -71,7 +71,8 @@ def __init__(self, *args, **kwargs): if not self.fields['contract'].queryset: self.fields['contract'].widget.attrs['disabled'] = True - # Set the delete input to be empty. If we are not on an update page, the button will not be shown! + # Set the delete input to be empty. If we are not on an update page, + # the button will not be shown! delete_html_inject = "" # Are we creating a new shift or updating an existing one? @@ -79,17 +80,12 @@ def __init__(self, *args, **kwargs): add_input_text = _('Create new shift') elif self.view == 'shift_update': add_input_text = _('Update') - delete_html_inject = u' \ - %(delete_translation)s' % { - 'delete_url': + delete_html_inject = '{}'.format( reverse_lazy('shift:delete', kwargs={'pk': self.instance.pk}), - 'delete_translation': - _('Delete') - } + 'btn btn-danger pull-right second-button', _('Delete')) - cancel_html_inject = '%(cancel_translation)s' % \ - {'cancel_url': get_return_url(self.request, 'shift:list'), - 'cancel_translation': _('Cancel')} + cancel_html = '{}'.format( + get_return_url(self.request, 'shift:list'), _('Cancel')) self.helper = FormHelper(self) self.helper.form_action = '.' @@ -107,7 +103,7 @@ def __init__(self, *args, **kwargs): Field('contract'), Field('key'), Field('tags'), Field('note')) self.helper.layout.append( FormActions( - HTML(cancel_html_inject), + HTML(cancel_html), Submit( 'submit', add_input_text, @@ -134,8 +130,9 @@ def clean(self): employee, shift_started, shift_finished) if check_for_overlaps: raise ValidationError( - _('Your selected starting/finishing time overlaps with at least one\ - finished shift of yours. Please adjust the times.')) + _('Your selected starting/finishing time overlaps with at ' + 'least one finished shift of yours. ' + 'Please adjust the times.')) if (shift_finished - shift_started) < pause_duration: raise ValidationError( @@ -159,11 +156,13 @@ def check_for_overlaps(self, employee, shift_started, shift_finished): shift_started__lte=shift_finished, shift_finished__gte=shift_started) - # Check if the retrieved shifts contain the shift we're trying to update. If yes: pass. + # Check if the retrieved shifts contain the shift we're trying to + # update. for shift in shifts: if shift.pk == self.instance.pk: pass - elif shift.shift_finished == shift_started or shift.shift_started == shift_finished: + elif (shift.shift_finished == shift_started) or ( + shift.shift_started == shift_finished): pass else: return shifts diff --git a/clock/shifts/models.py b/clock/shifts/models.py index 3ddc2c6..3f4a691 100755 --- a/clock/shifts/models.py +++ b/clock/shifts/models.py @@ -70,21 +70,27 @@ def clean(self, *args, **kwargs): self.shift_time_validation() def save(self, *args, **kwargs): - """ - If either the shift_finished or shift_started values were changed, + """If either the shift_finished or shift_started values were changed, then we'll calculate the shift_duration. Also substract the pause_duration while we're at it. This accounts for both the quick-action buttons and manual edits in the admin-backend or dashboard-frontend. - """ - """ - The following code block does some rounding to the start and end times of the shifts. - It does it as following and only if the shift is newly added: - 1) Round shift_started, shift_finished and pause_duration by 5 or 1 minute(s), respectively. - 2) The shift_finished is set to the most logic (up/down) value. Now check if it is the same as the - shift_started value (if shift_started and shift_finished were set to the same value). In this case, add 5 - minutes to the shift_finished value. - 3) If shift_started is somehow bigger than shift_finished, set shift_finished to be 5 minutes bigger. + + The following code block does some rounding to the start and end times + of the shifts. It does it as following and only if the shift is newly + added: + + 1) Round shift_started, shift_finished and pause_duration by 5 or 1 + minute(s), respectively. + + 2) The shift_finished is set to the most logic (up/down) value. Now + check if it is the same as the shift_started value (if + shift_started and shift_finished were set to the same value). In + this case, add 5 minutes to the shift_finished value. + + 3) If shift_started is somehow bigger than shift_finished, set + shift_finished to be 5 minutes bigger. + """ if self.bool_finished is True: self.shift_started = round_time(self.shift_started) @@ -94,14 +100,10 @@ def save(self, *args, **kwargs): # we will reset the former self.pause_duration = round_time( dt=self.pause_duration, date_delta=timedelta(minutes=5)) - # if self.current_duration > (timezone.now() - self.shift_started): - # self.pause_duration = round_time( - # self.pause_duration, timedelta(minutes=1)) - # else: - # self.pause_duration = timedelta(minutes=0) - - # account for the case that a user pauses his shift longer than he actually worked. This will make sure - # the shift duration is always longer than the pause duration by 5 minutes. + + # account for the case that a user pauses his shift longer than he + # actually worked. This will make sure the shift duration is always + # longer than the pause duration by 5 minutes. if self.total_pause_time == ( self.shift_finished - self.shift_started): self.shift_finished += timedelta(minutes=5) @@ -118,7 +120,8 @@ def save(self, *args, **kwargs): or self.pause_duration != self.__old_pause_duration): self.shift_duration = ( self.shift_finished - self.shift_started) - self.pause_duration - # Lets check if this shift did not exists before and was just added from the shell! + # Lets check if this shift did not exists before and was just added + # from the shell! elif self.pk is None and self.shift_finished is not None: self.shift_duration = ( self.shift_finished - self.shift_started) - self.pause_duration @@ -133,19 +136,14 @@ def shift_time_validation(self): errors = {} if self.shift_started and self.shift_finished: if self.shift_started > timezone.now(): - errors['shift_started'] = _('Your shift must not start in the \ - future!') + errors['shift_started'] = _('Your shift must not start ' + 'in the future!') if self.shift_finished > timezone.now(): - errors['shift_finished'] = _('Your shift must not finish in \ - the future!') + errors['shift_finished'] = _('Your shift must not finish ' + 'in the future!') if self.shift_finished < self.shift_started: - errors['shift_finished'] = _('A shift must not finish, before \ - it has even started!') - - # if (self.shift_finished - self.shift_started) > \ - # timedelta(hours=6): - # errors['shift_finished'] = _('Your shift may not be \ - # longer than 6 hours.') + errors['shift_finished'] = _('A shift must not finish, before ' + 'it has even started!') if errors: raise ValidationError(errors) @@ -176,16 +174,19 @@ def current_duration(self): def pause_start_end(self): if self.pause_duration.total_seconds() > 0: pause_begin = self.shift_finished - self.pause_duration - return time.strftime("%H:%M", pause_begin.utctimetuple()) + " - " + \ - time.strftime("%H:%M", self.shift_finished.utctimetuple()) + first = time.strftime("%H:%M", pause_begin.utctimetuple()) + last = time.strftime("%H:%M", self.shift_finished.utctimetuple()) + return '{} - {}'.format(first, last) + return "-" @property def contract_or_none(self): + """Returns the name of the contract connected to the shift or a string + containing "None". :return: Contract name or "None"-string + """ - Returns the name of the contract connected to the shift or a string containing "None". - :return: Contract name or "None"-string - """ + # TODO: This is awful if self.contract is None: return "None" return self.contract.department diff --git a/clock/shifts/utils.py b/clock/shifts/utils.py index 2b8384d..5bd5e19 100755 --- a/clock/shifts/utils.py +++ b/clock/shifts/utils.py @@ -7,13 +7,19 @@ def get_return_url(request, default_success): - """ - Checks whether the user should be returned to the default_success view or to a special one. Is mostly used for the - shift list views, as they can get filtered by month/year and contract ID. After updating/adding one, the user should - be redirected to either: + """Checks whether the user should be returned to the default_success view or to + a special one. Is mostly used for the shift list views, as they can get + filtered by month/year and contract ID. After updating/adding one, the user + should be redirected to either: + 1) The standard shift list view (if no filters were specified) - 2) The previous visited view (if he updated / created a shift in the same month / contract) - 3) A filtered view which corresponds to the date/contract of the updated/added shift + + 2) The previous visited view (if he updated / created a shift in the + same month / contract) + + 3) A filtered view which corresponds to the date/contract of the + updated/added shift + """ try: last_visited = "shift" in request.session['last_visited'] @@ -51,11 +57,14 @@ def get_return_url(request, default_success): def set_correct_session(request, k): - """ - Method to correctly set the 'last_kwargs' session key, so we can use MonthView filtering. + """Method to correctly set the 'last_kwargs' session key, so we can use + MonthView filtering. + :param request: request object :param k: Key for session + :return: int 00, datetime or None + """ try: return request.session['last_kwargs'][k] diff --git a/clock/shifts/views.py b/clock/shifts/views.py index 2591b17..574ed8f 100755 --- a/clock/shifts/views.py +++ b/clock/shifts/views.py @@ -1,4 +1,4 @@ -from datetime import date, datetime +from datetime import datetime from django.conf import settings from django.contrib import messages @@ -6,7 +6,6 @@ from django.shortcuts import redirect from django.utils import timezone from django.utils.decorators import method_decorator -from django.utils.timezone import activate from django.utils.translation import ugettext_lazy as _ from django.views.decorators.http import require_POST from django.views.generic.dates import ( @@ -247,26 +246,15 @@ def get_year(self): return year def get_month(self): - """Returns the current month if none was specified inside the kwargs.""" + """ + Returns the current month if none was specified inside the kwargs. + """ if 'month' not in self.kwargs: month = timezone.now().strftime("%m") else: month = super(ShiftMonthView, self).get_month() return month - @property - def prev_next_shift(self): - context = {} - try: - month = int(self.kwargs['month']) - year = int(self.kwargs['year']) - except KeyError: - d = date.today() - month = d.month - year = d.year - - return context - def get_queryset(self): return Shift.objects.filter( employee=self.request.user, shift_finished__isnull=False) diff --git a/clock/templates/account/email_confirm.html b/clock/templates/account/email_confirm.html index 483d7eb..77740c0 100755 --- a/clock/templates/account/email_confirm.html +++ b/clock/templates/account/email_confirm.html @@ -13,8 +13,7 @@

      {% trans 'Confirm your e-mail address' %}

      -

      {% blocktrans with confirmation.email_address.email as email %}Please confirm that - {{ email }} is your e-mail address.{% endblocktrans %}

      +

      {% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is your e-mail address.{% endblocktrans %}

      {% csrf_token %} @@ -25,8 +24,7 @@

      {% trans 'Confirm your e-mail address' %}

      {% url 'account_email' as email_url %} -

      {% blocktrans %}This e-mail confirmation link expired or is invalid. Please - issue a new e-mail confirmation request.{% endblocktrans %}

      +

      {% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktrans %}

      {% endif %}
      diff --git a/clock/templates/account/email_confirmed.html b/clock/templates/account/email_confirmed.html index 6a7df6c..b1b1eaa 100755 --- a/clock/templates/account/email_confirmed.html +++ b/clock/templates/account/email_confirmed.html @@ -11,8 +11,7 @@

      {% trans 'Confirm your e-mail address' %}

      -

      {% blocktrans with confirmation.email_address.email as email %}Please confirm that - {{ email }} is your e-mail address.{% endblocktrans %}

      +

      {% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is your e-mail address.{% endblocktrans %}

      {% csrf_token %} @@ -23,8 +22,7 @@

      {% trans 'Confirm your e-mail address' %}

      {% url 'account_email' as email_url %} -

      {% blocktrans with email_address.email as email %}You have confirmed that - {{ email }} is your e-mail address.{% endblocktrans %}

      +

      {% blocktrans with email_address.email as email %}You have confirmed that {{ email }} is your e-mail address.{% endblocktrans %}

      {% endif %}
      diff --git a/clock/templates/account/password_reset_done.html b/clock/templates/account/password_reset_done.html index 908370c..1879710 100755 --- a/clock/templates/account/password_reset_done.html +++ b/clock/templates/account/password_reset_done.html @@ -15,6 +15,5 @@

      {% trans 'Password reset link sent' %}

      {% include "account/snippets/already_logged_in.html" %} {% endif %} -

      {% blocktrans %}We have sent you an e-mail. Please contact us if you do not - receive it within a few minutes.{% endblocktrans %}

      +

      {% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

      {% endblock container %} diff --git a/clock/templates/account/verification_sent.html b/clock/templates/account/verification_sent.html index 170d50d..4deb859 100755 --- a/clock/templates/account/verification_sent.html +++ b/clock/templates/account/verification_sent.html @@ -11,9 +11,7 @@

      {% trans 'Confirmation email sent.' %}

      -

      {% blocktrans %}We have sent an e-mail to {{ email }} for verification. - Follow the link provided to finalize the signup process. Please contact us if you do not receive it - within a few minutes.{% endblocktrans %}

      +

      {% blocktrans %}We have sent an e-mail to {{ email }} for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

      {% endblock container %} diff --git a/clock/templates/account/verified_email_required.html b/clock/templates/account/verified_email_required.html index d075be9..770dc1d 100755 --- a/clock/templates/account/verified_email_required.html +++ b/clock/templates/account/verified_email_required.html @@ -13,14 +13,9 @@

      {% trans 'Restricted access' %}

      {% url 'account_email' as email_url %} -

      {% blocktrans %}This part of the site requires us to verify that - you are who you claim to be. For this purpose, we require that you - verify ownership of your e-mail address. {% endblocktrans %}

      +

      {% blocktrans %}This part of the site requires us to verify that you are who you claim to be. For this purpose, we require that you verify ownership of your e-mail address. {% endblocktrans %}

      -

      {% blocktrans %}We have sent an e-mail to you for - verification. Please click on the link inside this e-mail. Please - contact us if you do not receive it within a few - minutes.{% endblocktrans %}

      +

      {% blocktrans %}We have sent an e-mail to you for verification. Please click on the link inside this e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

      {% blocktrans %}Note: you can still change your e-mail address.{% endblocktrans %}

      diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 02fc1a9..d8113fb 100755 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -1,23 +1,18 @@ -# Clock German Language Messages -# Copyright (C) 2015-2016 Michael Gecht -# This file is distributed under the same license as the django-clock package. -# Michael Gecht mgecht@stud.uni-frankfurt.de, 2016. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-11-05 16:57+0100\n" -"PO-Revision-Date: 2016-12-05 17:07+0053\n" -"Last-Translator: Michael Gecht \n" +"POT-Creation-Date: 2017-11-11 14:20+0100\n" +"PO-Revision-Date: 2017-11-11 14:21+0053\n" +"Last-Translator: b'M G '\n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Translated-Using: django-rosetta 0.7.12\n" +"X-Translated-Using: django-rosetta 0.7.13\n" msgid "Name" msgstr "Name" @@ -46,8 +41,12 @@ msgstr "Verträge können nicht mehr als 80 Stunden im Monat umfassen!" msgid "Your total work time must be bigger than zero!" msgstr "Deine monatliche Arbeitszeit sollte zumindest eine Minute betragen!" -msgid "Please specify your working hours in the format HH:MM (eg. 12:15 - meaning 12 hours and 15 minutes)" -msgstr "Bitte gib deine Arbeitszeit im Format HH:MM an (z.B. 12:15, also 12 Stunden und 15 Minuten)." +msgid "" +"Please specify your working hours in the format HH:MM (eg. 12:15 - meaning " +"12 hours and 15 minutes)" +msgstr "" +"Bitte gib deine Arbeitszeit im Format HH:MM an (z.B. 12:15, also 12 Stunden " +"und 15 Minuten)." msgid "Create new contract" msgstr "Neuen Vertrag erstellen" @@ -70,14 +69,20 @@ msgstr "Kürzel" msgid "Contract" msgstr "Vertrag" +msgid "Could not split the value you provided." +msgstr "Der übergebene Wert konnte nicht verarbeitet werden." + msgid "None defined" msgstr "Ohne Vertrag" msgid "Update" msgstr "Schicht aktualisieren" -msgid "When specifying your real name, you must give both your first and last name." -msgstr "Wenn du deinen realen Namen angibst, musst du sowohl deinen Vor- als auch deinen Nachnamen eintragen." +msgid "" +"When specifying your real name, you must give both your first and last name." +msgstr "" +"Wenn du deinen realen Namen angibst, musst du sowohl deinen Vor- als auch " +"deinen Nachnamen eintragen." msgid "Username" msgstr "Benutzername" @@ -91,15 +96,22 @@ msgstr "Vor- und Nachname" msgid "Site language" msgstr "Sprache" -#, python-format -msgid "

      Are you sure you want to delete this profile? Please type in your username %s to confirm.

      " -msgstr "

      Falls du dein Profil wirklich löschen willst, dann tippe deinen Benutzernamen %s ein um dies zu bestätigen.

      " +msgid "" +"

      Are you sure you want to delete this profile? Please type in your " +"username {} to confirm.

      " +msgstr "" +"

      Falls du dein Profil wirklich löschen willst, dann tippe deinen " +"Benutzernamen {} ein um dies zu bestätigen.

      " msgid "Create new shift" msgstr "Neue Schicht erstellen" -msgid "Your selected starting/finishing time overlaps with at least one finished shift of yours. Please adjust the times." -msgstr "Deine Start-/Endzeit überlappt mit einer bereits beendeten Schicht. Bitte passe die Zeiten an." +msgid "" +"Your selected starting/finishing time overlaps with at least one finished " +"shift of yours. Please adjust the times." +msgstr "" +"Deine Start-/Endzeit überlappt mit einer bereits beendeten Schicht. Bitte " +"passe die Zeiten an." msgid "A pause may not be longer than your actual shift." msgstr "Eine Pause kann nicht länger als eine Schicht sein." @@ -137,13 +149,13 @@ msgstr "Schlüssel" msgid "Note" msgstr "Notiz" -msgid "Your shift must not start in the future!" +msgid "Your shift must not start in the future!" msgstr "Ihre Schicht darf nicht in der Zukunft beginnen!" -msgid "Your shift must not finish in the future!" +msgid "Your shift must not finish in the future!" msgstr "Ihre Schicht darf nicht in der Zukunft enden!" -msgid "A shift must not finish, before it has even started!" +msgid "A shift must not finish, before it has even started!" msgstr "Eine Schicht darf nicht enden, bevor sie überhaupt begonnen hat!" msgid "Shift currently paused?" @@ -210,8 +222,14 @@ msgstr "Entfernen" msgid "Warning:" msgstr "Warnung:" -msgid "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." -msgstr "Du hast aktuell keine E-Mail Adresse eingerichtet. Du solltest unbedingt eine eintragen damit du Benachrichtigungen erhältst und dein Passwort zurücksetzen kannst." +msgid "" +"You currently do not have any e-mail address set up. You should really add " +"an e-mail address so you can receive notifications, reset your password, " +"etc." +msgstr "" +"Du hast aktuell keine E-Mail Adresse eingerichtet. Du solltest unbedingt " +"eine eintragen damit du Benachrichtigungen erhältst und dein Passwort " +"zurücksetzen kannst." msgid "Add E-mail Address" msgstr "E-Mail-Adresse hinzufügen" @@ -230,24 +248,30 @@ msgstr "Bestätige deine E-Mail Adresse" #, python-format msgid "" -"Please confirm that\n" -" %(email)s is your e-mail address." -msgstr "Bitte bestätige, dass %(email)s deine E-Mail ist." +"Please confirm that %(email)s is your " +"e-mail address." +msgstr "" +"Bitte bestätige, dass %(email)s deine " +"E-Mail ist." msgid "Confirm" msgstr "Bestätigen" #, python-format msgid "" -"This e-mail confirmation link expired or is invalid. Please\n" -" issue a new e-mail confirmation request." -msgstr "Der Bestätigungslink ist abgelaufen oder ist ungültig. Bitte beantrage einen neuen Bestätigungslink per E-Mail." +"This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request." +msgstr "" +"Der Bestätigungslink ist abgelaufen oder ist ungültig. Bitte beantrage einen neuen Bestätigungslink per E-Mail." #, python-format msgid "" -"You have confirmed that\n" -" %(email)s is your e-mail address." -msgstr "Du hast bestätigt, dass %(email)s deine E-Mail ist." +"You have confirmed that %(email)s is your " +"e-mail address." +msgstr "" +"Du hast bestätigt, dass %(email)s deine " +"E-Mail ist." msgid "Login" msgstr "Login" @@ -267,8 +291,12 @@ msgstr "Sicher, dass du dich abmelden möchtest?" msgid "Reset password" msgstr "Passwort zurücksetzen" -msgid "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." -msgstr "Passwort vergessen? Gib einfach Deine E-Mail-Adresse ein und wir senden Dir eine E-Mail zu, mit der Du das Passwort zurücksetzen kannst." +msgid "" +"Forgotten your password? Enter your e-mail address below, and we'll send you" +" an e-mail allowing you to reset it." +msgstr "" +"Passwort vergessen? Gib einfach Deine E-Mail-Adresse ein und wir senden Dir" +" eine E-Mail zu, mit der Du das Passwort zurücksetzen kannst." msgid "Reset My Password" msgstr "Setze mein Passwort zurück" @@ -277,23 +305,33 @@ msgstr "Setze mein Passwort zurück" msgid "" "Please contact us if you have any\n" " trouble resetting your password." -msgstr "Bitte kontaktiere uns, falls du irgendwelche Probleme beim Zurücksetzen deines Passworts hast." +msgstr "" +"Bitte kontaktiere uns, falls du irgendwelche" +" Probleme beim Zurücksetzen deines Passworts hast." msgid "Password reset link sent" msgstr "Passwort Link verschickt" #, python-format msgid "" -"We have sent you an e-mail. Please contact us if you do not\n" -" receive it within a few minutes." -msgstr "Wir haben dir eine E-Mail geschickt. Bitte kontaktiere uns, falls du sie nicht innerhalb der nächsten Minuten erhälst." +"We have sent you an e-mail. Please contact " +"us if you do not receive it within a few minutes." +msgstr "" +"Wir haben dir eine E-Mail geschickt. Bitte kontaktiere uns, falls du sie nicht innerhalb " +"der nächsten Minuten erhälst." msgid "Wrong token" msgstr "Falscher Verifikationscode" #, python-format -msgid "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." -msgstr "Der Link zum Zurücksetzen des Passworts war ungültig. Neues Passwort beantragen." +msgid "" +"The password reset link was invalid, possibly because it has already been " +"used. Please request a new password " +"reset." +msgstr "" +"Der Link zum Zurücksetzen des Passworts war ungültig. Neues Passwort beantragen." msgid "Password reset" msgstr "Passwort zurücksetzen" @@ -322,20 +360,36 @@ msgstr "Anmelden" msgid "Completey free" msgstr "Vollständig kostenlos." -msgid "Just look at it as a gift. Or maybe Michael is stinking-rich and does not know how to make a profit out of this page." -msgstr "Sieh es als eine Art Dankeschön von uns an. Oder Michael ist einfach stinkendreich und weiß nicht wie er so ein Produkt monetarisieren soll." +msgid "" +"Just look at it as a gift. Or maybe Michael is stinking-rich and does not " +"know how to make a profit out of this page." +msgstr "" +"Sieh es als eine Art Dankeschön von uns an. Oder Michael ist einfach " +"stinkendreich und weiß nicht wie er so ein Produkt monetarisieren soll." msgid "No obligatons" msgstr "Keine Verpflichtungen." msgid "Do not want to track your work time? Fine. We do not force you to." -msgstr "Keine Lust deine Arbeitszeiten zu pflegen? Kein Problem, wir zwingen dich zu nichts." +msgstr "" +"Keine Lust deine Arbeitszeiten zu pflegen? Kein Problem, wir zwingen dich zu" +" nichts." msgid "Do you want more?" msgstr "Fehlt dir etwas?" -msgid "The whole code is available under the MIT-License and can be found on GitHub. You can join the development!" -msgstr "Der gesamte Code der Plattform ist innerhalb der MIT Lizenz auf GitHub veröffentlicht. Du kannst gerne mitmachen und das Projekt erweitern!" +msgid "" +"The whole code is available under the MIT-License and can be found on GitHub. You can join the development!" +msgstr "" +"Der gesamte Code der Plattform ist innerhalb der MIT Lizenz auf GitHub veröffentlicht. Du" +" kannst gerne mitmachen und das Projekt erweitern!" msgid "Signup closed" msgstr "Anmeldung geschlossen" @@ -351,31 +405,42 @@ msgstr "E-Mail-Adresse bestätigen" #, python-format msgid "" -"We have sent an e-mail to %(email)s for verification.\n" -" Follow the link provided to finalize the signup process. Please contact us if you do not receive it\n" -" within a few minutes." -msgstr "Wir haben eine E-Mail zur überprüfung an %(email)s geschickt. Folge dem Link, um den Vorgang abzuschließen. Bitte kontaktiere uns, wenn Du innerhalb der nächsten Minuten keine E-Mail erhalten hast." +"We have sent an e-mail to %(email)s for " +"verification. Follow the link provided to finalize the signup process. " +"Please contact us if you do not receive it within a few minutes." +msgstr "" +"Wir haben eine E-Mail zur überprüfung an %(email)s geschickt. Folge dem Link, um den " +"Vorgang abzuschließen. Bitte kontaktiere uns, wenn Du innerhalb der nächsten" +" Minuten keine E-Mail erhalten hast." msgid "Restricted access" msgstr "Eingeschränkter Zugang" msgid "" -"This part of the site requires us to verify that\n" -" you are who you claim to be. For this purpose, we require that you\n" -" verify ownership of your e-mail address. " -msgstr "Diesen Bereich kannst du nur betreten, wenn du eine E-Mail bestätigt hast." +"This part of the site requires us to verify that you are who you claim to " +"be. For this purpose, we require that you verify ownership of your e-mail " +"address. " +msgstr "" +"Diesen Bereich kannst du nur betreten, wenn du eine E-Mail bestätigt hast." #, python-format msgid "" -"We have sent an e-mail to you for\n" -" verification. Please click on the link inside this e-mail. Please\n" -" contact us if you do not receive it within a few\n" -" minutes." -msgstr "Wir haben eine E-Mail zur Überprüfung geschickt. Bitte kontaktiere uns, wenn Du innerhalb der nächsten Minuten keine E-Mail erhalten hast." +"We have sent an e-mail to you for verification. Please click on the link " +"inside this e-mail. Please contact us if you" +" do not receive it within a few minutes." +msgstr "" +"Wir haben eine E-Mail zur Überprüfung geschickt. Bitte kontaktiere uns, wenn Du innerhalb der nächsten" +" Minuten keine E-Mail erhalten hast." #, python-format -msgid "Note: you can still change your e-mail address." -msgstr "Hinweis: Du kannst deine E-Mail Adressen immer noch ändern!" +msgid "" +"Note: you can still change your " +"e-mail address." +msgstr "" +"Hinweis: Du kannst deine E-Mail " +"Adressen immer noch ändern!" msgid "Back to top" msgstr "Nach oben" @@ -392,8 +457,12 @@ msgstr "Kontakt" msgid "Thanks for your message!" msgstr "Danke für die Nachricht!" -msgid "Your message was sent successfully. We will get back to you as soon as possible!" -msgstr "Deine Nachricht wurde erfolgreich gesendet. Wir werden uns so schnell wie möglich melden!" +msgid "" +"Your message was sent successfully. We will get back to you as soon as " +"possible!" +msgstr "" +"Deine Nachricht wurde erfolgreich gesendet. Wir werden uns so schnell wie " +"möglich melden!" msgid "Contracts" msgstr "Verträge" @@ -405,7 +474,8 @@ msgid "Are you sure you want to delete the following contract?" msgstr "Bist Du sicher, dass Du den Vertrag löschen möchtest?" msgid "All shifts associated with this contract will also be deleted!" -msgstr "Es werden alle Schichten gelöscht, die mit diesem Vertrag assoziiert sind!" +msgstr "" +"Es werden alle Schichten gelöscht, die mit diesem Vertrag assoziiert sind!" msgid "New contract" msgstr "Neuen Vertrag hinzufügen" @@ -529,17 +599,3 @@ msgstr "Englisch" msgid "A new message has arrived!" msgstr "Neue Nachricht erhalten!" - -#~ msgid "We can't export more than 80 hours per month!" -#~ msgstr "Ein Monat darf nicht mehr als 80 gearbeitete Stunden beinhalten! " - -#~| msgid "Delete profile" -#~ msgid "Update profile" -#~ msgstr "Aktualisieren" - -#~| msgid "Update contract" -#~ msgid "Update information" -#~ msgstr "Profil aktualisieren" - -#~ msgid "At some point you will see some information regarding your account here." -#~ msgstr "In der Zukunft werden hier weitere Informationen zu deinem Profil stehen." From a02d6d64c6acf775220e8da515bdc57f0b50d9b2 Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Sat, 11 Nov 2017 14:28:02 +0100 Subject: [PATCH 12/14] Update to Python 3 syntax --- clock/contact/urls.py | 2 +- clock/contracts/fields.py | 4 ++-- clock/contracts/migrations/0001_initial.py | 2 +- clock/contracts/migrations/0002_auto_20160606_1320.py | 2 +- clock/contracts/utils.py | 2 +- clock/contrib/sites/migrations/0001_initial.py | 2 +- .../contrib/sites/migrations/0002_set_site_domain_and_name.py | 2 +- clock/contrib/sites/migrations/0003_auto_20160606_1320.py | 2 +- clock/contrib/sites/migrations/0004_auto_20171022_1456.py | 2 +- clock/exports/urls.py | 2 +- clock/exports/views.py | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/clock/contact/urls.py b/clock/contact/urls.py index 9e183d0..1111495 100755 --- a/clock/contact/urls.py +++ b/clock/contact/urls.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.conf.urls import url diff --git a/clock/contracts/fields.py b/clock/contracts/fields.py index 6577891..b790916 100755 --- a/clock/contracts/fields.py +++ b/clock/contracts/fields.py @@ -40,7 +40,7 @@ def clean(self, value): value = super(CharField, self).clean(value) try: - hours, minutes = map(int, value.split(':')) + hours, minutes = list(map(int, value.split(':'))) except ValueError: try: hours = int(value) @@ -86,7 +86,7 @@ def to_python(self, value): # Split into two values and return the duration in minutes! if isinstance(value, str): try: - hours, minutes = map(int, value.split(':')) + hours, minutes = list(map(int, value.split(':'))) except ValueError: raise ValidationError( _('Working hours entered must be in format HH:MM')) diff --git a/clock/contracts/migrations/0001_initial.py b/clock/contracts/migrations/0001_initial.py index 9320875..78c31c5 100755 --- a/clock/contracts/migrations/0001_initial.py +++ b/clock/contracts/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations from django.conf import settings diff --git a/clock/contracts/migrations/0002_auto_20160606_1320.py b/clock/contracts/migrations/0002_auto_20160606_1320.py index c1f4a5d..a31b5ed 100755 --- a/clock/contracts/migrations/0002_auto_20160606_1320.py +++ b/clock/contracts/migrations/0002_auto_20160606_1320.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.9.5 on 2016-06-06 13:20 -from __future__ import unicode_literals + from django.db import migrations, models diff --git a/clock/contracts/utils.py b/clock/contracts/utils.py index 4e420df..eaec696 100755 --- a/clock/contracts/utils.py +++ b/clock/contracts/utils.py @@ -4,7 +4,7 @@ def convert_work_hours(work_hours): try: - hours, minutes = map(int, work_hours.split(':')) + hours, minutes = list(map(int, work_hours.split(':'))) except ValueError: raise ValidationError(_('Could not split the value you provided.')) return (hours * 60) + minutes diff --git a/clock/contrib/sites/migrations/0001_initial.py b/clock/contrib/sites/migrations/0001_initial.py index 555d02c..91c0f89 100755 --- a/clock/contrib/sites/migrations/0001_initial.py +++ b/clock/contrib/sites/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.db import models, migrations import django.contrib.sites.models diff --git a/clock/contrib/sites/migrations/0002_set_site_domain_and_name.py b/clock/contrib/sites/migrations/0002_set_site_domain_and_name.py index 6a9f0e8..35cdeee 100755 --- a/clock/contrib/sites/migrations/0002_set_site_domain_and_name.py +++ b/clock/contrib/sites/migrations/0002_set_site_domain_and_name.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.conf import settings from django.db import migrations diff --git a/clock/contrib/sites/migrations/0003_auto_20160606_1320.py b/clock/contrib/sites/migrations/0003_auto_20160606_1320.py index 5cf0932..e4acd0f 100755 --- a/clock/contrib/sites/migrations/0003_auto_20160606_1320.py +++ b/clock/contrib/sites/migrations/0003_auto_20160606_1320.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.9.5 on 2016-06-06 13:20 -from __future__ import unicode_literals + import django.contrib.sites.models from django.db import migrations, models diff --git a/clock/contrib/sites/migrations/0004_auto_20171022_1456.py b/clock/contrib/sites/migrations/0004_auto_20171022_1456.py index e08c48c..8f70ce4 100644 --- a/clock/contrib/sites/migrations/0004_auto_20171022_1456.py +++ b/clock/contrib/sites/migrations/0004_auto_20171022_1456.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.6 on 2017-10-22 14:56 -from __future__ import unicode_literals + import django.contrib.sites.models from django.db import migrations diff --git a/clock/exports/urls.py b/clock/exports/urls.py index 45e4324..3ec05ce 100755 --- a/clock/exports/urls.py +++ b/clock/exports/urls.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + from django.conf.urls import url diff --git a/clock/exports/views.py b/clock/exports/views.py index 9e25e55..bff78ab 100755 --- a/clock/exports/views.py +++ b/clock/exports/views.py @@ -50,7 +50,7 @@ def get_queryset(self): class ExportMonthClass(JSONResponseMixin, MonthArchiveView): model = Shift date_field = "shift_started" - json_dumps_kwargs = {u"indent": 2} + json_dumps_kwargs = {"indent": 2} json_encoder_class = ShiftJSONEncoder def get_queryset(self): From 53c22d3ce967ba5e9b27622d9e78f8c0e9542331 Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Sat, 11 Nov 2017 16:47:11 +0100 Subject: [PATCH 13/14] Fix unit tests --- clock/shifts/forms.py | 7 +- clock/shifts/tests/test_forms.py | 6 +- clock/shifts/tests/test_views.py | 107 +++++++++++++++++++++---------- 3 files changed, 79 insertions(+), 41 deletions(-) diff --git a/clock/shifts/forms.py b/clock/shifts/forms.py index bfd59a7..0c2c21b 100755 --- a/clock/shifts/forms.py +++ b/clock/shifts/forms.py @@ -6,7 +6,6 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse_lazy -from django.forms import DateTimeField from django.utils.translation import ugettext_lazy as _ from clock.contracts.models import Contract @@ -32,9 +31,9 @@ def __init__(self, *args, **kwargs): class ShiftForm(forms.ModelForm): - shift_started = DateTimeField( + shift_started = forms.DateTimeField( input_formats=settings.DATETIME_INPUT_FORMATS) - shift_finished = DateTimeField( + shift_finished = forms.DateTimeField( input_formats=settings.DATETIME_INPUT_FORMATS) class Meta: @@ -59,7 +58,6 @@ def __init__(self, *args, **kwargs): self.user = self.request.user super(ShiftForm, self).__init__(*args, **kwargs) - # Hide the actual input fields self.fields['shift_started'].widget = forms.HiddenInput() self.fields['shift_finished'].widget = forms.HiddenInput() self.fields['pause_duration'].widget = forms.HiddenInput() @@ -112,7 +110,6 @@ def __init__(self, *args, **kwargs): def clean_pause_duration(self): pause_duration = self.cleaned_data.get('pause_duration') - return pause_duration * 60 def clean(self): diff --git a/clock/shifts/tests/test_forms.py b/clock/shifts/tests/test_forms.py index c8e3d67..f8d4c9b 100644 --- a/clock/shifts/tests/test_forms.py +++ b/clock/shifts/tests/test_forms.py @@ -58,7 +58,7 @@ def test_form_valid_w_pause(self): start = timezone.now() - timezone.timedelta(0, 3600) stop = timezone.now() - pause = timezone.timedelta(0, 600) + pause = '00:10' data = { 'shift_started': start, @@ -130,7 +130,8 @@ def test_form_update_shift_instance(self): data = { 'shift_started': start, 'shift_finished': stop, - 'pause_duration': timezone.timedelta(0, 3000), + 'pause_duration': '00:50', # The user input is defined in + # "%HH:%mm", so this is 50 minutes. 'contract': None, 'key': '', 'tags': '', @@ -143,7 +144,6 @@ def test_form_update_shift_instance(self): } form = ShiftForm(data=data, instance=shift, **kwargs) assert form.is_valid() - form.save() shift = Shift.objects.get(pk=shift.pk) diff --git a/clock/shifts/tests/test_views.py b/clock/shifts/tests/test_views.py index a171799..4c4a87c 100755 --- a/clock/shifts/tests/test_views.py +++ b/clock/shifts/tests/test_views.py @@ -1,13 +1,14 @@ +"""Test shift app views. + +All messages are tested for the default English strings. +""" from django.contrib.messages import get_messages -from django.utils import timezone +from django.utils import timezone, translation from test_plus.test import TestCase from clock.contracts.models import Contract from clock.shifts.models import Shift -"""Test shift app views.""" - - class ManualShiftViewTest(TestCase): """ @@ -20,16 +21,20 @@ def setUp(self): employee=self.user, department='Test department', hours='50') def test_manual_shift_start(self): - """ - Assert that we can start a shift when logged in and without having a current shift. + """Assert that we can start a shift when logged in and without having a + current shift. """ with self.login(username=self.user.username, password='password'): response = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_start': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) - messages = [msg for msg in get_messages(response.wsgi_request)] + with translation.override('en'): + messages = [msg for msg in get_messages(response.wsgi_request)] shift = Shift.objects.all()[0] self.assertFalse(shift.bool_finished) @@ -42,13 +47,19 @@ def test_cannot_start_another_shift(self): """Assert that we cannot have two shifts running at the same time.""" with self.login(username=self.user.username, password='password'): response1 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_start': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) response2 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_start': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) messages = [msg for msg in get_messages(response2.wsgi_request)] @@ -66,13 +77,19 @@ def test_start_pause_resume_shift(self): """ with self.login(username=self.user.username, password='password'): response1 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_start': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) response2 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_pause': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) messages = [msg for msg in get_messages(response2.wsgi_request)] @@ -85,9 +102,12 @@ def test_start_pause_resume_shift(self): self.assertEqual(messages[0].__str__(), 'Your shift was paused.') response3 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_pause': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) messages = [msg for msg in get_messages(response3.wsgi_request)] @@ -106,13 +126,19 @@ def test_start_stop_shift(self): """ with self.login(username=self.user.username, password='password'): response1 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_start': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) response2 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_stop': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) messages = [msg for msg in get_messages(response2.wsgi_request)] @@ -130,13 +156,19 @@ def test_start_pause_stop_shift(self): """ with self.login(username=self.user.username, password='password'): response1 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_start': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) response2 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_pause': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) messages = [msg for msg in get_messages(response2.wsgi_request)] @@ -149,9 +181,12 @@ def test_start_pause_stop_shift(self): self.assertEqual(messages[0].__str__(), 'Your shift was paused.') response3 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_stop': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) messages = [msg for msg in get_messages(response3.wsgi_request)] @@ -169,9 +204,12 @@ def test_cannot_stop_pause_non_existent_shift(self): """ with self.login(username=self.user.username, password='password'): response1 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_stop': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) messages1 = [msg for msg in get_messages(response1.wsgi_request)] self.assertEqual(len(messages1), 1) @@ -180,9 +218,12 @@ def test_cannot_stop_pause_non_existent_shift(self): 'You need an active shift to perform this action!') response2 = self.post( - 'shift:quick_action', data={ + 'shift:quick_action', + data={ '_pause': True, - }, follow=True) + }, + follow=True, + extra={'HTTP_ACCEPT_LANGUAGE': 'en'}) messages2 = [msg for msg in get_messages(response2.wsgi_request)] self.assertEqual(len(messages2), 1) self.assertEqual( From 0541cb6157eb7a3a6fc5c06ff9b8127959b4ff50 Mon Sep 17 00:00:00 2001 From: Michael Gecht Date: Sat, 11 Nov 2017 17:01:08 +0100 Subject: [PATCH 14/14] Version 2.1 --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b96c44..6f22da7 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 2.1 (2017-11-11) + +Small rework of the frontend foundation of the project (no direct frontend changes). + +* Use `yarn` to pull in all frontend dependencies. +* Use `webpack` to compile a `main.js` bundle including JavaScript code and CSS (minified in `production`). + * The user only makes one HTTP request to the server and not several to retrieve the frontend libraries. + * It's more convinient to add new JavaScript dependencies, as long as we are using this frontend +* Get rid of the unmaintained `django-bootstrap-datetimepicker3` package. Replace with normally embedded `bootstrap-datetimepicker3`. +* Change the DateTimePickers to be `inline` always, thus hiding the `input` field from the user. +* Update formatting and translations. +* Get rid of `from __future__` imports. The codebase should be Python 3 only now. + ## 2.0 (2017-11-01) ### Release 2.0 diff --git a/package.json b/package.json index b039460..2dceaa5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "django-clock", - "version": "2.0.0", + "version": "2.1", "main": "webpack.config.js", "repository": "git@github.com:mimischi/django-clock", "author": "Michael Gecht ",