diff --git a/.circleci/config.yml b/.circleci/config.yml index b04320fb60..5bc7d624ba 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -40,6 +40,7 @@ jobs: VERSION_TAG="ci-${VERSION_STR}-${BRANCH}" fi echo ${VERSION_TAG} > scm/version_tag.txt + echo "Using tag: ${VERSION_TAG}" - run: name: Ensure PyApp Image Exists, building if necessary command: | @@ -49,7 +50,10 @@ jobs: name: Build "Default Branding" Image command: | VERSION_TAG=$(cat scm/version_tag.txt) - source scm/utils-engage.sh; BuildVersionForRp "$VERSION_TAG" + # only build this if constructing a tag version + if [[ ! -z $CIRCLE_TAG ]]; then + source scm/utils-engage.sh; BuildVersionForRp "$VERSION_TAG" + fi - run: name: Build "Engage Branding" Image command: | @@ -66,7 +70,10 @@ jobs: name: Build "Generic Branding" Image command: | VERSION_TAG=$(cat scm/version_tag.txt) - source scm/utils-engage.sh; BuildVersionForGeneric "${VERSION_TAG}-generic" + # only build this if constructing a tag version + if [[ ! -z $CIRCLE_TAG ]]; then + source scm/utils-engage.sh; BuildVersionForGeneric "${VERSION_TAG}-generic" + fi deploy-dev-container: docker: @@ -92,16 +99,18 @@ jobs: VERSION_TAG="ci-${VERSION_STR}-${BRANCH}" fi echo ${VERSION_TAG} > scm/version_tag.txt + echo "Using tag: ${VERSION_TAG}" - run: name: Deploy To K8s command: | VERSION_TAG=$(cat scm/version_tag.txt) K8_URL=https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/p4-deploy/tree/develop + echo "Deploy to K8s: $CIRCLE_PROJECT_USERNAME/p4-engage:${VERSION_TAG}" curl -XPOST --user "${oCI_API_TOKEN}:" \ --data build_parameters[CIRCLE_JOB]=eks_deploy \ --data build_parameters[DEPLOY_PROJECT]=pulse-engage \ --data build_parameters[DEPLOY_IMAGE]=$CIRCLE_PROJECT_USERNAME/p4-engage:${VERSION_TAG} \ - $K8_URL + $K8_URL workflows: version: 2 diff --git a/.gitignore b/.gitignore index 32d8da87fd..8138208d61 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,7 @@ ansible/site.retry # CircleCI artifacts scm/*_tag.txt + +_v*/ + +workspace/info/ diff --git a/VERSION b/VERSION index e5ca64a35c..719289bf2d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.12.7 +4.0.12.8 diff --git a/docker/Dockerfile.engage b/docker/Dockerfile.engage index ae535bde7d..d3f42e5e67 100644 --- a/docker/Dockerfile.engage +++ b/docker/Dockerfile.engage @@ -17,13 +17,17 @@ LABEL org.label-schema.name="Engage" \ ENV VERSION_TAG=$VERSION_TAG # apply branding/overrides +COPY --chown=engage:engage docker/customizations/any /opt/rp COPY --chown=engage:engage docker/customizations/engage /opt/rp USER root RUN rsync -a /opt/rp/ ./ && rm -R /opt/rp USER engage # compress static files -RUN mkdir -p static/sitestatic \ - && cp -fr static/brands static/sitestatic/brands \ - && source /venv/bin/activate; REDIS_URL=redis://redis DATABASE_URL=postgres://bla SECRET_KEY=123 python manage.py collectstatic --noinput --no-post-process \ - && source /venv/bin/activate; REDIS_URL=redis://redis DATABASE_URL=postgres://bla SECRET_KEY=123 python manage.py compress --extension='.haml' --force -v0 +RUN echo "Collecting static files into one location..." \ + && source /venv/bin/activate; REDIS_URL=redis://redis DATABASE_URL=postgres://bla SECRET_KEY=123 \ + python manage.py collectstatic --noinput --settings=temba.settings_collect_static \ + && echo "Collected, compressing..." \ + && source /venv/bin/activate; REDIS_URL=redis://redis DATABASE_URL=postgres://bla SECRET_KEY=123 \ + python manage.py compress --extension='.haml' --force -v0 --settings=temba.settings_compress_static \ + && echo "Done" diff --git a/docker/Dockerfile.generic b/docker/Dockerfile.generic index 862185d131..c67cb45da1 100644 --- a/docker/Dockerfile.generic +++ b/docker/Dockerfile.generic @@ -14,6 +14,7 @@ LABEL org.label-schema.name="Engage" \ WORKDIR /rapidpro # apply branding/overrides +COPY --chown=engage:engage docker/customizations/any /opt/rp COPY --chown=engage:engage docker/customizations/generic /opt/rp USER root RUN rsync -a /opt/rp/ ./ && rm -R /opt/rp diff --git a/docker/Dockerfile.pygeos b/docker/Dockerfile.pygeos index b44c4f0148..6c405aa88f 100644 --- a/docker/Dockerfile.pygeos +++ b/docker/Dockerfile.pygeos @@ -1,3 +1,4 @@ +# Alpine image with Python 3.9 as default python3 install FROM node:12.22-alpine3.14 COPY docker/geolibs.sh / diff --git a/docker/Dockerfile.pylibs b/docker/Dockerfile.pylibs index 926bff2b70..9791745e8d 100644 --- a/docker/Dockerfile.pylibs +++ b/docker/Dockerfile.pylibs @@ -43,8 +43,10 @@ ENV PIP_RETRIES=$ARG_PIP_RETRIES \ PIP_EXTRA_INDEX_URL=$ARG_PIP_EXTRA_INDEX_URL \ LIBRARY_PATH=/lib:/usr/lib +USER root + ARG USER_PID -RUN addgroup engage; adduser -D -S -s /bin/false -u $USER_PID engage -g engage \ +RUN grp=engage; usr=engage && addgroup -S $grp && adduser -u $USER_PID -S $usr -G $grp \ && mkdir -p /rapidpro; chown -R engage:engage /rapidpro \ && mkdir -p /venv; chown -R engage:engage /venv diff --git a/docker/Dockerfile.rp b/docker/Dockerfile.rp index a46db4dc00..684cd20230 100644 --- a/docker/Dockerfile.rp +++ b/docker/Dockerfile.rp @@ -18,6 +18,7 @@ LABEL org.label-schema.name="RapidPro" \ WORKDIR /rapidpro # apply branding/overrides +COPY --chown=engage:engage docker/customizations/any /opt/rp COPY --chown=engage:engage docker/customizations/rp /opt/rp USER root RUN rsync -a /opt/rp/ ./ && rm -R /opt/rp diff --git a/docker/customizations/any/static/less/legacy.less b/docker/customizations/any/static/less/legacy.less new file mode 100644 index 0000000000..1a7e207b2b --- /dev/null +++ b/docker/customizations/any/static/less/legacy.less @@ -0,0 +1,2493 @@ +// ***************************************************** +// NOTE NOTE NOTE NOTE!! +// +// This file exists only to carry over non-less styles +// Do not add to this file, ultimately everything here +// needs to be moved into a page's extra-less block +// or should be converted into a master style in the +// main style.less file. +// +// ***************************************************** + +input { + &#id_min_value { + width: 60px; + } + + &#id_max_value { + width: 60px; + } +} + +// The dropdown options +.glyph.message-label.checked:before { + content: "\e05a"; +} + +.glyph.message-label.partial:before { + content: "\e003"; +} + +.glyph.message-label.checked-child:before { + content: "\e003"; +} + + +.glyph.checked:before { + content: "\e05a"; +} + + +.glyph.message-label:before { + content: "\e004"; +} + +.glyph.message-label.checked:before { + content: "\e05a"; +} + + +.dropdown-submenu:hover>a:after { + border-left-color: #CCC; +} + + +.label-menu .glyph { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + color: #999; + font-size: 12px; + margin-top: 2px; + width: 12px; +} + +.label-menu.dropdown-menu a { + padding-left: 10px; + padding-right: 10px; + +} + +.dropdown-submenu>a::after { + margin-right: -5px; +} + +// the message list +td.value-received { + text-align: right; +} + +.radio, +.checkbox { + min-height: initial; + padding-left: initial; +} + +.sms_list tbody td.checkbox { + color: #777; +} + +.glyph.message-checkbox:before { + content: "\e004"; +} + +.glyph.message-checkbox, +.glyph.message-label { + font-family: 'temba'; + speak: none; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + float: left; + margin-top: 3px; + margin-right: 5px; +} + +// overrides just for windows +.windows { + + .title-text>h1, + .title-text {} +} + +a:hover.clear-link { + text-decoration: none; +} + +.value-labels { + float: right; +} + +// android page +.rotate { + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; +} + +.float-left { + float: left; +} + +// contact read page + +.send-form { + width: 682px; + padding-bottom: 10px; + background-color: #efefef; + padding-left: 20px; + padding-right: 20px; + padding-top: 18px; + border-left: 1px solid #dedede; + border-right: 1px solid #dedede; + border-top: 1px solid #eee; +} + +#id_text { + width: 100%; + height: 120px; + margin-bottom: 15px; +} + +#id_send { + vertical-align: bottom; + margin-bottom: 10px; +} + +td.clickable a { + color: rgb(51, 51, 51); +} + +// ***************************************************** +// styles.less purge 013117 +// ***************************************************** + +// tweak toastr +.toast-bottom-full-width { + margin-bottom: 20px; +} + +.toast-info { + background-color: rgba(0, 0, 0, 0.85); +} + +// style all placeholder text + +.force-wrap { + word-break: break-all; +} + +.spin { + -webkit-animation: rotation 2s infinite linear; +} + +@-webkit-keyframes rotation { + from { + -webkit-transform: rotate(0deg); + } + + to { + -webkit-transform: rotate(359deg); + } +} + +#page-container { + .placeholder-text(#e6e6e6, 200); +} + +// Tweaking bootstrap styling + +[class^="icon-"], +[class*=" icon-"] { + /* override later */ + /* background-image: none; */ +} + +.btn, +.btn-group, +.dropdown, +.dropdown-toggle, +.dropdown-menu, +.btn-group>.btn:first-child, +.btn-group>.btn:last-child { + .rounded-corners(0); +} + +.btn-group>.btn:last-child, +.btn-group>.dropdown-toggle { + .rounded-corners(0); +} + +.btn { + &.btn-mini { + padding: 1px 6px; + margin-right: 0; + margin-left: 0; + float: left; + } + + &.remove { + margin-right: 5px; + } + + &.disabled, + &[disabled] { + opacity: 0.15; + } +} + +.table-condensed th, +.table-condensed td { + padding: 7px 6px; +} + +.label, +.badge { + text-shadow: none; +} + +.dropdown-menu li>a:hover, +.dropdown-menu li>a:focus, +.dropdown-submenu:hover>a { + background: @color-selected; + color: @color-selected-font; + text-shadow: none; +} + +.nav>li>a { + + text-shadow: none; + + &:hover { + background: @color-hovered; + color: @color-hovered-font; + text-shadow: none; + margin-right: -15px; + } +} + +.nav-list { + .active { + >a { + background: @color-selected; + color: @color-selected-font; + text-shadow: none; + + &:hover { + background: @color-selected; + color: @color-selected-font; + text-shadow: none; + } + } + } +} + +.nav-list.level2 a { + margin-right: -30px; + + &:hover { + margin-right: -30px; + } +} + +li { + list-style-type: none; +} + +ul.recipients-list { + li { + padding: 0; + } + + padding: 0; + margin: 0; +} + +// form stylig + +form, +input { + margin: 0; +} + +.input-append .add-on, +.input-prepend .add-on, +.input-append .btn, +.input-prepend .btn { + margin-left: -1px; + vertical-align: top; + .rounded-corners(0); +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + font-size: 14px; + line-height: 14px; + vertical-align: top; + .rounded-corners(0); +} + +.input-append .add-on, +.input-prepend .add-on { + display: inline-block; + width: auto; + height: 25px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: 400; + line-height: 25px; + text-align: center; + text-shadow: 0 1px 0 #fff; + background-color: #eee; + border: 1px solid #ccc; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + .rounded-corners(0); +} + +input, +textarea { + width: 350px; +} + +.controls .uneditable-input { + width: 350px; +} + +form.form-horizontal textarea, +form.form-vertical textarea { + height: auto; + margin-bottom: 5px; +} + +.form-horizontal .controls, +.form-horizontal .form-control { + margin-left: 10px; +} + +.formax-form .form-horizontal .form-control { + margin-left: 0px; +} + +.form-horizontal .formax-vertical .form-control { + margin-left: 0px; +} + +.form-horizontal .control-label { + float: left; + width: 150px; + padding-top: 5px; + margin-right: 10px; + text-align: right; +} + +.smartmin-form-buttons, +.form-actions { + background-color: transparent; + border: 0; +} + +.form-horizontal .smartmin-form-buttons { + padding-left: 160px; +} + +// nav styling + +.nav>li>a { + display: block; + font-weight: 200; + color: @color-links; +} + +.nav-list>li>a { + padding: 6px 15px; +} + +.pagination ul>li>a, +.pagination ul>li>span { + float: left; + padding: 4px 12px; + line-height: 20px; + text-decoration: none; + background-color: #ffffff; + border: 0px solid #dddddd; + border-left-width: 0; +} + +.pagination ul { + display: inline-block; + margin-bottom: 0; + margin-left: 0; + .rounded-corners(0); + -webkit-box-shadow: 0; + -moz-box-shadow: 0; + box-shadow: none; +} + +.pagination a, +.pagination span { + float: left; + padding: 0 14px; + line-height: 48px; + text-decoration: none; + background-color: #fff; + // border: 1px solid #ddd; + border-left-width: 0; +} + +// pagination +.pagination a { + float: left; + padding: 0 14px; + line-height: 48px; + text-decoration: none; + border: 0; + border-left-width: 0; +} + +.pagination a { + line-height: 30px; +} + +.pagination ul>li:first-child>a, +.pagination ul>li:first-child>span { + border-left-width: 0px; + .rounded-corners(0); +} + +.pagination ul>li:last-child>a, +.pagination ul>li:last-child>span { + .rounded-corners(0); +} + +body.modal-open { + overflow: hidden; +} + +.btn.disabled { + opacity: 0.2; +} + +.recompile_3 {} + +// ******************************* +// Primary widget styling +// ******************************* + +html, +body { + height: 100%; +} + +body { + font-family: @primary-font; + font-weight: 200; + text-transform: none; + overflow-x: hidden; +} + +.container { + min-height: 100%; + padding-top: -130px; // height of the header + position: relative; +} + +a { + color: @color-links; +} + +h1, +h2, +h3, +h4, +h5 { + font-family: @helvetica, @roboto-thin, sans-serif; + letter-spacing: 1px; + padding: 0; + text-transform: none; + font-weight: 100; + color: @color-font-grey; +} + +h1 { + font-size: 44px; + line-height: 40px; +} + +h2 { + font-size: 42px; +} + +h3 { + font-size: 26px; +} + +h4 {} + +h5 { + font-weight: 300; + color: @color-font-lightgrey; + margin-bottom: 5px; +} + +// All site buttons +.btn { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + background-image: none; + border: 0; + padding: 8px 20px; + font-weight: 300; + margin-right: 0px; +} + +.btn-group .btn { + margin-right: 0; +} + +.btn-group .tooltip { + top: -38px !important; +} + +.btn-group.open { + .btn.dropdown-toggle { + background-color: @color-button - #222; + color: @color-button-font; + // text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5); + text-shadow: none; + } +} + +.btn-colored(@bg, @font) { + border: 1px solid @bg - #060606; + background-color: @bg; + background-image: -webkit-linear-gradient(top, @bg, @bg - #101010); + background: -moz-linear-gradient(top, @bg, @bg - #101010); + color: @font; + text-shadow: none; + + &:hover, + &:focus, + &:active, + &.open { + background-color: @bg - #111; + background-image: none; + color: @font; + text-shadow: none; + } +} + +.btn { + .btn-colored(@color-button, @color-button-font) +} + +.btn-primary { + .btn-colored(@color-button-primary, @color-button-primary-font); +} + +.group-membership { + display: inline-block; + + .btn.btn-primary { + border: 0px; + } +} + +.btn-danger { + .btn-colored(@color-button-danger, @color-button-danger-font); +} + +.btn-success { + .btn-colored(@color-button-success, @color-button-success-font); +} + +.btn-tiny { + padding: 3px 8px; + font-size: 12px; +} + +.label-info, +.badge-info { + background-color: @color-label; +} + +// label badges +.label { + border: 1px solid @color-label - #111; + .rounded-corners(0); + font-weight: 200; + font-size: 10px; + padding: 2px 4px 0px 4px; + margin-right: 0px; + height: 16px; + line-height: 16px; + display: inline-block; + white-space: nowrap; + vertical-align: baseline; + color: @color-font-white; + padding-top: 0px; + + &.label-success { + border: 1px solid @flat-mutedgreen - #333; + background-color: @flat-mutedgreen - #222; + } + + &.label-group { + margin-right: -4px; + margin-left: 0; + padding: 2px 6px; + + &.remove { + line-height: 14px; + font-size: 14px; + text-align: match-parent; + padding: 2px 6px; + margin-right: 1px; + margin-top: 0px; + } + } +} + +.label { + &.btn { + border: 0; + } +} + +.glyph { + // default icon size + line-height: 1; + text-decoration: none; + font-size: 16px; + + &.success { + color: @color-font-success; + } + + &.warning { + color: @color-font-warning; + } +} + +a.logo { + color: #fff; + font-size: 84px; + margin-top: -2px; + text-decoration: none; + + .name { + display: none; + } +} + +.spacer-right-10 { + padding-right: 10px; +} + +.spacer-bottom-12 { + margin-bottom: 12px; +} + +// ******************************* +// The list tables we use on messages, contacts etc.. +// ******************************* + +.list-container { + .scroll-x(); +} + +table.list-table { + td.value-phone { + width: 100px; + } + + td.value-received { + width: 50px; + text-align: right; + } + + td.value-icon { + width: 20px; + text-align: right; + padding-right: 0px; + } + + tr:hover td { + color: #000; + } + + tr:hover td.checkbox { + color: @color-font-lightgrey; + } + + tr:hover td.checkbox:hover { + color: @flat-blue; + } +} + +.empty-message { + padding: 30px 20px; + font-size: 16px; + font-weight: normal; + letter-spacing: .1em; + + p { + padding-top: 0; + margin-top: 0; + } +} + +// ******************************* +// Frame styling +// ******************************* + +.logo { + position: absolute; + left: 0; + top: 0; + z-index: 500; + padding-left: 30px; + padding-top: 20px; +} + +#menu { + z-index: 100; + + &.expanded { + z-index: 5000; + } + + .more { + position: relative; + + a { + font-size: 32px; + } + + .submenu-container { + position: absolute; + top: -20px; + left: -30px; + padding: 10px 5px; + padding-bottom: 40px; + z-index: 2000; + + .submenu { + position: relative; + width: 60px; + top: 10px; + background: #fff; + .drop-shadow(0, 1px, 1px, 3px, .1, 0); + .rounded-corners(5px); + border: 1px solid @color-navbar; + + &::before { + position: absolute; + top: -7px; + left: 24px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #CCC; + border-left: 7px solid transparent; + border-bottom-color: rgba(255, 255, 255, 1); + content: ''; + } + } + } + } + + @media all and (max-width: 767px) { + text-align: right; + right: 25px !important; + margin-right:15px; + margin-top: -65px !important; + padding-bottom: 19px; + min-height:50px !important; + + a { + width: 42px !important; + } + + .circle { + border-width: 2px !important; + width: 25px !important; + height: 25px !important; + + div { + width: 13px !important; + height: 14px !important; + font-size: 14px !important; + } + } + + .title { + display: none !important; + } + + div { + // border:1px solid pink; + width: 15px; + height: 15px; + font-size: 15px; + text-align: left; + margin: 0; + + &.more { + height: 0; + width: 0; + visibility: hidden; + + .submenu-container { + text-transform: lowercase; + left: -20px; + top: 14px; + display: block; + + .submenu { + .drop-shadow(0); + + a { + height: 22px; + font-size: 24px; + } + } + } + } + + a { + width: 25px; + height: 25px; + padding: 0; + } + } + } +} + +.nav-alert { + a { + color: #fff; + + &:hover { + text-decoration: none; + color: #f3f3f3; + } + } + + &.interrupted { + background: rgb(178, 23, 26) + } + + .icon-warning { + margin-right: 1px; + } + + position: absolute; + top: 110px; + right: 30px; + display: none; + text-align: center; + float: right; + color: #fff; + background: rgba(0, 0, 0, .15); + margin-top: -0px; + height: 20px; + padding: 1px 20px; + .border-radius(3px, 3px, 0px, 0px); + font-size: 11px; + font-weight: 200; + + @media all and (max-width: 767px) { + .rounded-corners(3px); + margin-top: 10px; + right: 50px; + top: 25px; + width: 170px; + background: none; + text-align: left; + font-size: 12px; + padding: 0; + } +} + +a.org-choose { + padding: 5px; + background: #eee; + margin-bottom: 10px; + display: block; + color: #333; + line-height: 50px; + width: 500px; + height: 50px; + + .name { + margin-left: 50px; + } + + .icon-home { + margin-top: 10px; + margin-left: 10px; + font-size: 30px; + float: left; + } + + &:hover { + color: #f5f5f5; + text-decoration: none; + background-color: @flat-blue; + } +} + +.org-header { + + z-index: 1039; + margin-top:-6px; + margin-right:30px; + margin-bottom:-28px; + text-align: center; + background: rgba(0, 0, 0, .15); + padding-top: 6px; + font-size: 11px; + max-width: 180px; + .rounded-corners(3px); + + cursor: pointer; + color: #fff; + text-decoration: none; + + &:hover { + text-decoration: none; + color: #f3f3f3; + } + + .toggle { + margin-top: 1px; + padding: 0px 5px; + } + + .icon-warning { + color: @color-notification; + } + + .org { + line-height: 14px; + padding: 3px 10px 5px; + width: 160px; + } + + .other-orgs { + width: 178px; + position:absolute; + right:40px; + .account-details { + padding: 5px 0px; + border-bottom: 1px solid #ccc; + + a { + color: @color-navbar; + } + + margin: 0 10px 3px; + + .org { + padding: 0; + } + } + + .org { + + a { + color: @color-navbar; + + &:hover { + color: @color-navbar - #222; + } + } + + max-width: 200px; + } + + background: #fff; + margin: -2px -10px -10px; + text-align: left; + border: 1px solid @color-navbar - #111; + } + + &.expanded { + .other-orgs { + display: inline-block; + .border-radius(0, 0, 3px, 3px); + } + } + +} + +#header { + + background-color: var(--header-bg); + border: 0px solid pink; + z-index: 0; + position: relative; + + @media all and (max-width: 940px) and (min-width: 767px) { + min-width: 940px; + } + + @media all and (max-width: 767px) { + margin-left: -20px; + margin-right: -20px; + padding-left: 20px; + padding-right: 20px; + } + + #menu-right-container { + height: 60px; + // border: 1px solid purple; + position: absolute; + width: 600px; + right: 0px; + + top: 10px; + right: 0px; + overflow-x: hidden; + + #menu-right { + float: right; + border: 1px solid yellow; + height: 60px; + width: 600px; + } + } + + #menu-left { + height: 70px; + border: 0px solid yellow; + } +} + +#print-header { + text-align: right; + font-size: 32px; + margin-right: 20px; + color: #666; + display: none; +} + +.instructions { + font-size: 16px; + margin-bottom: 20px; + color: #999; + line-height: 25px; + width: 80%; +} + +.instructions { + @media screen { + display: gone; + } +} + +// ******************************* +// Form styling +// ******************************* + +form.smartmin-form { + .help-block { + margin-top: 0px !important; + } + + .uneditable-input { + margin-bottom: 5px; + } + + select { + margin-bottom: 9px; + } + + ul.errorlist { + margin: 0; + + li { + color: @color-alert-error; + } + } + + .alert-error { + .rounded-corners(0px); + } +} + +form.smartmin-form.form-horizontal { + ul.errorlist { + margin-left: 150px; + } +} + +.sort-placeholder { + border-top: 1px solid @flat-white - #111; + border-bottom: 1px solid @flat-white - #111; + background-color: @flat-white + #090909; + width: 94%; + +} + +input[type='text'].search-query { + .rounded-corners(12px); + width: 200px; + padding: 5px 15px; + padding-top: 2; + font-size: 14px; + font-weight: 200; + +} + +// ******************************* +// Main content styling +// ******************************* + +.top-bar { + margin-top: 5px; + border-top: 0px; +} + +.top-form { + margin-top: 15px; +} + +// main page header icon +.title-icon { + .glyph { + margin-top: 22px; + font-size: 56px; + line-height: 0; + color: #999; + float: left; + } + + .glyph.icon-bubble-notification { + margin-top: 35px; + } + + +.title-text { + margin-left: 85px; + } +} + +.title-text { + h2 { + margin-bottom: 0px; + margin-top: 0px; + } + + h5 { + margin-bottom: 0px; + margin-top: 2px; + } +} + +#gear-container { + + height: 46px; + display: inline-block; + + .gear-menu { + + .dropdown-menu { + left: -96px; + + li>a { + padding: 8px 8px; + + &:hover {} + } + } + + .btn { + padding: 8px 20px; + } + + .glyph { + color: @color-button-font; + } + + .caret { + border-top-color: @color-button-font; + } + } +} + +// the big help treatment +#big-help { + font-size: 250px; + color: @flat-darkwhite; + margin-top: 20px; + + @media (max-width: 2000px) { + font-size: 180px; + height: 200px; + margin-left: 25px; + } + + @media (max-width: 768px) { + font-size: 80px; + height: 200px; + margin-left: 25px; + display: none; + } + + &.icon-contact { + margin-top: -30px; + } +} + +.medium-help { + color: #999; + height: 90px; + font-size: 72px; +} + +.medium-glyph { + line-height: 1; + font-size: 18px; + text-decoration: none; +} + +.italics { + font-style: italic; +} + +// ******************************* +// List views +// ******************************* + +#sidebar-nav { + border: 1px solid #e6e6e6; + + .nav.nav-list:not(.level2) { + .scroll-y(); + height: 450px; + + li { + a { + font-size: 13px; + } + } + } +} + +// buttons at the top of the list +.message-buttons { + margin-bottom: 0px; +} + +#send-message { + label { + margin-top: 0px; + color: #666; + } + + #counter { + font-size: 12px; + float: right; + } + + #errors { + margin-top: 10px; + } + +} + +// ******************************* +// Smartmin +// ******************************* +.control-group.error .control-label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: var(--color-error); +} + +.controls { + temba-select { + input:focus { + box-shadow: none !important; + } + } +} + +.control-group.error .control-label { + color: var(--color-error) !important; +} + +.control-group.error { + + input, + select, + texarea { + border-color: var(--color-error); + color: var(--color-error); + + &:focus, + &.invalid:focus { + border-color: var(--color-error) !important; + color: var(--color-error) !important; + box-shadow: inset 0 1px 1px rgba(var(--error-rgb), 0.075), 0 0 6px var(--color-error) !important; + } + } +} + +form.smartmin-form .alert-error { + background: rgba(255, 181, 181, .17); + border: none; + border-left: 0px solid var(--color-error); + color: var(--color-error); + padding: 10px; + margin-bottom: 10px; +} + +form.smartmin-form ul.errorlist { + font-size: 12px; +} + +form.smartmin-form ul.errorlist li { + text-shadow: none; + line-height: inherit; +} + +.form-horizontal .form-actions { + padding-left: 160px; +} + +.form-actions { + padding: 0px; + margin-top: 0px; +} + +// ******************************* +// Modals +// ******************************* +#modal-container { + + .smartmin-form-buttons, + .form-actions { + display: none; + } + + .span12 { + width: 100%; + margin-left: 0; + } + +} + +#active-modal .fetched-content {} + +.modal, +.send-message { + + .span12 { + width: 100%; + margin-left: 0; + } + + .smartmin-form-buttons, + .form-actions { + display: none; + } + + .modal-header { + padding: 10px 15px 8px 15px; + border-bottom: 1px solid @color-bg-blue - #111; + background: @color-alert-notice; + color: @color-font-white; + + .title { + color: @color-font-white; + } + + #modal-title { + color: @color-font-white; + font-size: 22px; + font-weight: 200; + bottom: 15px; + top: -2px; + position: relative; + } + + .glyph { + color: @color-font-white; + font-size: 23px; + margin-right: 15px; + + &.icon-bubble-3 { + margin-top: 4px; + } + } + } + + .modal-body { + + padding: 20px; + position: initial; + + .details { + color: #ccc; + margin-top: 15px; + font-size: 14px; + font-style: italic; + } + + #send-form { + #counter { + margin-right: 3px; + } + + .error { + padding-left: 5px; + border: none; + } + + } + + .control-group, + .form-group { + margin-bottom: 0; + } + + .control-label { + width: auto; + text-align: left; + display: block; + float: none; + } + + .controls, + .form-control { + display: block; + margin-left: 0; + float: none; + } + + .help-block { + color: @color-font-lightgrey !important; + font-size: 12px !important; + margin: 5px 0px 10px 0px !important; + + margin-block-start: 0; + margin-block-end: 0; + + ul.errorlist { + margin-top: 8px; + + li { + color: var(--color-error) !important; + padding: 3px 8px; + border-left: 6px solid var(--color-error); + } + } + } + + #modal-message { + color: @flat-darkgrey; + } + } + + .modal-footer { + .rounded-corners(0); + background-color: @color-bg-lightgrey; + border-top: 1px solid @color-font-lightestgrey; + padding-top: 10px; + + .primary { + float: right; + margin-left: 10px; + } + + .ussd-counter { + float: left; + margin: 12px 0 0; + font-size: 13px; + line-height: 18px; + } + } + + &.airtime-warning { + .modal-header { + background: @flat-grey + #333; + border-bottom-color: @flat-grey + #111; + } + + #modal-title { + color: @flat-white; + text-shadow: none; + } + + .modal-footer { + .primary { + background: @flat-grey + #333; + border-bottom-color: @flat-grey + #111; + } + } + + } + + &.alert { + color: @color-font-black; + padding: 0px; + + .modal-header { + background: @color-alert-error; + border-bottom: 1px solid @color-alert-error - #111; + + #modal-title { + color: @flat-white; + text-shadow: none; + } + + .close { + margin-right: 20px; + } + } + + .modal-footer { + .primary { + background: @color-alert-error; + border: 1px solid @color-alert-error - #111; + } + } + + .glyph { + color: @color-alert-error - #444; + } + + } + + .error { + color: @color-alert-error; + font-size: 14px; + } +} + +.weight-200 { + font-weight: 200; +} + +.weight-100 { + font-weight: 100; +} + +// Styles used primarily in our API explorer + +code { + color: @color-bg-blue; + padding: 1px 4px; +} + +// style for the tooltip + +div.tooltip.in { + opacity: 1; +} + +.header-margin { + margin-left: 100px; + margin-top: 4px; +} + +// ******************************* +// Time picker styling +// ******************************* +.ui-timepicker-div .ui-widget-header { + margin-bottom: 8px; +} + +.ui-timepicker-div dl { + text-align: left; +} + +.ui-timepicker-div dl dt { + width: 0px; + margin-bottom: -25px; + display: none; +} + +.ui-timepicker-div dl dd { + margin: 0 10px 10px 10px; +} + +.ui-timepicker-div td { + font-size: 90%; +} + +.ui-tpicker-grid-label { + background: none; + border: none; + margin: 0; + padding: 0; +} + +.ui-timepicker-rtl { + direction: rtl; +} + +.ui-timepicker-rtl dl { + text-align: right; +} + +.ui-corner-all { + .rounded-corners(0); +} + +#posterizer { + display: none; +} + +.loader-circles { + background-image: url('../images/loader-circles.gif'); + width: 150px; + height: 14px; +} + +/*.btn-primary { +background: #997ad1; + +&.hover { +background: #624e87; +} +}*/ + +// some surgery for IE +.ie { + + #big-help { + font-size: 190px; + } + + #header { + a.charm { + .glyph { + position: absolute; + font-size: 17px; + margin-top: -21px; + } + } + } + + .formax { + .formax-container { + .formax-form { + + margin-left: 60px; + + .category-input input { + height: 25px; + } + + .icon-link { + margin-top: -4px; + } + + #later-option { + margin-top: -5px; + } + + #start-datetime { + margin-left: -5px; + margin-top: 10px; + } + + } + } + } + +} + +.span9 .list-table { + margin-top: 48px; +} + +.modal-body { + .row { + margin-left: 0px; + } + + input, + textarea { + width: 505px; + } + + temba-select { + input { + width: inherit; + } + } + + .loader { + display: none; + width: 270px; + margin: 0 auto; + padding: 30px 0; + } +} + +/*.empty-list +.icon.icon-feed +.message +%h2 Triggering your Flows +.details +*/ + +.empty-list { + position: relative; + + .icon { + position: absolute; + font-size: 180px; + color: @flat-white; + left: 30px; + top: 5px; + } + + .message { + position: absolute; + } +} + +/* Schedules UI */ +#start-datetime { + width: 400px; + font-size: 14px; + font-weight: 200; + line-height: 20px; + color: #999; + background-color: transparent; + border: 0px solid #999; + cursor: text; +} + +.weekly-repeat-options { + .btn-group { + z-index: 0; + + .btn { + padding: 2px 8px; + } + } +} + +#start-datetime-value { + display: none; +} + +#modal .repeat-peroid, +.repeat-period { + + select { + width: 100px; + margin-top: 5px; + } + + .control-label { + float: left; + text-align: left; + margin-right: 5px; + margin-top: 2px; + width: 50px; + display: block; + font-size: 14px; + font-weight: 200; + line-height: 20px; + color: #999; + } +} + +input[type='radio'] { + margin-top: -5px; + margin-right: 6px; + margin-left: 6px; +} + +#schedule-next-run>#start-datetime { + color: #2E8BCC; + +} + +#start-datetime.hasDatepicker { + &:focus { + outline: none; + text-shadow: none; + box-shadow: none; + -moz-box-shadow: none; + } +} + +#schedule-next-run>#start-datetime:hover { + text-decoration: underline; +} + +[data-handler=today] { + display: none; +} + +#id-schedule.fixed .formax-container { + margin-left: 115px; + margin-top: 15px; +} + +#id-schedule .formax-container .formax-form input[value='Save'] { + display: none; +} + +.stop-button { + margin-top: 20px +} + +.fixed #form-buttons { + border-top: 0px solid #ddd; + margin-top: 20px; + margin-left: -70px; +} + +#schedule-options { + font-size: 14px; +} + +.schedule-options-label { + display: initial; + font-weight: 200; +} + +.error { + color: @color-font-white; + background-color: @color-alert-error + #222; + border: 1px solid @color-alert-error; + text-shadow: none; + width: 100%; + + ul.errorlist { + margin: 0px; + } + + &.control-group, + &.form-group { + // padding:0; + color: @color-alert-error; + background-color: inherit; + border: 0; + box-shadow: none; + + --focus-rgb: var(--error-rgb); + --color-focus: var(--color-error); + --widget-box-shadow-focused: 0 0 0 3px rgba(var(--error-rgb), .25); + --color-widget-border: var(--color-error); + } +} + +.modal { + .error { + // padding:0; + background-color: #fff; + border: 1px solid @color-alert-error; + + &.control-group, + &.form-group { + // padding:0; + background-color: #fff; + border: 0; + box-shadow: none; + } + } +} + +.login { + margin-top: 0px; + + width: 150px; + + padding: 3px 8px; + margin: 0; + + &.TIER_249 { + background: #EAE180; + } + + &.TIER1 {} + + &.TIER_39 { + background: #CB8D56; + color: #fff; + } + + &.FREE { + background: #fff; + color: @flat-blue; + } + + &.TRIAL { + background: #75AE82; + color: #fff; + } + +} + +// connect and disconnect in orgs +.disconnect-help { + font-size: 16px; + line-height: 20px; + + .account_details { + padding: 15px 30px 0px 0px; + + .account_key { + float: left; + width: 140px; + text-align: right; + font-weight: 400; + } + + .account_value { + margin-left: 150px; + } + } +} + +.org-button { + width: 130px; +} + +.glyph.notif-checkbox:before { + content: "\e004"; +} + +.field-input.checked .glyph.notif-checkbox:before, +.view_toggle.checked .glyph.notif-checkbox:before { + content: "\e05a"; +} + +.field-input .help-block { + padding-top: 4px !important; +} + +.glyph.notif-checkbox { + font-family: 'temba'; + speak: none; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + float: left; + margin-top: 6px; + margin-right: 5px; +} + +.font-checkbox { + padding: 10px 0px 5px 0px; + + #checkbox-label { + display: none; + } +} + +.help-block { + label { + color: #333 !important; + } +} + + +.list-buttons-container { + height: 35px; + display: none; + + &.visible { + display: block; + } + + .list-buttons { + + button { + min-width: 70px; + + .glyph { + margin-right: 3px; + } + + .glyph.icon-excel { + margin-left: -2px; + } + } + } +} + +.glyph.label-checkbox:before { + content: "\e004"; +} + +.glyph.object-row-checkbox:before { + content: "\e004"; +} + +.glyph.label-checkbox.checked:before { + content: "\e05a"; +} + +.glyph.label-checkbox.partial:before { + content: "\e003"; +} + +.glyph.label-checkebox.checked-child:before { + content: "\e003"; +} + +.glyph.checked:before { + content: "\e05a"; +} + +.form #number { + width: 150px; +} + +a:hover.clar-link { + text-decoration: none; +} + +// labels dropdown + +.label-menu.dropdown-menu a { + padding-left: 10px; + padding-right: 10px; +} + +.label-menu .glyph { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + color: #999; + font-size: 12px; + margin-top: 2px; + width: 12px; +} + +li.level1 ul { + display: none; +} + +li.active ul { + display: block; +} + +ul.nav.level2 { + display: none; +} + +li.expanded ul.nav.level2 { + display: block; +} + +ul.level1 li.expanded ul { + display: block; +} + +.dropdown-submenu>a::after { + margin-right: -5px; +} + +.level2 li a { + padding-left: 10px; + margin-right: -30px; +} + +// trigger list modal + +.modal-body #stop-option, +.modal-body #later-option { + width: auto; +} + +.modal-body .repeat-period .control-label { + float: left; + width: 55px; +} + +.modal-body .repeat-period { + margin-top: 10px; +} + +.modal-body .instructions { + margin-bottom: 0px; +} + +span.label a { + color: #fff; + text-decoration: none; + margin: 0px; +} + +.label-responses { + background-color: #aaa; + border-color: #aaa; +} + +// Styling to mimic iOS messaging client + +.imsg { + .rounded-corners(12px); + margin: 0 10px 5px; + padding: 8px 20px; + position: relative; + word-wrap: break-word; + min-height: 15px; +} + +.imsg.to { + background-color: #2095FE; + color: #fff; + margin-left: 30px; +} + +.imsg.from { + background-color: #E5E4E9; + color: #363636; + margin-right: 30px; +} + +.imsg.to+.message.to, +.imsg.from+.message.from { + margin-top: -7px; +} + +.imsg:before { + border-color: #2095FE; + border-radius: 50% 50% 50% 50%; + border-style: solid; + border-width: 0 20px; + bottom: 0; + clip: rect(20px, 35px, 42px, 0px); + content: " "; + height: 40px; + position: absolute; + right: -50px; + width: 30px; +} + +.imsg.from:before { + border-color: #E5E4E9; + left: -50px; + -webkit-transform: rotateY(180deg); + -moz-transform: rotateY(180deg); + transform: rotateY(180deg); +} + +// completion custom styles + +.atwho-view { + width: 280px; + max-width: 300px; + color: #555555; + + .atwho-view-ul { + color: #555555; + + .cur { + background: #efefef; + color: #555555; + + small { + color: #555555; + } + + } + } +} + +.completion-dropdown { + + .option-name { + font-size: 1em; + } + + .option-example { + display: none; + } + + .option-display { + display: none; + } + + .display-labels { + display: none; + } + + .cur>& { + .option-display { + display: block; + } + } + + .cur:first-child:last-child>& { + .option-example { + display: block; + margin-top: 5px; + } + + .display-labels { + display: block; + font-size: 0.75em; + + } + + .option-display { + display: block; + margin-top: 5px; + } + + } + +} + +#intercom-container .intercom-launcher { + bottom: 60px; + right: 12px; +} + +.featherlight .featherlight-image { + max-width: 100%; + max-height: 500px; + width: auto; +} + +.image-preview { + max-height: 200px; +} + +.image-full { + // max-width:800px; +} + +.download-link { + position: absolute; + right: 50px; + bottom: 15px; + font-size: 30px; + color: @color-font-white; +} + +.video-js { + min-width: 300px; + @big-play-bg-color: #ff00b7; + @big-play-bg-alpha: 1; +} + +.download { + margin-top: 5px; + padding: 5px; + background: @color-font-lightestgrey; + + .icon { + margin-right: 5px; + margin-top: 2px; + } + +} + +.wrapped { + /* forces wrapping of messages with no spaces, see http://stackoverflow.com/questions/26292408/ */ + max-width: 0; + word-wrap: break-word; + /* IE 10 */ + overflow-wrap: break-word; +} + +#indicator { + .loader { + height: 1px; + width: 100%; + position: relative; + overflow: hidden; + background-color: #ddd; + + &:before { + display: block; + position: absolute; + content: ""; + left: -200px; + width: 200px; + height: 1px; + background-color: #2980b9; + animation: loading 2s linear infinite; + } + } +} + +@keyframes loading { + from { + left: -200px; + width: 30%; + } + + 50% { + width: 30%; + } + + 70% { + width: 70%; + } + + 80% { + left: 50%; + } + + 95% { + left: 120%; + } + + to { + left: 100%; + } +} + +.nobreak { + white-space: nowrap; +} + +.breaks { + overflow-wrap: break-word; + word-wrap: break-word; + + -ms-word-break: break-all; + word-break: break-word; + + -ms-hyphens: auto; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +@-moz-keyframes rotate { + 100% { + -moz-transform: rotate(180deg); + } +} + +@-webkit-keyframes rotate { + 100% { + -webkit-transform: rotate(180deg); + } +} + +@keyframes rotate { + 100% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } +} + +.greyed { + color: rgba(0, 0, 0, .4); +} + +.box-sizing { + -webkit-box-sizing: border-box; + /* Safari/Chrome, other WebKit */ + -moz-box-sizing: border-box; + /* Firefox, other Gecko */ + box-sizing: border-box + /* Opera/IE 8+ */ +} + +/* Message bubble colors */ +.queued { + color: @color-status-queued +} + +.queued_instantly { + color: @color-status-queued-instantly +} + +.pending { + color: @color-status-pending +} + +.pending_instantly { + color: @color-status-pending-instantly +} + +.sent { + color: @color-status-sent +} + +.delivered { + color: @color-status-delivered +} + +.handled { + color: @color-status-handled +} + +.errored { + color: @color-status-errored +} + +.failed { + color: @color-status-failed +} + +/* Message table stylings */ +.glyph { + + &.green { + color: @color-status-green; + } + + &.orange { + color: @color-status-orange; + } + + &.red { + color: @color-status-red; + } + + &.grey { + color: @color-status-grey; + } + + &.blue { + color: @color-status-blue; + } + + &.primary { + color: @color-primary; + } +} + +[class^="icon-"], +[class*=" icon-"] { + transition: transform 400ms; + -webkit-transition: -webkit-transform 400ms; +} + +img.image-full.featherlight-inner { + max-height: 80%; +} + +// ***************************************************** +// NOTE NOTE NOTE NOTE!! +// +// This file exists only to carry over non-less styles +// Do not add to this file, ultimately everything here +// needs to be moved into a page's extra-less block +// or should be converted into a master style in the +// main style.less file. +// +// ***************************************************** diff --git a/docker/customizations/any/static/less/reset.less b/docker/customizations/any/static/less/reset.less new file mode 100644 index 0000000000..8424347f6e --- /dev/null +++ b/docker/customizations/any/static/less/reset.less @@ -0,0 +1,174 @@ +@import-once "variables"; + +.recompile_2 {} + +// Tweaking bootstrap styling + +[class^="icon-"], [class*=" icon-"] { + /* override later */ + /* background-image: none; */ +} + +.btn, .btn-group, .dropdown, .dropdown-toggle, .dropdown-menu, +.btn-group>.btn:first-child,.btn-group>.btn:last-child { + .rounded-corners(0); +} +.btn-group > .btn:last-child, .btn-group > .dropdown-toggle { + .rounded-corners(0); +} +.table-condensed th, .table-condensed td { + padding: 7px 6px; +} + +.label, .badge { + text-shadow: none; +} + + +// form stylig + +form, input { + margin:0; +} + +.input-append .add-on, .input-prepend .add-on, .input-append .btn, .input-prepend .btn { + margin-left: -1px; + vertical-align: top; + .rounded-corners(0); +} + +.input-append input, .input-prepend input, .input-append select, .input-prepend select, .input-append .uneditable-input, .input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + font-size: 14px; + vertical-align: top; + .rounded-corners(0); +} + +.input-append .add-on, .input-prepend .add-on { + display: inline-block; + width: auto; + height: 25px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: 400; + line-height: 25px; + text-align: center; + text-shadow: 0 1px 0 #fff; + background-color: #eee; + border: 1px solid #ccc; +} + +.input-prepend .add-on:first-child, .input-prepend .btn:first-child { + .rounded-corners(0); +} + +input, textarea { + width: 350px; +} + +select, textarea, input[type="text"], input[type="password"], input[type="datetime"], input[type="datetime-local"], input[type="date"], input[type="month"], input[type="time"], input[type="week"], input[type="number"], input[type="email"], input[type="url"], input[type="search"], input[type="tel"], input[type="color"], .uneditable-input { + display: inline-block; + height: 25px; + padding: 4px 6px; + margin-bottom: 9px; + font-size: 14px; + line-height: 25px; + color: #555; + .rounded-corners(0); +} + +.form-horizontal .controls { + margin-left: 150px; +} + +.form-horizontal .control-label { + float: left; + width: 140px; + padding-top: 5px; + text-align: right; +} + +.form-actions { + background-color: transparent; + border: 0; +} + +.form-horizontal .form-actions { + padding-left: 150px; +} + +// nav styling + +.nav > li > a { + display: block; + font-weight: 200; + color: @color-links; +} + +.nav-list>li>a { + padding: 6px 15px; +} + + +.pagination ul > li > a, .pagination ul > li > span { + float: left; + padding: 4px 12px; + line-height: 20px; + text-decoration: none; + background-color: #ffffff; + border: 0px solid #dddddd; + border-left-width: 0; +} + +.pagination ul { + display: inline-block; + margin-bottom: 0; + margin-left: 0; + .rounded-corners(0); + -webkit-box-shadow: 0; + -moz-box-shadow: 0; + box-shadow: none; +} + +.pagination a, .pagination span { + float: left; + padding: 0 14px; + line-height: 48px; + text-decoration: none; + background-color: #fff; + // border: 1px solid #ddd; + border-left-width: 0; +} + +// pagination +.pagination a { + float: left; + padding: 0 14px; + line-height: 48px; + text-decoration: none; + border: 0; + border-left-width: 0; +} + +.pagination a { + line-height: 30px; +} + +.pagination ul > li:first-child > a, .pagination ul > li:first-child > span { + border-left-width: 0px; + .rounded-corners(0); +} + +.pagination ul > li:last-child > a, .pagination ul > li:last-child > span { + .rounded-corners(0); +} + +body.modal-open { + overflow: hidden; +} + +.btn.disabled { + opacity: 0.2; +} diff --git a/docker/customizations/any/temba/contacts/templatetags/contacts.py b/docker/customizations/any/temba/contacts/templatetags/contacts.py new file mode 100644 index 0000000000..4828ef32fb --- /dev/null +++ b/docker/customizations/any/temba/contacts/templatetags/contacts.py @@ -0,0 +1,226 @@ +from django import template +from django.utils.safestring import mark_safe + +from temba.campaigns.models import EventFire +from temba.channels.models import ChannelEvent +from temba.contacts.models import URN, ContactField, ContactURN +from temba.flows.models import FlowRun +from temba.ivr.models import IVRCall +from temba.mailroom.events import Event +from temba.msgs.models import DELIVERED, ERRORED, FAILED, IVR + +register = template.Library() + +URN_SCHEME_ICONS = { + URN.TEL_SCHEME: "icon-phone", + URN.TWITTER_SCHEME: "icon-twitter", + URN.TWITTERID_SCHEME: "icon-twitter", + URN.TWILIO_SCHEME: "icon-twilio_original", + URN.EMAIL_SCHEME: "icon-envelop", + URN.FACEBOOK_SCHEME: "icon-facebook", + URN.TELEGRAM_SCHEME: "icon-telegram", + URN.LINE_SCHEME: "icon-line", + URN.EXTERNAL_SCHEME: "icon-channel-external", + URN.FCM_SCHEME: "icon-fcm", + URN.FRESHCHAT_SCHEME: "icon-freshchat", + URN.WHATSAPP_SCHEME: "icon-whatsapp", + URN.PM_WHATSAPP_SCHEME: "icon-whatsapp", + URN.PM_TELEGRAM_SCHEME: "icon-telegram", + URN.PM_SIGNAL_SCHEME: "icon-signal pm-icon", + URN.PM_LINE_SCHEME: "icon-line", + URN.PM_VK_SCHEME: "icon-vk pm-icon", + URN.PM_VIBER_SCHEME: "icon-viber pm-icon", + URN.PM_TWITTER_SCHEME: "icon-twitter", + URN.PM_KAKAO_SCHEME: "icon-tembatoo-kakao pm-icon", + URN.PM_IMO_SCHEME: "icon-tembatoo-imo pm-icon", + URN.PM_FACEBOOK_SCHEME: "icon-facebook", + URN.PM_INSTAGRAM_SCHEME: "icon-tembatoo-instagram pm-icon", + URN.PM_MOBYX_SCHEME: "icon-tembatoo-mobyx pm-icon", + URN.PM_FBM_SCHEME: "icon-tembatoo-fbm pm-icon", + URN.PM_EMAIL_SCHEME: "icon-envelop", +} + +ACTIVITY_ICONS = { + Event.TYPE_AIRTIME_TRANSFERRED: "icon-cash", + Event.TYPE_BROADCAST_CREATED: "icon-bullhorn", + Event.TYPE_CALL_STARTED: "icon-phone", + Event.TYPE_CAMPAIGN_FIRED: "icon-clock", + Event.TYPE_CHANNEL_EVENT: "icon-power", + Event.TYPE_CHANNEL_EVENT + ":missed_incoming": "icon-call-incoming", + Event.TYPE_CHANNEL_EVENT + ":missed_outgoing": "icon-call-outgoing", + Event.TYPE_CONTACT_FIELD_CHANGED: "icon-pencil", + Event.TYPE_CONTACT_GROUPS_CHANGED: "icon-users", + Event.TYPE_CONTACT_LANGUAGE_CHANGED: "icon-language", + Event.TYPE_CONTACT_NAME_CHANGED: "icon-contact", + Event.TYPE_CONTACT_URNS_CHANGED: "icon-address-book", + Event.TYPE_EMAIL_SENT: "icon-envelop", + Event.TYPE_ERROR: "icon-warning", + Event.TYPE_FAILURE: "icon-warning", + Event.TYPE_FLOW_ENTERED: "icon-flow", + Event.TYPE_FLOW_EXITED + ":expired": "icon-clock", + Event.TYPE_FLOW_EXITED + ":interrupted": "icon-cancel-circle", + Event.TYPE_FLOW_EXITED + ":completed": "icon-checkmark", + Event.TYPE_INPUT_LABELS_ADDED: "icon-tags", + Event.TYPE_IVR_CREATED: "icon-call-outgoing", + Event.TYPE_MSG_CREATED: "icon-bubble-right", + Event.TYPE_MSG_CREATED + ":failed": "icon-bubble-notification", + Event.TYPE_MSG_CREATED + ":delivered": "icon-bubble-check", + Event.TYPE_MSG_RECEIVED: "icon-bubble-user", + Event.TYPE_MSG_RECEIVED + ":voice": "icon-call-incoming", + Event.TYPE_RUN_RESULT_CHANGED: "icon-bars", + Event.TYPE_TICKET_ASSIGNED: "icon-ticket", + Event.TYPE_TICKET_REOPENED: "icon-ticket", + Event.TYPE_TICKET_OPENED: "icon-ticket", + Event.TYPE_TICKET_CLOSED: "icon-ticket", + Event.TYPE_TICKET_NOTE_ADDED: "icon-pencil", + Event.TYPE_WEBHOOK_CALLED: "icon-cloud-upload", +} + +MSG_EVENTS = {Event.TYPE_MSG_CREATED, Event.TYPE_MSG_RECEIVED, Event.TYPE_IVR_CREATED, Event.TYPE_BROADCAST_CREATED} + +# events that are included in the summary view +SUMMARY_EVENTS = { + Event.TYPE_CALL_STARTED, + Event.TYPE_CAMPAIGN_FIRED, + Event.TYPE_FLOW_ENTERED, + Event.TYPE_FLOW_EXITED, + Event.TYPE_BROADCAST_CREATED, + Event.TYPE_IVR_CREATED, + Event.TYPE_MSG_CREATED, + Event.TYPE_MSG_RECEIVED, +} + +MISSING_VALUE = "--" + + +@register.filter +def contact_field(contact, arg): + field = ContactField.get_by_key(contact.org, arg.lower()) + if field is None: + return MISSING_VALUE + + value = contact.get_field_display(field) + return value or MISSING_VALUE + + +@register.filter +def short_name(contact, org): + return contact.get_display(org, short=True) + + +@register.filter +def name_or_urn(contact, org): + return contact.get_display(org) + + +@register.filter +def name(contact, org): + if contact.name: + return contact.name + elif org.is_anon: + return contact.anon_identifier + else: + return MISSING_VALUE + + +@register.filter +def format_urn(urn, org): + if org and org.is_anon: + return ContactURN.ANON_MASK_HTML + + if isinstance(urn, ContactURN): + return urn.get_display(org=org, international=True) + else: + return URN.format(urn, international=True) + + +@register.filter +def urn(contact, org): + contact_urn = contact.get_urn() + if contact_urn: + return format_urn(contact_urn, org) + else: + return MISSING_VALUE + + +@register.filter +def format_contact(contact, org): # pragma: needs cover + return contact.get_display(org=org) + + +@register.filter +def urn_icon(urn): + return URN_SCHEME_ICONS.get(urn.scheme, "") + + +@register.filter +def history_icon(event: dict) -> str: + event_type = event["type"] + variant = None + + if event_type == Event.TYPE_MSG_CREATED: + if event["status"] in (ERRORED, FAILED): + variant = "failed" + elif event["status"] == DELIVERED: + variant = "delivered" + + elif event_type == Event.TYPE_MSG_RECEIVED: + if event["msg_type"] == IVR: + variant = "voice" + + elif event_type == Event.TYPE_FLOW_EXITED: + if event["status"] == FlowRun.STATUS_INTERRUPTED: + variant = "interrupted" + elif event["status"] == FlowRun.STATUS_EXPIRED: + variant = "expired" + else: + variant = "completed" + + elif event_type == Event.TYPE_CHANNEL_EVENT: + if event["channel_event_type"] == ChannelEvent.TYPE_CALL_IN_MISSED: + variant = "missed_incoming" + elif event["channel_event_type"] == ChannelEvent.TYPE_CALL_OUT_MISSED: + variant = "missed_outgoing" + + if variant: + glyph_name = ACTIVITY_ICONS.get(event_type + ":" + variant) + else: + glyph_name = ACTIVITY_ICONS.get(event_type) + + return mark_safe(f'') + + +@register.filter +def history_user(user: dict) -> str: + name = " ".join([user.get("first_name"), user.get("last_name")]).strip() + if not name: + name = user.get("email") + return name + + +@register.filter +def history_class(event: dict) -> str: + event_type = event["type"] + classes = [] + + if event_type in MSG_EVENTS: + classes.append("msg") + + if event.get("status") in (ERRORED, FAILED): + classes.append("warning") + else: + classes.append("non-msg") + + if event_type == Event.TYPE_ERROR or event_type == "failure": + classes.append("warning") + elif event_type == Event.TYPE_WEBHOOK_CALLED and event["status"] != "success": + classes.append("warning") + elif event_type == Event.TYPE_CALL_STARTED and event["status"] == IVRCall.FAILED: + classes.append("warning") + elif event_type == Event.TYPE_CAMPAIGN_FIRED and event["fired_result"] == EventFire.RESULT_SKIPPED: + classes.append("skipped") + + if event_type not in SUMMARY_EVENTS: + classes.append("detail-event") + + return " ".join(classes) diff --git a/docker/customizations/any/temba/settings_collect_static.py b/docker/customizations/any/temba/settings_collect_static.py new file mode 100644 index 0000000000..0426c99d68 --- /dev/null +++ b/docker/customizations/any/temba/settings_collect_static.py @@ -0,0 +1,5 @@ +# Django settings when first running collectstatic +from temba.settings import * # noqa + + +# nothing to override, yet diff --git a/docker/customizations/any/temba/settings_compress_static.py b/docker/customizations/any/temba/settings_compress_static.py new file mode 100644 index 0000000000..b4c51f79b9 --- /dev/null +++ b/docker/customizations/any/temba/settings_compress_static.py @@ -0,0 +1,14 @@ +# Django settings when running compress +from temba.settings import * # noqa + + +COMPRESS_ENABLED = True +COMPRESS_OFFLINE = True +COMPRESS_CSS_HASHING_METHOD = "content" + +COMPRESS_URL = None +COMPRESS_ROOT = STATIC_ROOT + +COMPRESS_OFFLINE_CONTEXT = dict( + STATIC_URL=STATIC_URL, base_template="frame.html", brand=BRANDING[DEFAULT_BRAND], debug=False, testing=False +) diff --git a/docker/customizations/any/temba/storage.py b/docker/customizations/any/temba/storage.py new file mode 100644 index 0000000000..7bdb7a7932 --- /dev/null +++ b/docker/customizations/any/temba/storage.py @@ -0,0 +1,35 @@ +from whitenoise.storage import CompressedManifestStaticFilesStorage +from django.contrib.staticfiles.storage import ManifestStaticFilesStorage +from urllib.parse import unquote, urlsplit + + +class WhiteNoiseStaticFilesStorage(CompressedManifestStaticFilesStorage): + """ + Library, framework, and ancestor assets in tests are malformed or missing. + However, these assets are not used, so we need to ignore them during + static file processing/compression. the `self.manifest_strict` set to False + is supposed to handle that for us, however, due to a combination of older + WhiteNoise middleware (required due to the Python 3.6 we use) and Django + itself, we need to tweak a few of the methods to ignore malformed/missing + files. + """ + def __init__(self, *args, **kwargs): + self.manifest_strict = False + super().__init__(*args, **kwargs) + + def post_process(self, *args, **kwargs): + files = super(ManifestStaticFilesStorage, self).post_process(*args, **kwargs) + for name, hashed_name, processed in files: + yield name, hashed_name, processed + + def hashed_name(self, name, content=None, filename=None): + # `filename` is the name of file to hash if `content` isn't given. + # `name` is the base name to construct the new hashed filename from. + parsed_name = urlsplit(unquote(name)) + clean_name = parsed_name.path.strip() + filename = (filename and urlsplit(unquote(filename)).path.strip()) or clean_name + if content is None and not self.exists(filename): + # Handle self.manifest_strict = False since ancestors apparently don't. + return name + else: + return super().hashed_name(name, content, filename) diff --git a/docker/customizations/any/templates/contacts/contact_read.haml b/docker/customizations/any/templates/contacts/contact_read.haml index 9f042149a8..7a5d26ab94 100644 --- a/docker/customizations/any/templates/contacts/contact_read.haml +++ b/docker/customizations/any/templates/contacts/contact_read.haml @@ -521,12 +521,12 @@ return false; }); - $('.show-all.upcoming').live('click', function() { + $('.show-all.upcoming').on('click', function() { $(this).hide(); $('table.activity.upcoming tbody:nth-child(1)').show(); }); - $('.show-all.fields').live('click', function() { + $('.show-all.fields').on('click', function() { if ($(this).hasClass('expanded')) { $(this).removeClass('expanded'); $('.contact-fields').removeClass('expanded'); @@ -538,10 +538,10 @@ } }); - $('.time').live('mouseover', function() { + $('.time').on('mouseover', function() { $(this).children('.long').show(); $(this).children('.short').hide(); - }).live('mouseleave', function() { + }).on('mouseleave', function() { $(this).children('.short').show(); $(this).children('.long').hide(); }); @@ -576,7 +576,7 @@ %script {% if org_perms.contacts.contact_update %} - $('.contact-field-value.editable').live('click', function(evt) { + $('.contact-field-value.editable').on('click', function(evt) { var endpoint = '{% url "contacts.contact_update_fields" object.pk %}'; var field_id = $(this).parent().data('id'); var modax = document.querySelector("#update-custom-fields"); @@ -599,7 +599,7 @@ {% endif %} {% if org_perms.contacts.contact_delete %} - $(".contact-delete-button").live('click', function() { + $(".contact-delete-button").on('click', function() { $(".gear-menu").removeClass("open"); modal = new ConfirmationModal($('.deletion > .title').html(), $('.deletion > .body').html()); modal.addClass('alert'); diff --git a/docker/customizations/any/web-static.sh b/docker/customizations/any/web-static.sh index 30684aa311..b712dea7ae 100755 --- a/docker/customizations/any/web-static.sh +++ b/docker/customizations/any/web-static.sh @@ -16,14 +16,13 @@ PATH=node_modules/coffeescript/bin:$PATH set -ex # fail on any error & print commands as they're run -echo 'Clearing compressor cache...' +echo 'Clearing static cache...' python clear-compressor-cache.py -echo 'Finished clearing compressor cache.' +echo 'Finished clearing static cache.' + +echo 'Collecting compressed static website files...' +python manage.py collectstatic --noinput --settings=temba.settings_collect_static echo 'Compressing static website files...' -python manage.py compress --extension=".haml" --force -v0 +python manage.py compress --extension=".haml" --force -v0 --settings=temba.settings_compress_static echo 'Finished compressing static website files.' - -echo 'Collecting compressed static website files...' -python manage.py collectstatic --noinput --no-post-process -echo 'Finished collecting compressed static website files.' diff --git a/docker/customizations/engage/static/brands/engage/less/engage.less b/docker/customizations/engage/static/brands/engage/less/engage.less index 96907eab6b..c87a252c7e 100644 --- a/docker/customizations/engage/static/brands/engage/less/engage.less +++ b/docker/customizations/engage/static/brands/engage/less/engage.less @@ -78,6 +78,7 @@ body { margin-top: 3px; color: var(--color-nav-unselected); align-self: center; + user-select: text; } } #menu-area { @@ -132,3 +133,10 @@ body { width: 100%; } } + +/* refresh uses ::before to add unicode char and font */ +/* just display bkg-img for pm-only icons */ +[class^="icon-"]:not(.pm-icon), +[class*=" icon-"]:not(.pm-icon) { + background-image: none; +} diff --git a/docker/customizations/engage/temba/settings.py b/docker/customizations/engage/temba/settings.py index e5b8501898..f50f08e80f 100644 --- a/docker/customizations/engage/temba/settings.py +++ b/docker/customizations/engage/temba/settings.py @@ -12,7 +12,6 @@ from django.utils.translation import ugettext_lazy as _ from temba.settings_common import * # noqa -AWS_QUERYSTRING_EXPIRE = '157784630' SUB_DIR = env('SUB_DIR', required=False) COURIER_URL = env('COURIER_URL', 'http://localhost:8080') DEFAULT_TPS = env('DEFAULT_TPS', 10) # Default Transactions Per Second for newly create Channels. @@ -28,9 +27,6 @@ if POST_MASTER_DL_QRCODE is not None and not POST_MASTER_DL_QRCODE.startswith("data:"): POST_MASTER_DL_QRCODE = "data:png;base64, {}".format(POST_MASTER_DL_QRCODE) -if SUB_DIR is not None and len(SUB_DIR) > 0: - MEDIA_URL = "{}{}".format(SUB_DIR, MEDIA_URL) - MAILROOM_URL=env('MAILROOM_URL', 'http://localhost:8000') INSTALLED_APPS = ( @@ -75,19 +71,52 @@ LOGGING['root']['level'] = env('DJANGO_LOG_LEVEL', 'INFO') AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME', '') -AWS_BUCKET_DOMAIN = env('AWS_BUCKET_DOMAIN', AWS_STORAGE_BUCKET_NAME + '.s3.amazonaws.com') -CDN_DOMAIN_NAME = env('CDN_DOMAIN_NAME', '') -AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID', '') -AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY', '') -AWS_S3_REGION_NAME = env('AWS_S3_REGION_NAME', '') +AWS_S3_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID', '') +AWS_S3_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY', '') +AWS_S3_REGION_NAME = env('AWS_S3_REGION_NAME', 'us-east-1') +IS_AWS_S3_REGION_DEFAULT = bool(AWS_S3_REGION_NAME == 'us-east-1') AWS_SIGNED_URL_DURATION = int(env('AWS_SIGNED_URL_DURATION', '1800')) AWS_DEFAULT_ACL = env('AWS_DEFAULT_ACL', '') AWS_LOCATION = env('AWS_LOCATION', '') -AWS_STATIC = env('AWS_STATIC', bool(AWS_STORAGE_BUCKET_NAME)) -AWS_MEDIA = env('AWS_MEDIA', bool(AWS_STORAGE_BUCKET_NAME)) -STORAGE_URL = "https://"+AWS_BUCKET_DOMAIN +AWS_QUERYSTRING_EXPIRE = '157784630' #unsure why this is hardcoded as a string +AWS_STATIC = bool(AWS_STORAGE_BUCKET_NAME) and env('AWS_STATIC', False) +AWS_MEDIA = bool(AWS_STORAGE_BUCKET_NAME) and env('AWS_MEDIA', True) +AWS_S3_USE_SSL = bool(env('AWS_S3_USE_SSL', True)) +AWS_S3_HTTP_SCHEME = "https" if AWS_S3_USE_SSL else "http" +AWS_S3_VERIFY = env('AWS_S3_VERIFY', False) +AWS_S3_CUSTOM_DOMAIN_NAME = env('AWS_S3_CUSTOM_DOMAIN_NAME', None) +AWS_S3_CUSTOM_URL = env('AWS_S3_CUSTOM_URL', None) if AWS_STORAGE_BUCKET_NAME: + + if AWS_S3_CUSTOM_URL: + #TODO FUTURE, IF NEEDED: if there's replacements in string, do them + AWS_S3_ENDPOINT_URL = AWS_S3_CUSTOM_URL + AWS_S3_URL = AWS_S3_ENDPOINT_URL + else: + if AWS_S3_CUSTOM_DOMAIN_NAME: + AWS_S3_CUSTOM_DOMAIN = AWS_S3_CUSTOM_DOMAIN_NAME + AWS_S3_DOMAIN = AWS_S3_CUSTOM_DOMAIN_NAME + else: + theRegionDomainSegment = f".{AWS_S3_REGION_NAME}" if not IS_AWS_S3_REGION_DEFAULT else '' + # middleware still expects us-east-1 in special domain format with bucket leading the subdomain (legacy format). + theDefaultRegionDomainSegment = f"{AWS_STORAGE_BUCKET_NAME}." if IS_AWS_S3_REGION_DEFAULT else '' + # useful to override for local dev and hosts file entries + # e.g. 127.0.0.1 s3.dev.nostromo.box + # 127.0.0.1 s3.us-gov-west-1.dev.nostromo.box + theBaseDomain = env('AWS_BASE_DOMAIN', 'amazonaws.com') + # useful for local dev ngnix proxies, eg. 'location ^~ /s3/ {' + thePathPrefix = env('AWS_S3_PATH_PREFIX', '') # MUST START WITH A SLASH, eg. '/s3' + AWS_S3_DOMAIN = f"{theDefaultRegionDomainSegment}s3{theRegionDomainSegment}.{theBaseDomain}{thePathPrefix}" + + # middleware still expects us-east-1 in special domain format with bucket leading the subdomain (legacy format). + theBucketPathSegment = f"/{AWS_STORAGE_BUCKET_NAME}" if not IS_AWS_S3_REGION_DEFAULT else '' + AWS_S3_URL = f"{AWS_S3_HTTP_SCHEME}://{AWS_S3_DOMAIN}{theBucketPathSegment}" + + # explicity remove any trailing slash + if AWS_S3_URL.endswith('/'): + AWS_S3_URL = AWS_S3_URL[:-1] + # Tell django-storages that when coming up with the URL for an item in S3 storage, keep # it simple - just use this domain plus the path. (If this isn't set, things get complicated). # This controls how the `static` template tag from `staticfiles` gets expanded, if you're using it. @@ -96,45 +125,50 @@ # django-storages. Use our own setting for the domain instead, which is unknown to # django-storages. - if CDN_DOMAIN_NAME: - AWS_S3_DOMAIN = CDN_DOMAIN_NAME - else: - AWS_S3_DOMAIN = '%s.s3.amazonaws.com' % AWS_STORAGE_BUCKET_NAME - if AWS_STATIC: # This is used by the `static` template tag from `static`, if you're using that. Or if anything else # refers directly to STATIC_URL. So it's safest to always set it. - STATIC_URL = "https://%s/" % AWS_S3_DOMAIN + STATIC_URL = AWS_S3_URL # Tell the staticfiles app to use S3Boto storage when writing the collected static files (when # you run `collectstatic`). STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' - COMPRESS_STORAGE = STATICFILES_STORAGE if AWS_MEDIA: - MEDIA_URL = "https://s3.amazonaws.com/%s/media/" % (AWS_STORAGE_BUCKET_NAME) - DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' + STORAGE_URL = AWS_S3_URL + MEDIA_URL = f"{AWS_S3_URL}/media/" + +if not AWS_MEDIA: + if SUB_DIR is not None and len(SUB_DIR) > 0: + MEDIA_URL = f"{SUB_DIR}{MEDIA_URL}" + STORAGE_URL = MEDIA_URL[:-1] if not AWS_STATIC: if SUB_DIR is not None and len(SUB_DIR) > 0: STATIC_URL = '/' + SUB_DIR + '/sitestatic/' - else: - STATIC_URL = '/sitestatic/' - STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' - MIDDLEWARE = list(MIDDLEWARE) + ['whitenoise.middleware.WhiteNoiseMiddleware'] -COMPRESS_ENABLED = env('DJANGO_COMPRESSOR', 'on') == 'on' -COMPRESS_OFFLINE = False + # @see whitenoise middleware usage: https://whitenoise.evans.io/en/stable/django.html + STATICFILES_STORAGE = 'temba.storage.WhiteNoiseStaticFilesStorage' + # insert just after security middleware (which is at idx 0) + MIDDLEWARE = MIDDLEWARE[:1] + ('whitenoise.middleware.WhiteNoiseMiddleware',) + MIDDLEWARE[1:] + WHITENOISE_MANIFEST_STRICT = False + +STATIC_ROOT = os.path.join(PROJECT_DIR, "../sitestatic/") -COMPRESS_URL = STATIC_URL -# Use MEDIA_ROOT rather than STATIC_ROOT because it already exists and is -# writable on the server. It's also the directory where other cached files -# (e.g., translations) are stored -COMPRESS_ROOT = STATIC_ROOT -COMPRESS_CSS_HASHING_METHOD = 'content' -COMPRESS_OFFLINE_MANIFEST = 'manifest-%s.json' % env('RAPIDPRO_VERSION', required=True) +COMPRESS_ENABLED = env('DJANGO_COMPRESSOR', 'on') == 'on' +if COMPRESS_ENABLED: + COMPRESS_URL = STATIC_URL + COMPRESS_ROOT = STATIC_ROOT + #COMPRESS_STORAGE = STATICFILES_STORAGE + +COMPRESS_OFFLINE_MANIFEST = f"manifest-{env('VERSION_CI', '1-dev')[:-4]}.json" +# If COMPRESS_OFFLINE is False, compressor will look in COMPRESS_STORAGE for +# previously processed results, but if not found, will create them on the fly +# and save them to use again. +#COMPRESS_OFFLINE = False +COMPRESS_OFFLINE = COMPRESS_ENABLED MAGE_AUTH_TOKEN = env('MAGE_AUTH_TOKEN', None) MAGE_API_URL = env('MAGE_API_URL', 'http://localhost:8026/api/v1') @@ -173,6 +207,8 @@ 'name': env('BRANDING_NAME', 'Pulse'), 'title': env('BRANDING_TITLE', 'Engage'), 'org': env('BRANDING_ORG', 'IST'), + 'meta_desc': 'Pulse Engage', + 'meta_author': 'IST Research Corp', 'colors': dict([rule.split('=') for rule in env('BRANDING_COLORS', 'primary=#0c6596').split(';')]), 'styles': ['brands/engage/font/style.css', 'brands/engage/less/style.less', 'fonts/style.css'], 'final_style': 'brands/engage/less/engage.less', diff --git a/docker/customizations/engage/templates/frame.haml b/docker/customizations/engage/templates/frame.haml index 6ca037a410..45836c95ab 100644 --- a/docker/customizations/engage/templates/frame.haml +++ b/docker/customizations/engage/templates/frame.haml @@ -1,3 +1,4 @@ +{% load static %} -load humanize i18n smartmin sms compress @@ -12,68 +13,51 @@ -else {{ brand.name }} - %link{href:'{{STATIC_URL}}fonts/roboto/stylesheet.css', rel:'stylesheet', type:'text/css'} - %link{href:'{{STATIC_URL}}brands/engage/images/engage.ico', rel:'shortcut icon', type:'image/png'} + %link{href:"{% static 'fonts/roboto/stylesheet.css' %}", rel:'stylesheet', type:'text/css'} + %link{href:"{% static 'brands/engage/images/engage.ico' %}", rel:'shortcut icon', type:'image/png'} %meta{charset:"utf-8"} %meta{name:"viewport", content:"width=device-width, initial-scale=1.0"} - %meta{name:"description", content:"{% block page-description %}Pulse Engage{% endblock %}"} - %meta{name:"author", content:"IST Research Corp"} + %meta{name:"description", content:"{% block page-description %}{{ brand.meta_desc }}{% endblock %}"} + %meta{name:"author", content:"{{ brand.meta_author }}"} %meta{http-equiv:"X-UA-Compatible", content:"IE=10"} - :javascript - - window.supportEmail = '{{brand.support_email}}'; - window.subdir = '{{brand.sub_dir}}'; - function conditionalLoad(local, remote) { - if (local != null && (window.location.hostname == "localhost" || remote == null)) { - loadResource("{{ STATIC_URL }}" + local); - } else if (remote != null) { - loadResource(remote); - } - } - - function loadResource(src) { - (function() { document.write(unescape('%3Cscript src="' + src + '"%3E%3C/script%3E')); })(); - } - - // ==== JQUERY ==== - :javascript - conditionalLoad('bower/jquery/jquery.js', null); - conditionalLoad('bower/jquery-migrate/jquery-migrate.min.js', null); - -# this view make it possible to process translations from javascript land -compress js :javascript - var static_url = '{{STATIC_URL}}'; - - %script{src:"{{ STATIC_URL }}bower/toastr/toastr.js"} - %script{src:"{{ STATIC_URL }}bower/bootstrap/js/bootstrap-modal.js"} - %script{src:"{{ STATIC_URL }}bower/bootstrap/js/bootstrap-dropdown.js"} - %script{src:"{{ STATIC_URL }}bower/bootstrap/js/bootstrap-tooltip.js"} - %script{src:"{{ STATIC_URL }}bower/bootstrap/js/bootstrap-tab.js"} - %script{src:"{{ STATIC_URL }}bower/intercooler-js/src/intercooler.js"} - %script{src:"{{ STATIC_URL }}bower/moment/moment.js"} - %script{src:"{{ STATIC_URL }}bower/moment-timezone/builds/moment-timezone-with-data.js"} - %script{src:"{{ STATIC_URL }}bower/featherlight/src/featherlight.js"} - %script{src:"{{ STATIC_URL }}bower/video.js/dist/video.js"} - %script{src:"{{ STATIC_URL }}bower/videojs-vjsdownload/dist/videojs-vjsdownload.js"} - %script{src:"{{ STATIC_URL }}bower/xregexp/xregexp-all.js"} + window.supportEmail = "{{ brand.support_email }}"; + window.subdir = "{{ brand.sub_dir }}"; + var static_url = "{{ STATIC_URL }}"; + + %script{src:"{% static 'bower/jquery/jquery.min.js' %}"} + %script{src:"{% static 'bower/jquery-migrate/jquery-migrate.min.js' %}"} + %script{src:"{% static 'bower/toastr/toastr.js' %}"} + %script{src:"{% static 'bower/bootstrap/js/bootstrap-modal.js' %}"} + %script{src:"{% static 'bower/bootstrap/js/bootstrap-dropdown.js' %}"} + %script{src:"{% static 'bower/bootstrap/js/bootstrap-tooltip.js' %}"} + %script{src:"{% static 'bower/bootstrap/js/bootstrap-tab.js' %}"} + %script{src:"{% static 'bower/intercooler-js/src/intercooler.js' %}"} + %script{src:"{% static 'bower/moment/moment.js' %}"} + %script{src:"{% static 'bower/moment-timezone/builds/moment-timezone-with-data.js' %}"} + %script{src:"{% static 'bower/featherlight/src/featherlight.js' %}"} + %script{src:"{% static 'bower/video.js/dist/video.js' %}"} + %script{src:"{% static 'bower/videojs-vjsdownload/dist/videojs-vjsdownload.js' %}"} + %script{src:"{% static 'bower/xregexp/xregexp-all.js' %}"} // expanded nav dropdown - %script{src:"{{ STATIC_URL }}bower/jquery-hoverintent/jquery.hoverIntent.js"} + %script{src:"{% static 'bower/jquery-hoverintent/jquery.hoverIntent.js' %}"} // Non-bower packages - %script{src:"{{ STATIC_URL }}lib/uuid.js"} - %script{src:"{{ STATIC_URL }}lib/bootstrap-limit.js"} + %script{src:"{% static 'lib/uuid.js' %}"} + %script{src:"{% static 'lib/bootstrap-limit.js' %}"} -if not debug and not testing - %script{src:"{{ STATIC_URL }}lib/raven.min.js"} + %script{src:"{% static 'lib/raven.min.js' %}"} -compress js - %script{src:"{{ STATIC_URL }}js/temba.js"} - %script{src:"{{ STATIC_URL }}js/labels.js"} - %script{src:"{{ STATIC_URL }}js/formax.js"} + %script{src:"{% static 'js/temba.js' %}"} + %script{src:"{% static 'js/labels.js' %}"} + %script{src:"{% static 'js/formax.js' %}"} -include "includes/frame_top.html" @@ -82,31 +66,31 @@ -# Favicon works without the need to be explicit -# -if brand.favico - -# %link{type:"image/ico", rel:"shortcut icon", href:"{{ STATIC_URL }}{{ brand.favico }}"} + -# %link{type:"image/ico", rel:"shortcut icon", href:"{% static brand.favico %}"} -# -else - -# %link{type:"image/ico", rel:"shortcut icon", href:"{{ STATIC_URL }}images/favicon.ico"} + -# %link{type:"image/ico", rel:"shortcut icon", href:"{% static 'images/favicon.ico' %}"} -block styles -# do not want to use non-local fonts -# %link{rel:'stylesheet', href:'https://fonts.googleapis.com/css?family=Roboto+Mono:300|Roboto:200,300,400,500'} -compress css - %link{type:'text/css', rel:'stylesheet', href:'{{ STATIC_URL }}bower/bootstrap-css/css/bootstrap.css', media:'all' } + %link{type:'text/css', rel:'stylesheet', href:"{% static 'bower/bootstrap-css/css/bootstrap.css' %}", media:'all' } -compress css - %link{type:'text/css', rel:'stylesheet', href:"{{ STATIC_URL }}bower/toastr/toastr.css", media:'all'} - %link{type:'text/css', rel:'stylesheet', href:"{{ STATIC_URL }}bower/featherlight/src/featherlight.css", media:'all'} - %link{type:'text/css', rel:'stylesheet', href:"{{ STATIC_URL }}bower/video.js/dist/video-js.css", media:'all'} - %link{type:'text/css', rel:'stylesheet', href:"{{ STATIC_URL }}bower/videojs-vjsdownload/dist/videojs-vjsdownload.css", media:'all'} + %link{type:'text/css', rel:'stylesheet', href:"{% static 'bower/toastr/toastr.css' %}", media:'all'} + %link{type:'text/css', rel:'stylesheet', href:"{% static 'bower/featherlight/src/featherlight.css' %}", media:'all'} + %link{type:'text/css', rel:'stylesheet', href:"{% static 'bower/video.js/dist/video-js.css' %}", media:'all'} + %link{type:'text/css', rel:'stylesheet', href:"{% static 'bower/videojs-vjsdownload/dist/videojs-vjsdownload.css' %}", media:'all'} -compress css - %link{type:'text/css', rel:'stylesheet', href:"{{ STATIC_URL }}fonts/icons/style.css", media:'all'} - %link{type:'text/less', rel:'stylesheet', href:'{{STATIC_URL}}less/print.less', media:'print'} - %link{type:'text/css', rel:'stylesheet', href:"{{ STATIC_URL }}fonts/tembatoo/style.css", media:'all'} + %link{type:'text/css', rel:'stylesheet', href:"{% static 'fonts/icons/style.css' %}", media:'all'} + %link{type:'text/less', rel:'stylesheet', href:"{% static 'less/print.less' %}", media:'print'} + %link{type:'text/css', rel:'stylesheet', href:"{% static 'fonts/tembatoo/style.css' %}", media:'all'} - %link{type:"text/css", rel:"stylesheet", href:"{{ STATIC_URL }}css/temba-components.css"} + %link{type:"text/css", rel:"stylesheet", href:"{% static 'css/temba-components.css' %}"} -# no reason to list this twice - -# %link{type:'text/css', rel:'stylesheet', href:"{{ STATIC_URL }}fonts/tembatoo/style.css", media:'all'} + -# %link{type:'text/css', rel:'stylesheet', href:"{% static 'fonts/tembatoo/style.css' %}", media:'all'} -compress css @@ -123,15 +107,15 @@ // any additional brand styling such as fonts, etc -for style in brand.styles -if 'less' in style - %link{type:'text/less', rel:'stylesheet', href:'{{STATIC_URL}}{{style}}', media:'all'} + %link{type:'text/less', rel:'stylesheet', href:"{% static style %}", media:'all'} -else - %link{type:'text/css', rel:'stylesheet', href:"{{ STATIC_URL }}{{style}}", media:'all'} + %link{type:'text/css', rel:'stylesheet', href:"{% static style %}", media:'all'} -compress css - %link{rel:"stylesheet", href:"{{ STATIC_URL }}css/tailwind.css", type:"text/css"} - %link{rel:"stylesheet", href:"{{ STATIC_URL }}less/refresh.less", type:"text/less"} + %link{rel:"stylesheet", href:"{% static 'css/tailwind.css' %}", type:"text/css"} + %link{rel:"stylesheet", href:"{% static 'less/refresh.less' %}", type:"text/less"} -if brand.final_style - %link{rel:"stylesheet", href:"{{ STATIC_URL }}{{brand.final_style}}", type:"text/less"} + %link{rel:"stylesheet", href:"{% static brand.final_style %}", type:"text/less"} -block extra-less -block extra-style @@ -169,7 +153,7 @@ %a#big-logo.logo.icon-logo{href:"{% if brand.logo_link %}{{brand.logo_link}}{%else%}/{%endif%}", title:"{{brand.name}}", class:"hover:no-underline"} .name {{brand.name}} - %img{ src:"{{ STATIC_URL }}{{brand.logo}}" } + %img{ src:"{% static brand.logo %}" } %span.version-str {% if user.is_authenticated and brand.version %}{{brand.version}}{%else%} {%endif%} @@ -322,7 +306,7 @@ -block extra-script - +
{% csrf_token %} @@ -336,26 +320,31 @@ $(".btn").tooltip(); - $('#btn-org-list').live('click', function(evt) { - evt.stopPropagation(); - var header = $('.org-header'); - if (header.hasClass('expanded')) { - header.removeClass('expanded'); - } else { - header.addClass('expanded'); - } - }); - - $('#org-name').live('click', function(evt) { - url = '{% url "orgs.org_home" %}'; - evt.stopPropagation(); - if (evt.ctrlKey || evt.metaKey){ - window.open(url,'_blank') - } else { - window.location = url; - } + var theOrgListBtn = $('#btn-org-list'); + if ( theOrgListBtn ) { + theOrgListBtn.on('click', function(evt) { + evt.stopPropagation(); + var header = $('.org-header'); + if (header.hasClass('expanded')) { + header.removeClass('expanded'); + } else { + header.addClass('expanded'); + } + }); + } - }); + var theOrgNameArea = $('#org-name'); + if ( theOrgNameArea ) { + theOrgNameArea.on('click', function(evt) { + var theUrl = '{% url "orgs.org_home" %}'; + evt.stopPropagation(); + if (evt.ctrlKey || evt.metaKey){ + window.open(theUrl,'_blank') + } else { + window.location = theUrl; + } + }); + } $('#menu .more').hoverIntent({ over:function() { @@ -384,9 +373,7 @@ handlePosterize(ele); }); - }); {% endblock body %} - diff --git a/pyproject.toml b/pyproject.toml index a5366560ac..f66337f349 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dicttoxml = "1.7.4" django-getenv = "1.3.1" django-cache-url = "1.3.1" uwsgi = "2.0.18" -whitenoise = "4.0" +whitenoise = "^5.1" [tool.poetry.dev-dependencies] diff --git a/scm/src-refresh.sh b/scm/src-refresh.sh index cb7c9bb3cf..db6f56a5e1 100755 --- a/scm/src-refresh.sh +++ b/scm/src-refresh.sh @@ -20,13 +20,10 @@ # Required: mount docker/customizations/ to /opt/dev/brand # - /DevCode/rapidpro/docker/customizations/engage:/opt/dev/brand # -# Once you have your volume mounts added, you just need to run -# two commands to refresh your docker container to use your newly -# modified code. First, execute this script in the container: -# -# $ docker-compose exec engage /opt/dev/src-refresh.sh -# -# then restart the container itself, e.g. +# Once you have your volume mounts added, you just need to restart +# the container itself whenever you want update your browser view. +# The restart script will copy over the "any" and "brand" flavor +# code as well as re-collect the static files for you. e.g. # # $ docker-compose restart engage # @@ -57,3 +54,8 @@ if [ -d "$SRC" ]; then echo "Refreshing brand customizations" rsync -a "${SRC}/" /rapidpro/ fi + +# re-collect all the sitestatic assets +echo "Refreshing static files" +source /venv/bin/activate; REDIS_URL=redis://redis DATABASE_URL=postgres://bla SECRET_KEY=123 \ + python manage.py collectstatic --noinput --settings=temba.settings_collect_static diff --git a/scm/utils-engage.sh b/scm/utils-engage.sh index 9848eaba70..1b18a672ff 100755 --- a/scm/utils-engage.sh +++ b/scm/utils-engage.sh @@ -115,9 +115,11 @@ function EnsurePyAppImage() set -x getVersionStr VERSION_CI=${VERSION_STR} + echo $VERSION_CI > "${UTILS_PATH}/version_ci_tag.txt" + set +x #if debugging, can add arg --progress=plain to the docker build command - docker build --build-arg FROM_STAGE_TAG=$FROM_STAGE_TAG \ + docker build --no-cache --build-arg FROM_STAGE_TAG=$FROM_STAGE_TAG \ --build-arg VERSION_CI=$VERSION_CI \ -t $IMAGE_NAME:$IMAGE_TAG -f ${DOCKERFILE2USE} . docker push $IMAGE_NAME:$IMAGE_TAG @@ -147,7 +149,7 @@ function BuildVersionForX() PrintPaddedTextRight " Using pyapp Tag" $FROM_STAGE_TAG ${COLOR_MSG_INFO} echo "Building Docker container ${IMAGE_NAME}:${IMAGE_TAG}..." #if debugging, can add arg --progress=plain to the docker build command - docker build --build-arg FROM_STAGE_TAG=$FROM_STAGE_TAG \ + docker build --no-cache --build-arg FROM_STAGE_TAG=$FROM_STAGE_TAG \ --build-arg VERSION_TAG=$IMG_TAG \ -t $IMAGE_NAME:$IMAGE_TAG -f ${DOCKERFILE2USE} . docker push $IMAGE_NAME:$IMAGE_TAG @@ -171,6 +173,11 @@ function BuildVersionForRp() function BuildVersionForEngage() { BuildVersionForX default engage $1 + IMAGE_NAME=$DEFAULT_IMAGE_NAME + VERSION_CI_TAG=`GetImgStageTag version_ci` + docker tag $IMAGE_NAME:$1 $IMAGE_NAME:$VERSION_CI_TAG + docker push $IMAGE_NAME:$VERSION_CI_TAG + PrintPaddedTextRight "Created Image" "$IMAGE_NAME:${VERSION_CI_TAG}" ${COLOR_MSG_INFO} } #################### diff --git a/temba/channels/types/postmaster/type.py b/temba/channels/types/postmaster/type.py index a5095da3c1..93a5091dc5 100644 --- a/temba/channels/types/postmaster/type.py +++ b/temba/channels/types/postmaster/type.py @@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from temba.channels.types.postmaster.views import ClaimView +from temba.contacts.models import URN from .. import TYPES @@ -71,7 +72,7 @@ def save(self, commit=True): pm_chat_mode = 'SMS' prefix = '' - schemes = [getattr(Contacts, + schemes = [getattr(URN, '{}{}_SCHEME'.format(prefix, dict(ClaimView.Form.CHAT_MODE_CHOICES)[pm_chat_mode]).upper())] self.object.schemes = schemes return super().save(commit=commit) diff --git a/temba/contacts/templatetags/contacts.py b/temba/contacts/templatetags/contacts.py index fca7804a1d..3af0468789 100644 --- a/temba/contacts/templatetags/contacts.py +++ b/temba/contacts/templatetags/contacts.py @@ -24,21 +24,6 @@ URN.FCM_SCHEME: "icon-fcm", URN.FRESHCHAT_SCHEME: "icon-freshchat", URN.WHATSAPP_SCHEME: "icon-whatsapp", - URN.PM_WHATSAPP_SCHEME: "icon-whatsapp", - URN.PM_TELEGRAM_SCHEME: "icon-telegram", - URN.PM_SIGNAL_SCHEME: "icon-signal", - URN.PM_LINE_SCHEME: "icon-line", - URN.PM_VK_SCHEME: "icon-vk", - URN.PM_VIBER_SCHEME: "icon-viber", - URN.PM_TWITTER_SCHEME: "icon-twitter", - URN.PM_KAKAO_SCHEME: "icon-tembatoo-kakao", - URN.PM_IMO_SCHEME: "icon-tembatoo-imo", - URN.PM_FACEBOOK_SCHEME: "icon-facebook", - URN.PM_INSTAGRAM_SCHEME: "icon-tembatoo-instagram", - URN.PM_MOBYX_SCHEME: "icon-tembatoo-mobyx", - URN.PM_FBM_SCHEME: "icon-tembatoo-fbm", - URN.PM_EMAIL_SCHEME: "icon-envelop", - URN.PM_ELEMENT_SCHEME: "icon-tembatoo-ele", } ACTIVITY_ICONS = {