Skip to content

Commit

Permalink
Merge pull request #3689 from kobotoolbox/edit-with-openrosa-server
Browse files Browse the repository at this point in the history
Open edit/preview Enketo Express links with KPI as OpenRosa server
  • Loading branch information
jnm authored Feb 22, 2022
2 parents e2452d3 + a8a7930 commit 07ec8dc
Show file tree
Hide file tree
Showing 25 changed files with 708 additions and 252 deletions.
10 changes: 3 additions & 7 deletions jsapp/js/enketoHandler.es6
Original file line number Diff line number Diff line change
Expand Up @@ -59,23 +59,19 @@ const enketoHandler = {
resolve();
} else {
dataIntMethod(aid, sid)
.done((enketoData) => {
.always((enketoData) => {
if (enketoData.url) {
this._saveEnketoUrl(urlId, enketoData.url);
this._openEnketoUrl(urlId);
resolve();
} else {
let errorMsg = t('There was an error loading Enketo.');
if (enketoData.detail) {
errorMsg += `<br><code>${enketoData.detail}</code>`;
if (enketoData?.responseJSON?.detail) {
errorMsg += `<br><code>${enketoData.responseJSON.detail}</code>`;
}
notify(errorMsg, 'error');
reject();
}
})
.fail(() => {
notify(t('There was an error getting Enketo link'), 'error');
reject();
});
}
}).catch(() => {
Expand Down
6 changes: 3 additions & 3 deletions kobo/apps/mfa/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django import forms
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as t
from trench.serializers import CodeLoginSerializer
from trench.utils import (
get_mfa_model,
Expand Down Expand Up @@ -53,7 +53,7 @@ class MFATokenForm(forms.Form):
required=True,
widget=forms.TextInput(
attrs={
'placeholder': _(
'placeholder': t(
'Enter the ##token length##-character token'
).replace('##token length##', str(settings.TRENCH_AUTH['CODE_LENGTH']))
}
Expand All @@ -65,7 +65,7 @@ class MFATokenForm(forms.Form):
)

error_messages = {
'invalid_code': _(
'invalid_code': t(
'Your token is invalid'
)
}
Expand Down
3 changes: 3 additions & 0 deletions kobo/apps/reports/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@

SPECIFIC_REPORTS_KEY = 'specified'
DEFAULT_REPORTS_KEY = 'default'

FUZZY_VERSION_ID_KEY = '_version_'
INFERRED_VERSION_ID_KEY = '__inferred_version__'
15 changes: 8 additions & 7 deletions kobo/apps/reports/report_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

from django.utils.translation import gettext as t
from rest_framework import serializers

from formpack import FormPack

from kpi.utils.log import logging
from .constants import (
FUZZY_VERSION_ID_KEY,
INFERRED_VERSION_ID_KEY,
)


def build_formpack(asset, submission_stream=None, use_all_form_versions=True):
Expand All @@ -16,8 +20,6 @@ def build_formpack(asset, submission_stream=None, use_all_form_versions=True):
then only the newest version of the form is considered, and all submissions
are assumed to have been collected with that version of the form.
"""
FUZZY_VERSION_ID_KEY = '_version_'
INFERRED_VERSION_ID_KEY = '__inferred_version__'

if asset.has_deployment:
if use_all_form_versions:
Expand All @@ -38,9 +40,8 @@ def build_formpack(asset, submission_stream=None, use_all_form_versions=True):
except TypeError as e:
# https://github.com/kobotoolbox/kpi/issues/1361
logging.error(
'Failed to get formpack schema for version: %s'
% repr(e),
exc_info=True
f'Failed to get formpack schema for version: {repr(e)}',
exc_info=True
)
else:
fp_schema['version_id_key'] = INFERRED_VERSION_ID_KEY
Expand All @@ -58,7 +59,7 @@ def build_formpack(asset, submission_stream=None, use_all_form_versions=True):
# Find the AssetVersion UID for each deprecated reversion ID
_reversion_ids = dict([
(str(v._reversion_version_id), v.uid)
for v in _versions if v._reversion_version_id
for v in _versions if v._reversion_version_id
])

# A submission often contains many version keys, e.g. `__version__`,
Expand Down
4 changes: 3 additions & 1 deletion kobo/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,8 @@ def __init__(self, *args, **kwargs):
# http://apidocs.enketo.org/v2/
ENKETO_SURVEY_ENDPOINT = 'api/v2/survey/all'
ENKETO_PREVIEW_ENDPOINT = 'api/v2/survey/preview/iframe'
ENKETO_EDIT_INSTANCE_ENDPOINT = 'api/v2/instance'
ENKETO_VIEW_INSTANCE_ENDPOINT = 'api/v2/instance/view'


''' Celery configuration '''
Expand Down Expand Up @@ -852,4 +854,4 @@ def __init__(self, *args, **kwargs):
# Session Authentication is supported by default.
MFA_SUPPORTED_AUTH_CLASSES = [
'kpi.authentication.TokenAuthentication',
]
]
3 changes: 3 additions & 0 deletions kobo/settings/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@
MONGO_CONNECTION = MockMongoClient(
MONGO_CONNECTION_URL, j=True, tz_aware=True)
MONGO_DB = MONGO_CONNECTION['formhub_test']

ENKETO_URL = 'http://enketo.mock'
ENKETO_INTERNAL_URL = 'http://enketo.mock'
39 changes: 39 additions & 0 deletions kpi/authentication.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# coding: utf-8
from django.utils.translation import gettext as t
from django_digest import HttpDigestAuthenticator
from rest_framework.authentication import (
BaseAuthentication,
BasicAuthentication as DRFBasicAuthentication,
TokenAuthentication as DRFTokenAuthentication,
get_authorization_header,
)
from rest_framework.exceptions import AuthenticationFailed

from kpi.mixins.mfa import MFABlockerMixin

Expand All @@ -24,6 +29,40 @@ def authenticate_credentials(self, userid, password, request=None):
return user, _


class DigestAuthentication(MFABlockerMixin, BaseAuthentication):

verbose_name = 'Digest authentication'

def __init__(self):
self.authenticator = HttpDigestAuthenticator()

def authenticate(self, request):

auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'digest':
return None

if self.authenticator.authenticate(request):

# If user provided correct credentials but their account is
# disabled, return a 401
if not request.user.is_active:
raise AuthenticationFailed()

self.validate_mfa_not_active(request.user)

return request.user, None
else:
raise AuthenticationFailed(t('Invalid username/password'))

def authenticate_header(self, request):
response = self.build_challenge_response()
return response['WWW-Authenticate']

def build_challenge_response(self):
return self.authenticator.build_challenge_response()


class TokenAuthentication(MFABlockerMixin, DRFTokenAuthentication):
"""
Extend DRF class to support MFA.
Expand Down
79 changes: 40 additions & 39 deletions kpi/deployment_backends/base_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,33 @@ def connect(self, active=False):
def delete(self):
self.asset._deployment_data.clear() # noqa

@abc.abstractmethod
def delete_submission(self, submission_id: int, user: 'auth.User') -> dict:
pass

@abc.abstractmethod
def delete_submissions(self, data: dict, user: 'auth.User', **kwargs) -> dict:
pass

@abc.abstractmethod
def duplicate_submission(
self, submission_id: int, user: 'auth.User'
) -> dict:
pass

@abc.abstractmethod
def get_attachment(
self,
submission_id: int,
user: 'auth.User',
attachment_id: Optional[int] = None,
xpath: Optional[str] = None,
) -> tuple:
pass

def get_attachment_objects_from_dict(self, submission: dict) -> list:
pass

def get_data(
self, dotted_path: str = None, default=None
) -> Union[None, int, str, dict]:
Expand Down Expand Up @@ -98,52 +125,22 @@ def get_data(

return value

@abc.abstractmethod
def get_attachment(
self,
submission_id: int,
user: 'auth.User',
attachment_id: Optional[int] = None,
xpath: Optional[str] = None,
) -> tuple:
pass

@abc.abstractmethod
def delete_submission(self, submission_id: int, user: 'auth.User') -> dict:
pass

@abc.abstractmethod
def delete_submissions(self, data: dict, user: 'auth.User', **kwargs) -> dict:
pass

@abc.abstractmethod
def duplicate_submission(
self, submission_id: int, user: 'auth.User'
) -> dict:
pass

@abc.abstractmethod
def get_data_download_links(self):
pass

@abc.abstractmethod
def get_enketo_submission_url(
self, submission_id: int, user: 'auth.User', params: dict = None
) -> dict:
"""
Return a formatted dict to be passed to a Response object
"""
pass

@abc.abstractmethod
def get_enketo_survey_links(self):
pass

def get_submission(self,
submission_id: int,
user: 'auth.User',
format_type: str = SUBMISSION_FORMAT_TYPE_JSON,
**mongo_query_params: dict) -> Union[dict, str, None]:
def get_submission(
self,
submission_id: int,
user: 'auth.User',
format_type: str = SUBMISSION_FORMAT_TYPE_JSON,
request: Optional['rest_framework.request.Request'] = None,
**mongo_query_params: dict
) -> Union[dict, str, None]:
"""
Retrieve the corresponding submission whose id equals `submission_id`
and which `user` is allowed to access.
Expand All @@ -163,7 +160,11 @@ def get_submission(self,

submissions = list(
self.get_submissions(
user, format_type, [int(submission_id)], **mongo_query_params
user,
format_type,
[int(submission_id)],
request,
**mongo_query_params
)
)
try:
Expand Down
11 changes: 8 additions & 3 deletions kpi/deployment_backends/kc_access/shadow_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ class Meta(ShadowModel.Meta):
uuid = models.CharField(max_length=32, default='')
last_submission_time = models.DateTimeField(blank=True, null=True)
num_of_submissions = models.IntegerField(default=0)
kpi_asset_uid = models.CharField(max_length=32, null=True)

@property
def md5_hash(self):
Expand Down Expand Up @@ -566,12 +567,15 @@ class Meta(ReadOnlyModel.Meta):
db_index=True)
media_file_basename = models.CharField(
max_length=260, null=True, blank=True, db_index=True)
# `PositiveIntegerField` will only accomodate 2 GiB, so we should consider
# `PositiveIntegerField` will only accommodate 2 GiB, so we should consider
# `PositiveBigIntegerField` after upgrading to Django 3.1+
media_file_size = models.PositiveIntegerField(blank=True, null=True)
mimetype = models.CharField(
max_length=100, null=False, blank=True, default=''
)
# TODO: hide attachments that were deleted or replaced; see
# kobotoolbox/kobocat#792
# replaced_at = models.DateTimeField(blank=True, null=True)

@property
def absolute_mp3_path(self):
Expand Down Expand Up @@ -666,6 +670,7 @@ def _wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except ProgrammingError as e:
raise ProgrammingError('kc_access error accessing kobocat '
'tables: {}'.format(e.message))
raise ProgrammingError(
'kc_access error accessing kobocat tables: {}'.format(str(e))
)
return _wrapper
Loading

0 comments on commit 07ec8dc

Please sign in to comment.