Skip to content

Commit

Permalink
PWA updates (#795)
Browse files Browse the repository at this point in the history
Progressive Web Application
  • Loading branch information
dodumosu authored Jun 1, 2021
1 parent c85fd12 commit 6233c60
Show file tree
Hide file tree
Showing 51 changed files with 1,011 additions and 62,941 deletions.
8 changes: 6 additions & 2 deletions apollo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from urllib.parse import urlparse

from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
Expand All @@ -16,8 +17,8 @@
from loginpass import create_flask_blueprint, Facebook, Google
from werkzeug.urls import url_encode
from whitenoise import WhiteNoise
from apollo import assets, models, services, utils

from apollo import assets, models, services, settings, utils
from apollo.frontend import permissions, template_filters
from apollo.core import (
admin, csrf, db, docs, gravatar, menu, oauth, security, webpack
Expand Down Expand Up @@ -135,11 +136,14 @@ def frame_buster(response):
# content security policy
@app.after_request
def content_security_policy(response):
sentry_dsn = settings.SENTRY_DSN or ''
sentry_host = urlparse(sentry_dsn).netloc.split('@')[-1]
response.headers['Content-Security-Policy'] = "default-src 'self' blob: " + \
"*.googlecode.com *.google-analytics.com fonts.gstatic.com fonts.googleapis.com " + \
"*.googletagmanager.com " + \
"cdn.heapanalytics.com heapanalytics.com " + \
"'unsafe-inline' 'unsafe-eval' data:; img-src * data: blob:"
"'unsafe-inline' 'unsafe-eval' data:; img-src * data: blob:; " + \
f"connect-src 'self' {sentry_host}; "
return response

# automatic token refresh
Expand Down
2 changes: 1 addition & 1 deletion apollo/deployments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def overlapping_events(cls, event, timestamp=None):

cond1 = cls.start <= timestamp
cond2 = cls.end >= timestamp
cond3 = cls.id == event.id
cond3 = (cls.id == event.id) if event else (cls.id == None) # noqa
term = and_(cond1, cond2)

return cls.query.filter(or_(term, cond3))
14 changes: 2 additions & 12 deletions apollo/frontend/template_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
import pandas as pd

from apollo.process_analysis.common import generate_field_stats
from apollo.submissions.models import QUALITY_STATUSES
from apollo.submissions.qa.query_builder import get_inline_qa_status
from apollo.submissions.qa.query_builder import qa_status as _qa_status


def _clean(fieldname):
Expand Down Expand Up @@ -105,16 +104,7 @@ def reverse_dict(d):


def qa_status(submission, check):
result, tags = get_inline_qa_status(submission, check)
verified_fields = submission.verified_fields or set()
if result is True and not tags.issubset(verified_fields):
return QUALITY_STATUSES['FLAGGED']
elif result is True and tags.issubset(verified_fields):
return QUALITY_STATUSES['VERIFIED']
elif result is False:
return QUALITY_STATUSES['OK']
else:
return None
return _qa_status(submission, check)


def longitude(geom):
Expand Down
24 changes: 18 additions & 6 deletions apollo/frontend/views_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from zipfile import ZipFile, ZIP_DEFLATED

import magic
import pytz
from PIL import Image
from flask import flash, g, send_file, redirect, request, session
from flask_admin import (
form, BaseView, expose)
Expand All @@ -19,7 +21,6 @@
from flask_security.utils import hash_password, url_for_security
from flask_wtf.file import FileField
from jinja2 import contextfunction
import pytz
from slugify import slugify
from wtforms import (
BooleanField, PasswordField, SelectField, SelectMultipleField, validators)
Expand Down Expand Up @@ -50,6 +51,22 @@
DATETIME_FORMAT_SPEC = '%Y-%m-%d %H:%M:%S %Z'


def resize_logo(pil_image: Image):
background_color = (255, 255, 255, 0)

width, height = pil_image.size
if width == height:
return pil_image
elif width > height:
result = Image.new('RGBA', (width, width), background_color)
result.paste(pil_image, (0, (width - height) // 2))
return result
else:
result = Image.new('RBBA', (height, height), background_color)
result.paste(pil_image, ((height - width) // 2, 0))
return result


class MultipleSelect2Field(fields.Select2Field):
def iter_choices(self):
if self.allow_blank:
Expand Down Expand Up @@ -231,11 +248,6 @@ def _on_model_change(self, form, model, is_created):
if not mimetype.startswith('image'):
return

if 'svg' in mimetype:
model.brand_image = logo_file
model.brand_image_is_svg = True
return

logo_image = Image.open(BytesIO(logo_bytes))
resized_logo = resize_image(logo_image, 300)
with BytesIO() as buf:
Expand Down
76 changes: 74 additions & 2 deletions apollo/participants/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@
from flask_apispec import MethodResource, marshal_with, use_kwargs
from flask_babelex import gettext
from flask_jwt_extended import (
create_access_token, get_jwt, jwt_required, set_access_cookies,
unset_access_cookies)
create_access_token, get_jwt, get_jwt_identity, jwt_required,
set_access_cookies, unset_access_cookies)
from sqlalchemy import bindparam, func, or_, text, true
from sqlalchemy.orm.exc import NoResultFound
from webargs import fields

from apollo import settings
from apollo.api.common import BaseListResource
from apollo.api.decorators import protect
from apollo.core import csrf, red
from apollo.deployments.models import Event
from apollo.formsframework.api.schema import FormSchema
from apollo.formsframework.models import Form
from apollo.participants.api.schema import ParticipantSchema
from apollo.participants.models import (
Participant, ParticipantSet, ParticipantFirstNameTranslations,
ParticipantFullNameTranslations, ParticipantLastNameTranslations,
ParticipantOtherNamesTranslations)
from apollo.submissions.models import Submission


@marshal_with(ParticipantSchema)
Expand Down Expand Up @@ -203,3 +207,71 @@ def logout():
unset_access_cookies(resp)

return resp


def _get_form_data(participant):
# get incident forms
incident_forms = Form.query.join(Form.events).filter(
Event.participant_set_id == participant.participant_set_id,
Form.form_type == 'INCIDENT'
).with_entities(Form).order_by(Form.name, Form.id)

# get participant submissions
participant_submissions = Submission.query.filter(
Submission.participant_id == participant.id
).join(Submission.form)

# get checklist and survey forms based on the available submissions
non_incident_forms = participant_submissions.with_entities(
Form).distinct(Form.id)
checklist_forms = non_incident_forms.filter(Form.form_type == 'CHECKLIST')
survey_forms = non_incident_forms.filter(Form.form_type == 'SURVEY')

# get form serial numbers
form_ids_with_serials = participant_submissions.filter(
Form.form_type == 'SURVEY'
).with_entities(
Form.id, Submission.serial_no
).distinct(
Form.id, Submission.serial_no
).order_by(Form.id, Submission.serial_no)

all_forms = checklist_forms.all() + incident_forms.all() + \
survey_forms.all()

serials = [
{'form': pair[0], 'serial': pair[1]}
for pair in form_ids_with_serials]

return all_forms, serials


@csrf.exempt
@jwt_required()
def get_forms():
participant_uuid = get_jwt_identity()

try:
participant = Participant.query.filter_by(uuid=participant_uuid).one()
except NoResultFound:
response = {
'message': gettext('Invalid participant'),
'status': 'error'
}

return jsonify(response), HTTPStatus.BAD_REQUEST

forms, serials = _get_form_data(participant)

form_data = FormSchema(many=True).dump(forms).data

result = {
'data': {
'forms': form_data,
'serials': serials,
},
'message': gettext('ok'),
'status': 'ok'
}

return jsonify(result)
4 changes: 4 additions & 0 deletions apollo/participants/views_participants.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
view_func=api_views.logout,
methods=['DELETE']
)
bp.add_url_rule(
'/api/participants/forms',
view_func=api_views.get_forms,
)
bp.add_url_rule(
'/api/participants/<int:participant_id>',
view_func=api_views.ParticipantItemResource.as_view(
Expand Down
5 changes: 5 additions & 0 deletions apollo/pwa/static/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@
.form-signin .form-control:focus {
z-index: 2;
}

body {
background-color: #f0f8ff;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23b0c4de' fill-opacity='0.3' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E");
}
56 changes: 26 additions & 30 deletions apollo/pwa/static/js/app.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
window.isUpdateAvailable = new Promise((resolve, reject) => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/pwa/serviceworker.js', {scope: '/pwa/'})
.then(registration => {
console.log('service worker registered with scope:', registration.scope);
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// update available
resolve(true);
} else {
// no update available
resolve(false);
}
break;
}
};
};
})
.catch(error => console.error('service worker registration failed:', error));
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/pwa/serviceworker.js', {scope: '/pwa/'})
.then(registration => {
console.log('service worker registered successfully with scope:', registration.scope);

if ('SyncManager' in window)
console.log('Background sync supported');
else
console.log('Background sync not supported');
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// update available
resolve(true);
} else {
// no update available
resolve(false);
}

} else {
console.error('Can\'t use service workers');
}
});
break;
}
};
};
})
.catch(error => console.error('service worker registration failed:', error));
} else {
console.error('Cannot use service workers');
}
});
24 changes: 15 additions & 9 deletions apollo/pwa/static/js/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ class APIClient {
this.endpoints = endpoints;
}

