Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open edit/preview Enketo Express links with KPI as OpenRosa server #3689

Merged
merged 20 commits into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)}',
jnm marked this conversation as resolved.
Show resolved Hide resolved
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
])
jnm marked this conversation as resolved.
Show resolved Hide resolved

# 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'))
jnm marked this conversation as resolved.
Show resolved Hide resolved

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