_getResult = (response) => {
let status = response.status;
_getResult = function (response) {
return {
status: status,
ok: response.ok,
status: response.status,
result: response.json()
};
};

authenticate = (participant_id, password) => {
authenticate = function (participant_id, password) {
return fetch(this.endpoints.login, {
body: JSON.stringify({
participant_id: participant_id,
Expand All @@ -24,7 +24,7 @@ class APIClient {
}).then(this._getResult);
};

submit = (formData, csrf_token) => {
submit = function (formData, csrf_token) {
return fetch(this.endpoints.submit, {
body: formData,
credentials: 'same-origin',
Expand All @@ -35,17 +35,23 @@ class APIClient {
}).then(this._getResult);
};

getForms = (events) => {
const endpoint = (events === [] || events === undefined || events === null) ? this.endpoints.list : `${this.endpoints.list}?events=${events.join(',')}`;
return fetch(endpoint, {
getForms = function () {
return fetch(this.endpoints.list, {
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
}
}).then(this._getResult);
};

logout = (csrf_token) => {
checkQAStatus = function (submissionUUID) {
let endpoint = `${this.endpoints.qaStatus}${submissionUUID}`;
return fetch(endpoint, {
credentials: 'same-origin'
}).then(this._getResult);
}

logout = function (csrf_token) {
return fetch(this.endpoints.logout, {
credentials: 'same-origin',
headers: {
Expand Down
Loading

0 comments on commit 6233c60

Please sign in to comment.