diff --git a/.flake8 b/.flake8 index 45555e7a0d..55fb4e173f 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] max-line-length = 88 select = C,E,F,W,B,B950 -extend-ignore = E203, E501 +extend-ignore = E203,E501 per-file-ignores = __init__.py:F401 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1eda51018..7d993a785f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,14 @@ jobs: pip install mock pip install requests-mock + - name: Python Static Analysis + env: + DJANGO_SETTINGS_MODULE: onadata.settings.github_actions_test + run: | + pip install prospector + pip install -r requirements/azure.pip + prospector -X -s veryhigh onadata + - name: Run tests run: | python manage.py test ${{ matrix.testfolder }} --noinput --settings=onadata.settings.github_actions_test --verbosity=2 --parallel=4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..5feb00ad06 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,12 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black diff --git a/.prospector.yaml b/.prospector.yaml index 27ee1e3a30..ef872b4a17 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -5,10 +5,14 @@ autodetect: true member-warnings: false max-line-length: 88 +pycodestyle: + disable: + - E203 # Whitespace before ':' pylint: options: extension-pkg-allow-list: - ujson + - lxml.etree mccabe: run: false diff --git a/.pylintrc b/.pylintrc index 686428e7b6..056fb09489 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,7 +3,7 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code -extension-pkg-allow-list=ujson +extension-pkg-allow-list=ujson,lxml.etree # Add files or directories to the blacklist. They should be base names, not # paths. @@ -50,7 +50,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,too-few-public-methods +disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,too-few-public-methods,django-not-configured # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/onadata/__init__.py b/onadata/__init__.py index 6a6c8dff5c..fc79f9a9a0 100644 --- a/onadata/__init__.py +++ b/onadata/__init__.py @@ -11,6 +11,6 @@ # This will make sure the app is always imported when # Django starts so that shared_task will use this app. -from .celery import app as celery_app +from .celeryapp import app as celery_app __all__ = ("celery_app",) diff --git a/onadata/apps/api/management/commands/apply_can_add_project_perms.py b/onadata/apps/api/management/commands/apply_can_add_project_perms.py index b351c3fdca..f2a377ccaa 100644 --- a/onadata/apps/api/management/commands/apply_can_add_project_perms.py +++ b/onadata/apps/api/management/commands/apply_can_add_project_perms.py @@ -9,6 +9,7 @@ """ from django.core.management.base import BaseCommand from django.utils.translation import gettext as _ + from guardian.shortcuts import assign_perm from onadata.apps.api.models import OrganizationProfile @@ -24,9 +25,10 @@ def org_can_add_project_permission(): for organization in organizations.iterator(): permissions = organization.orgprofileuserobjectpermission_set.filter( - permission__codename='can_add_xform') + permission__codename="can_add_xform" + ) for permission in permissions: - assign_perm('can_add_project', permission.user, organization) + assign_perm("can_add_project", permission.user, organization) def user_can_add_project_permission(): @@ -38,9 +40,10 @@ def user_can_add_project_permission(): for user in users.iterator(): permissions = user.userprofileuserobjectpermission_set.filter( - permission__codename='can_add_xform') + permission__codename="can_add_xform" + ) for permission in permissions: - assign_perm('can_add_project', permission.user, user) + assign_perm("can_add_project", permission.user, user) class Command(BaseCommand): @@ -48,7 +51,8 @@ class Command(BaseCommand): Command apply_can_add_preject_perms - applys can_add_project permission to all users who have can_add_xform permission to a user/organization profile. """ - help = _(u"Apply can_add_project permissions") + + help = _("Apply can_add_project permissions") def handle(self, *args, **options): user_can_add_project_permission() diff --git a/onadata/apps/api/management/commands/assign_team_member_permission.py b/onadata/apps/api/management/commands/assign_team_member_permission.py index 9057244f3e..b2fe8d6a13 100644 --- a/onadata/apps/api/management/commands/assign_team_member_permission.py +++ b/onadata/apps/api/management/commands/assign_team_member_permission.py @@ -1,20 +1,26 @@ -from django.core.management.base import BaseCommand +# -*- coding: utf-8 -*- +""" +Assign permission to the member team +""" from django.core.exceptions import ObjectDoesNotExist +from django.core.management.base import BaseCommand from django.utils.translation import gettext as _ -from onadata.apps.api.models.team import Team +from guardian.shortcuts import assign_perm, get_perms_for_model + from onadata.apps.api.models.organization_profile import OrganizationProfile +from onadata.apps.api.models.team import Team from onadata.libs.utils.model_tools import queryset_iterator -from guardian.shortcuts import assign_perm, get_perms_for_model - class Command(BaseCommand): - args = '' - help = _(u"Assign permission to the member team") + """Assign permission to the member team""" + + args = "" + help = _("Assign permission to the member team") def handle(self, *args, **options): - self.stdout.write("Assign permission to the member team", ending='\n') + self.stdout.write("Assign permission to the member team", ending="\n") count = 0 fail = 0 @@ -24,38 +30,41 @@ def handle(self, *args, **options): org_name = args[0] org = OrganizationProfile.objects.get(user__username=org_name) - team = Team.objects.get(organization=org.user, - name=u'%s#%s' % ( - org.user.username, - 'members')) + team = Team.objects.get( + organization=org.user, name=f"{org.user.username}#members" % ("") + ) self.assign_perm(team, org) count += 1 total += 1 except ObjectDoesNotExist as e: fail += 1 - self.stdout.write(str(e), ending='\n') + self.stdout.write(str(e), ending="\n") else: # Get all the teams for team in queryset_iterator( - Team.objects.filter(name__contains='members')): + Team.objects.filter(name__contains="members") + ): self.assign_perm(team, team.organization) count += 1 total += 1 - self.stdout.write("Assigned {} of {} records. failed: {}". - format(count, total, fail), ending='\n') + self.stdout.write( + f"Assigned {count} of {total} records. failed: {fail}", ending="\n" + ) def assign_perm(self, team, org): + """Assign a team org permissions""" for perm in get_perms_for_model(Team): - org = org.user \ - if isinstance(org, OrganizationProfile) else org + org = org.user if isinstance(org, OrganizationProfile) else org assign_perm(perm.codename, org, team) if team.created_by: assign_perm(perm.codename, team.created_by, team) - if hasattr(org.profile, 'creator') and \ - org.profile.creator != team.created_by: - assign_perm(perm.codename, org.profile.creator, team) + if ( + hasattr(org.profile, "creator") + and org.profile.creator != team.created_by + ): + assign_perm(perm.codename, org.profile.creator, team) if org.profile.created_by != team.created_by: assign_perm(perm.codename, org.profile.created_by, team) diff --git a/onadata/apps/api/management/commands/cleanup_permissions.py b/onadata/apps/api/management/commands/cleanup_permissions.py index 8b34b59953..29ff362e3e 100644 --- a/onadata/apps/api/management/commands/cleanup_permissions.py +++ b/onadata/apps/api/management/commands/cleanup_permissions.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Cleanup permissions +""" from django.core.management.base import BaseCommand from django.utils.translation import gettext as _ from guardian.models import UserObjectPermission @@ -7,32 +11,28 @@ class Command(BaseCommand): - help = _(u"Cleanup permissions") + """Cleanup permissions""" + + help = _("Cleanup permissions") def handle(self, *args, **options): deleted = 0 self.stdout.write("Starting UserObject") - for perm in queryset_iterator( - UserObjectPermission.objects.select_related()): + for perm in queryset_iterator(UserObjectPermission.objects.select_related()): try: perm.content_object except AttributeError: perm.delete() deleted += 1 - self.stdout.write( - "deleted {} stale permission".format(deleted) - ) + self.stdout.write(f"deleted {deleted} stale permission") self.stdout.write("Starting GroupObject") - for perm in queryset_iterator( - GroupObjectPermission.objects.select_related()): + for perm in queryset_iterator(GroupObjectPermission.objects.select_related()): try: perm.content_object except AttributeError: perm.delete() deleted += 1 - self.stdout.write( - "deleted {} stale permission".format(deleted) - ) + self.stdout.write(f"deleted {deleted} stale permission") self.stdout.write( - "Total removed orphan object permissions instances: %d" % deleted + f"Total removed orphan object permissions instances: {deleted}" ) diff --git a/onadata/apps/api/management/commands/create_default_project.py b/onadata/apps/api/management/commands/create_default_project.py index 58ffbf029c..d9000ac093 100644 --- a/onadata/apps/api/management/commands/create_default_project.py +++ b/onadata/apps/api/management/commands/create_default_project.py @@ -1,18 +1,26 @@ +# -*- coding: utf-8 -*- +""" +Check for forms not in a project and move them to the default project +""" from django.conf import settings +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from django.contrib.auth.models import User from django.utils.translation import gettext as _ from onadata.apps.logger.models.project import Project from onadata.libs.utils.model_tools import queryset_iterator +User = get_user_model() XFORM_DEFAULT_PROJECT_ID = 1 class Command(BaseCommand): - help = _(u"Check for forms not in a project" - u" and move them to the default project") + """ + Check for forms not in a project and move them to the default project + """ + + help = _("Check for forms not in a project and move them to the default project") def handle(self, *args, **options): self.stdout.write("Task started ...") @@ -27,16 +35,19 @@ def handle(self, *args, **options): self.stdout.write("Task completed ...") def set_project_to_user_forms(self, user): - default_project_name = user.username + '\'s Project' + """Set default project for all user forms.""" + default_project_name = user.username + "'s Project" try: project = Project.objects.get(name=default_project_name) except Project.DoesNotExist: - metadata = {'description': 'Default Project'} - project = Project.objects.create(name=default_project_name, - organization=user, - created_by=user, - metadata=metadata) - self.stdout.write("Created project %s" % project.name) + metadata = {"description": "Default Project"} + project = Project.objects.create( + name=default_project_name, + organization=user, + created_by=user, + metadata=metadata, + ) + self.stdout.write(f"Created project {project.name}") finally: xforms = user.xforms.filter(project=XFORM_DEFAULT_PROJECT_ID) diff --git a/onadata/apps/api/management/commands/create_user_profiles.py b/onadata/apps/api/management/commands/create_user_profiles.py index bc89b3618c..b1df518a51 100644 --- a/onadata/apps/api/management/commands/create_user_profiles.py +++ b/onadata/apps/api/management/commands/create_user_profiles.py @@ -1,15 +1,18 @@ # -*- coding: utf-8 -*- """Management Command to add missing user profiles to users.""" +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from django.contrib.auth.models import User from django.utils.translation import gettext as _ from onadata.apps.main.models.user_profile import UserProfile from onadata.libs.utils.model_tools import queryset_iterator +User = get_user_model() + class Command(BaseCommand): """Create missing user profiles management command.""" + help = _("Build out missing user profiles") def handle(self, *args, **options): diff --git a/onadata/apps/api/management/commands/delete_users.py b/onadata/apps/api/management/commands/delete_users.py index c30e467883..6339f27e77 100644 --- a/onadata/apps/api/management/commands/delete_users.py +++ b/onadata/apps/api/management/commands/delete_users.py @@ -1,89 +1,80 @@ +# -*- coding: utf-8 -*- """ Delete users management command. """ import sys -from django.contrib.auth.models import User -from onadata.apps.logger.models import XForm -from onadata.apps.logger.models import Instance -from onadata.apps.logger.models import Project + +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError from django.utils import timezone +from onadata.apps.logger.models import Instance, Project, XForm + +User = get_user_model() -def get_user_object_stats( - username): # pylint: disable=R0201 + +def get_user_object_stats(username): """ Get User information. """ # Get the number of projects for this user - user_projects = Project.objects.filter( - created_by__username=username).count() + user_projects = Project.objects.filter(created_by__username=username).count() # Get the number of forms - user_forms = XForm.objects.filter( - user__username=username).count() + user_forms = XForm.objects.filter(user__username=username).count() # Get the number of submissions - user_sumbissions = Instance.objects.filter( - user__username=username).count() + user_sumbissions = Instance.objects.filter(user__username=username).count() user_response = input( - "User account '{}' has {} projects, " - "{} forms and {} submissions. " + f"User account '{username}' has {user_projects} projects, " + f"{user_forms} forms and {user_sumbissions} submissions. " "Do you wish to continue " - "deleting this account?".format( - username, - user_projects, - user_forms, - user_sumbissions - )) + "deleting this account?" + ) return user_response def inactivate_users(users, user_input): """ - Soft deletes the user termporarily. + Soft deletes the user. """ if users: for user in users: - username, email = user.split(':') - user_response = 'True' - if user_input == 'False': + username, email = user.split(":") + user_response = "True" + if user_input == "False": # If the --user_input flag is not provided. # Get acknowledgement from the user on this user_response = get_user_object_stats(username) - if user_response == 'True': + if user_response == "True": try: user = User.objects.get(username=username, email=email) # set inactive status on user account user.is_active = False # append a timestamped suffix to the username # to make the initial username available - deletion_suffix = timezone.now().strftime('-deleted-at-%s') + deletion_suffix = timezone.now().strftime("-deleted-at-%s") user.username += deletion_suffix user.email += deletion_suffix user.save() - sys.stdout.write( - 'User {} deleted successfully.'.format(username)) + sys.stdout.write(f"User {username} deleted successfully.") # confirm too that no user exists with provided email - if len(User.objects.filter( - email=email, is_active=True)) > 1: + if len(User.objects.filter(email=email, is_active=True)) > 1: other_accounts = [ - user.username for user in User.objects.filter( - email=email)] + user.username for user in User.objects.filter(email=email) + ] sys.stdout.write( - 'User accounts {} have the same ' - 'email address with this User'.format( - other_accounts)) + f"User accounts {other_accounts} have the same " + "email address with this User" + ) - except User.DoesNotExist: - raise CommandError( - 'User {} does not exist.'.format(username)) + except User.DoesNotExist as exc: + raise CommandError(f"User {username} does not exist.") from exc else: - sys.stdout.write( - 'No actions taken') + sys.stdout.write("No actions taken") else: - raise CommandError('No User Account provided!') + raise CommandError("No User Account provided!") class Command(BaseCommand): @@ -101,18 +92,17 @@ class Command(BaseCommand): To change this, pass this in with the value True i.e --user_input True """ - help = 'Delete users' + + help = "Delete users" def add_arguments(self, parser): - parser.add_argument('--user_details', nargs='*') + parser.add_argument("--user_details", nargs="*") parser.add_argument( - '--user_input', - help='Confirm deletion of user account', - default='False' + "--user_input", help="Confirm deletion of user account", default="False" ) def handle(self, *args, **kwargs): - users = kwargs.get('user_details') - user_input = kwargs.get('user_input') + users = kwargs.get("user_details") + user_input = kwargs.get("user_input") inactivate_users(users, user_input) diff --git a/onadata/apps/api/management/commands/fix_readonly_role_perms.py b/onadata/apps/api/management/commands/fix_readonly_role_perms.py index 4e2eab79c4..c0e8692c35 100644 --- a/onadata/apps/api/management/commands/fix_readonly_role_perms.py +++ b/onadata/apps/api/management/commands/fix_readonly_role_perms.py @@ -2,29 +2,27 @@ """ fix_readonly_role_perms - Reassign permission to the model when permissions are changed """ -from guardian.shortcuts import get_perms - -from django.core.management.base import BaseCommand, CommandError +from django.conf import settings from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand, CommandError from django.utils.translation import gettext as _ -from django.conf import settings -from onadata.apps.api.models import Team +from guardian.shortcuts import get_perms +from onadata.apps.api.models import Team from onadata.libs.permissions import ( - ReadOnlyRole, + DataEntryMinorRole, + DataEntryOnlyRole, DataEntryRole, + EditorMinorRole, EditorRole, ManagerRole, OwnerRole, + ReadOnlyRole, ReadOnlyRoleNoDownload, - DataEntryOnlyRole, - DataEntryMinorRole, - EditorMinorRole, ) from onadata.libs.utils.model_tools import queryset_iterator - # pylint: disable=invalid-name User = get_user_model() @@ -83,7 +81,7 @@ def reassign_perms(user, model, new_perm): for perm_obj in objects: obj = perm_obj.content_object - ROLES = [ + roles = [ ReadOnlyRoleNoDownload, ReadOnlyRole, DataEntryOnlyRole, @@ -96,7 +94,7 @@ def reassign_perms(user, model, new_perm): ] # For each role reassign the perms - for role_class in reversed(ROLES): + for role_class in reversed(roles): not_readonly = role_class.user_has_role(user, obj) or role_class not in [ ReadOnlyRoleNoDownload, ReadOnlyRole, diff --git a/onadata/apps/api/management/commands/migrate_group_permissions.py b/onadata/apps/api/management/commands/migrate_group_permissions.py index 5bdc24d8ab..90f1478e8a 100644 --- a/onadata/apps/api/management/commands/migrate_group_permissions.py +++ b/onadata/apps/api/management/commands/migrate_group_permissions.py @@ -1,9 +1,16 @@ +# -*- coding: utf-8 -*- +""" +Migrate group permissions +""" +import sys + +from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand from django.db.models import Count -from django.db.models.loading import get_model from django.db.utils import IntegrityError from django.utils.translation import gettext as _ + from guardian.models import GroupObjectPermissionBase from onadata.apps.api.models import Team @@ -11,70 +18,83 @@ class Command(BaseCommand): - help = _(u"Migrate group permissions") + """Migrate group permissions""" + + help = _("Migrate group permissions") def add_arguments(self, parser): parser.add_argument( - '--model', - '-m', - action='store_true', - dest='app_model', + "--model", + "-m", + action="store_true", + dest="app_model", default=False, - help='The model the permission belong too.' - ' (app.model format)') + help="The model the permission belong too." " (app.model format)", + ) parser.add_argument( - '--perm-table', - '-p', - action='store_true', - dest='perms_tbl', + "--perm-table", + "-p", + action="store_true", + dest="perms_tbl", default=False, - help='The new model permission are stored in' - ' (app.model format)') + help="The new model permission are stored in" " (app.model format)", + ) def handle(self, *args, **options): - self.stdout.write("Migrate group permissions started", ending='\n') + self.stdout.write("Migrate group permissions started", ending="\n") if len(args) < 2: - self.stdout.write("This command takes two argument -m and -p " - "Example: " - "-m logger.Team " - "-p logger.TeamUserObjectPermission") - exit() + self.stdout.write( + "This command takes two argument -m and -p " + "Example: " + "-m logger.Team " + "-p logger.TeamUserObjectPermission" + ) + sys.exit() - if options['app_model']: + if options["app_model"]: app_model = args[0] else: self.stdout.write("-m , should be set as the first argument") - exit() + sys.exit() - if options['perms_tbl']: + if options["perms_tbl"]: perms_tbl = args[1] else: self.stdout.write("-p , should be set as the second argument") - exit() + sys.exit() - model = get_model(app_model) - perms_model = get_model(perms_tbl) + model = apps.get_model(app_model) + perms_model = apps.get_model(perms_tbl) if not issubclass(perms_model, GroupObjectPermissionBase): - self.stdout.write("-p , should be a model of a class that is " - "a subclass of GroupObjectPermissionBase") - exit() + self.stdout.write( + "-p , should be a model of a class that is " + "a subclass of GroupObjectPermissionBase" + ) + sys.exit() - ct = ContentType.objects.get( - model=model.__name__.lower(), app_label=model._meta.app_label) - teams = Team.objects.filter().annotate( - c=Count('groupobjectpermission')).filter(c__gt=0) + content_type = ContentType.objects.get( + model=model.__name__.lower(), app_label=model._meta.app_label + ) + teams = ( + Team.objects.filter() + .annotate(c=Count("groupobjectpermission")) + .filter(c__gt=0) + ) for team in queryset_iterator(teams): - self.stdout.write("Processing: {} - {}".format(team.pk, team.name)) - for gop in team.groupobjectpermission_set.filter(content_type=ct)\ - .select_related('permission', 'content_type')\ - .prefetch_related('permission', 'content_type'): + self.stdout.write(f"Processing: {team.pk} - {team.name}") + for gop in ( + team.groupobjectpermission_set.filter(content_type=content_type) + .select_related("permission", "content_type") + .prefetch_related("permission", "content_type") + ): try: perms_model( content_object=gop.content_object, group=team, - permission=gop.permission).save() + permission=gop.permission, + ).save() except IntegrityError: continue except ValueError: diff --git a/onadata/apps/api/management/commands/migrate_permissions.py b/onadata/apps/api/management/commands/migrate_permissions.py index e9e7993456..5cdfcd08ce 100644 --- a/onadata/apps/api/management/commands/migrate_permissions.py +++ b/onadata/apps/api/management/commands/migrate_permissions.py @@ -1,85 +1,103 @@ +# -*- coding: utf-8 -*- +""" +Migrate permissions +""" +import sys + +from django.apps import apps from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand -from django.db.models.loading import get_model from django.db.utils import IntegrityError from django.utils.translation import gettext as _ + from guardian.models import UserObjectPermissionBase from onadata.libs.utils.model_tools import queryset_iterator +User = get_user_model() + class Command(BaseCommand): - help = _(u"Migrate permissions") + """Migrate permissions""" + + help = _("Migrate permissions") def add_arguments(self, parser): parser.add_argument( - '--model', - '-m', - action='store_true', - dest='app_model', + "--model", + "-m", + action="store_true", + dest="app_model", default=False, - help='The model the permission belong too.' - ' (app.model format)') + help="The model the permission belong too." " (app.model format)", + ) parser.add_argument( - '--perm-table', - '-p', - action='store_true', - dest='perms_tbl', + "--perm-table", + "-p", + action="store_true", + dest="perms_tbl", default=False, - help='The new model permission are stored in' - ' (app.model format)') + help="The new model permission are stored in" " (app.model format)", + ) def handle(self, *args, **options): - self.stdout.write("Migrate permissions started", ending='\n') + self.stdout.write("Migrate permissions started", ending="\n") if len(args) < 2: - self.stdout.write("This command takes two argument -m and -p " - "Example: " - "-m logger.Team " - "-p logger.TeamUserObjectPermission") - exit() + self.stdout.write( + "This command takes two argument -m and -p " + "Example: " + "-m logger.Team " + "-p logger.TeamUserObjectPermission" + ) + sys.exit() - if options['app_model']: + if options["app_model"]: app_model = args[0] else: self.stdout.write("-m , should be set as the first argument") - exit() + sys.exit() - if options['perms_tbl']: + if options["perms_tbl"]: perms_tbl = args[1] else: self.stdout.write("-p , should be set as the second argument") - exit() + sys.exit() - model = get_model(app_model) - perms_model = get_model(perms_tbl) + model = apps.get_model(app_model) + perms_model = apps.get_model(perms_tbl) if not issubclass(perms_model, UserObjectPermissionBase): - self.stdout.write("-p , should be a model of a class that is " - "a subclass of UserObjectPermissionBase") - exit() + self.stdout.write( + "-p , should be a model of a class that is " + "a subclass of UserObjectPermissionBase" + ) + sys.exit() - ct = ContentType.objects.get( - model=model.__name__.lower(), app_label=model._meta.app_label) + content_type = ContentType.objects.get( + model=model.__name__.lower(), app_label=model._meta.app_label + ) # Get all the users users = User.objects.exclude( - username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME).order_by( - 'username') + username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME + ).order_by("username") for user in queryset_iterator(users): - self.stdout.write( - "Processing: {} - {}".format(user.pk, user.username)) - for uop in user.userobjectpermission_set.filter(content_type=ct)\ - .select_related('permission', 'content_type')\ - .prefetch_related('permission', 'content_type'): + self.stdout.write(f"Processing: {user.pk} - {user.username}") + for uop in ( + user.userobjectpermission_set.filter(content_type=content_type) + .select_related("permission", "content_type") + .prefetch_related("permission", "content_type") + ): try: perms_model( content_object=uop.content_object, user=user, - permission=uop.permission).save() + permission=uop.permission, + ).save() except IntegrityError: continue except ValueError: diff --git a/onadata/apps/api/management/commands/print_profiler_results.py b/onadata/apps/api/management/commands/print_profiler_results.py deleted file mode 100644 index 09c4fc725c..0000000000 --- a/onadata/apps/api/management/commands/print_profiler_results.py +++ /dev/null @@ -1,13 +0,0 @@ -from hotshot import stats -from django.core.management.base import BaseCommand - - -class Command(BaseCommand): - args = '' - - def handle(self, *args, **options): - self.stdout.write("Show profiler log file output..", ending='\n') - - _stats = stats.load(args[0]) - _stats.sort_stats('time', 'calls') - _stats.print_stats(20) diff --git a/onadata/apps/api/management/commands/reassign_permission.py b/onadata/apps/api/management/commands/reassign_permission.py index 38ee347f10..065361e841 100644 --- a/onadata/apps/api/management/commands/reassign_permission.py +++ b/onadata/apps/api/management/commands/reassign_permission.py @@ -119,7 +119,6 @@ def reassign_perms(self, user, app, model, new_perm): role_class.add(user, obj) break - # pylint: disable=no-self-use def check_role(self, role_class, user, obj, new_perm=None): """ Test if the user has the role for the object provided diff --git a/onadata/apps/api/management/commands/regenerate_auth_tokens.py b/onadata/apps/api/management/commands/regenerate_auth_tokens.py index 16b7f992c6..38ebaf29ed 100644 --- a/onadata/apps/api/management/commands/regenerate_auth_tokens.py +++ b/onadata/apps/api/management/commands/regenerate_auth_tokens.py @@ -1,58 +1,73 @@ -from django.contrib.auth.models import User +# -*- coding: utf-8 -*- +""" +Regenerate Authentication Tokens +""" +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand from django.utils.translation import gettext as _ from rest_framework.authtoken.models import Token +ALL = "all" + +User = get_user_model() + class Command(BaseCommand): - help = _(u"Regenerate Authentication Tokens") + """Regenerate Authentication Tokens""" + + help = _("Regenerate Authentication Tokens") def add_arguments(self, parser): parser.add_argument( - '--users', - '-u', - action='store_true', - dest='users', + "--users", + "-u", + action="store_true", + dest="users", default=False, - help='Users to Regenerate Tokens') + help="Users to Regenerate Tokens", + ) def handle(self, *args, **options): - ALL = "all" # check if the users option has been used and # at least one argument passed - if options['users'] and len(args) > 0: + if options["users"] and len(args) > 0: # check if keyword 'all' has been included in the list of arguments if len(args) > 1 and ALL in args: - self.stdout.write("Keyword 'all' should be passed as single " - "argument and not be part of a list") + self.stdout.write( + "Keyword 'all' should be passed as single " + "argument and not be part of a list" + ) else: # check if the single argument passed in 'all' if len(args) == 1 and args[0] == ALL: Token.objects.all().delete() for user in User.objects.all(): Token.objects.create(user=user) - self.stdout.write("All users' api tokens have " - "been updated") + self.stdout.write("All users' api tokens have been updated") else: users = User.objects.filter(username__in=args) usernames = [a.username for a in users] # check if ALL usernames provided were valid if len(users) == 0: - self.stdout.write("The usernames provided were " - "invalid") + self.stdout.write("The usernames provided were invalid") else: # check some of the usernames passed were invalid if len(users) != len(args): users_not_found = list(set(args) - set(usernames)) - self.stdout.write("The following usernames don't " - "exist: %s" % users_not_found) + self.stdout.write( + "The following usernames don't exist: " + f"{users_not_found}" + ) for user in users: Token.objects.get(user=user).delete() Token.objects.create(user=user) - self.stdout.write("The API tokens for the users " - "provided have been updated") + self.stdout.write( + "The API tokens for the users provided have been updated" + ) else: - self.stdout.write("This command takes at least one argument with " - "the '--users' or '-u' option e.g -u ") + self.stdout.write( + "This command takes at least one argument with " + "the '--users' or '-u' option e.g -u " + ) diff --git a/onadata/apps/api/management/commands/retrieve_org_or_project_list.py b/onadata/apps/api/management/commands/retrieve_org_or_project_list.py index 5d992d2c93..cdd343d1a0 100644 --- a/onadata/apps/api/management/commands/retrieve_org_or_project_list.py +++ b/onadata/apps/api/management/commands/retrieve_org_or_project_list.py @@ -1,79 +1,92 @@ +# -*- coding: utf-8 -*- +""" +Retrieve collaborators list from all/a specific project(s) or organization(s) +""" import json -from django.core.management.base import ( - BaseCommand, CommandError, CommandParser) + +from django.core.management.base import BaseCommand, CommandError, CommandParser from django.utils.translation import gettext as _ -from onadata.apps.logger.models import Project from onadata.apps.api.models import OrganizationProfile -from onadata.libs.utils.project_utils import get_project_users +from onadata.apps.logger.models import Project from onadata.libs.utils.organization_utils import get_organization_members +from onadata.libs.utils.project_utils import get_project_users class Command(BaseCommand): + """ + Retrieve collaborators list from all/a specific project(s) or organization(s) + """ + help = _( "Retrieve collaborators list from all/a specific" - " project(s) or organization(s)") + " project(s) or organization(s)" + ) def add_arguments(self, parser: CommandParser): parser.add_argument( - '--project-ids', - '-p', + "--project-ids", + "-p", default=None, - dest='project_ids', - help='Comma separated list of project ID(s) to' - ' retrieve collaborators/members from.' + dest="project_ids", + help="Comma separated list of project ID(s) to" + " retrieve collaborators/members from.", ) parser.add_argument( - '--organization-ids', - '-oid', + "--organization-ids", + "-oid", default=None, - dest='organization_ids', - help='Comma separated list of organization ID(s) to retrieve' - ' collaborators/members from.' + dest="organization_ids", + help="Comma separated list of organization ID(s) to retrieve" + " collaborators/members from.", ) parser.add_argument( - '--output-file', - '-o', - dest='output_file', + "--output-file", + "-o", + dest="output_file", default=None, - help='JSON file to output the collaborators/members list too' + help="JSON file to output the collaborators/members list too", ) + # pylint: disable=too-many-branches def handle(self, *args, **options): result = {} - project_ids = options.get('project_ids') - organization_ids = options.get('organization_ids') - output_file = options.get('output_file') + project_ids = options.get("project_ids") + organization_ids = options.get("organization_ids") + output_file = options.get("output_file") if project_ids or organization_ids: if project_ids: - project_ids = project_ids.split(',') + project_ids = project_ids.split(",") for project_id in project_ids: try: project = Project.objects.get(id=int(project_id)) - except Project.DoesNotExist: + except Project.DoesNotExist as exc: raise CommandError( - f'Project with ID {project_id} does not exist.') - except ValueError: + f"Project with ID {project_id} does not exist." + ) from exc + except ValueError as exc: raise CommandError( - f'Invalid project ID input "{project_id}"') + f'Invalid project ID input "{project_id}"' + ) from exc else: result[project.name] = get_project_users(project) if organization_ids: - organization_ids = organization_ids.split(',') + organization_ids = organization_ids.split(",") for org_id in organization_ids: try: - org = OrganizationProfile.objects.get( - id=int(org_id)) - except OrganizationProfile.DoesNotExist: + org = OrganizationProfile.objects.get(id=int(org_id)) + except OrganizationProfile.DoesNotExist as exc: raise CommandError( - f'Organization with ID {org_id} does not exist.') - except ValueError: + f"Organization with ID {org_id} does not exist." + ) from exc + except ValueError as exc: raise CommandError( - f'Invalid organization ID input "{org_id}"') + f'Invalid organization ID input "{org_id}"' + ) from exc else: result[org.name] = get_organization_members(org) else: @@ -81,12 +94,11 @@ def handle(self, *args, **options): for project in Project.objects.filter(deleted_at__isnull=True): result[project.name] = get_project_users(project) - for org in OrganizationProfile.objects.filter( - user__is_active=True): + for org in OrganizationProfile.objects.filter(user__is_active=True): result[org.name] = get_organization_members(org) if output_file: - with open(output_file, 'w+') as outfile: + with open(output_file, "w+", encoding="utf-8") as outfile: json.dump(result, outfile) self.stdout.write( f'Outputted members/collaborators list to "{output_file}"' diff --git a/onadata/apps/api/management/commands/set_api_permissions.py b/onadata/apps/api/management/commands/set_api_permissions.py index a8d6b7bf2a..dae524217f 100644 --- a/onadata/apps/api/management/commands/set_api_permissions.py +++ b/onadata/apps/api/management/commands/set_api_permissions.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Set object permissions for all objects. +""" from django.core.management.base import BaseCommand from django.utils.translation import gettext as _ @@ -11,7 +15,9 @@ class Command(BaseCommand): - help = _(u"Set object permissions for all objects.") + """Set object permissions for all objects.""" + + help = _("Set object permissions for all objects.") def handle(self, *args, **options): # XForms @@ -29,18 +35,15 @@ def handle(self, *args, **options): # OrganizationProfile for profile in queryset_iterator(OrganizationProfile.objects.all()): OwnerRole.add(profile.user, profile) - OwnerRole.add( - profile.user, profile.userprofile_ptr) + OwnerRole.add(profile.user, profile.userprofile_ptr) if profile.created_by is not None: OwnerRole.add(profile.created_by, profile) - OwnerRole.add( - profile.created_by, profile.userprofile_ptr) + OwnerRole.add(profile.created_by, profile.userprofile_ptr) if profile.creator is not None: OwnerRole.add(profile.creator, profile) - OwnerRole.add( - profile.creator, profile.userprofile_ptr) + OwnerRole.add(profile.creator, profile.userprofile_ptr) # Project for project in queryset_iterator(Project.objects.all()): diff --git a/onadata/apps/api/models/odk_token.py b/onadata/apps/api/models/odk_token.py index 2d86886f7c..261daea331 100644 --- a/onadata/apps/api/models/odk_token.py +++ b/onadata/apps/api/models/odk_token.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ ODK token model module """ @@ -6,17 +7,17 @@ from datetime import timedelta from django.conf import settings +from django.contrib.auth import get_user_model from django.db import models from django.db.models.signals import post_save from django.utils.translation import gettext_lazy as _ from cryptography.fernet import Fernet -from django_digest.models import (_persist_partial_digests, - _prepare_partial_digests) +from django_digest.models import _persist_partial_digests, _prepare_partial_digests -AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') -ODK_TOKEN_LENGTH = getattr(settings, 'ODK_TOKEN_LENGTH', 7) -ODK_TOKEN_FERNET_KEY = getattr(settings, 'ODK_TOKEN_FERNET_KEY', '') +AUTH_USER_MODEL = get_user_model() +ODK_TOKEN_LENGTH = getattr(settings, "ODK_TOKEN_LENGTH", 7) +ODK_TOKEN_FERNET_KEY = getattr(settings, "ODK_TOKEN_FERNET_KEY", "") ODK_TOKEN_LIFETIME = getattr(settings, "ODK_KEY_LIFETIME", 7) @@ -24,26 +25,21 @@ class ODKToken(models.Model): """ ODK Token class """ - ACTIVE = '1' - INACTIVE = '2' - STATUS_CHOICES = ( - (ACTIVE, _('Active')), - (INACTIVE, _('Inactive')) - ) + + ACTIVE = "1" + INACTIVE = "2" + STATUS_CHOICES = ((ACTIVE, _("Active")), (INACTIVE, _("Inactive"))) key = models.CharField(max_length=150, primary_key=True) - user = models.ForeignKey( - AUTH_USER_MODEL, on_delete=models.CASCADE) + user = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.CASCADE) status = models.CharField( - 'Status', - choices=STATUS_CHOICES, - default=ACTIVE, - max_length=1) + "Status", choices=STATUS_CHOICES, default=ACTIVE, max_length=1 + ) created = models.DateTimeField(auto_now_add=True) expires = models.DateTimeField(blank=True, null=True) class Meta: - app_label = 'api' + app_label = "api" def _generate_partial_digest(self, raw_key): """ @@ -61,10 +57,14 @@ def save(self, *args, **kwargs): # pylint: disable=arguments-differ if not self.key: self.key = self.generate_key() - return super(ODKToken, self).save(*args, **kwargs) + return super().save(*args, **kwargs) def generate_key(self): - key = binascii.hexlify(os.urandom(ODK_TOKEN_LENGTH)).decode('utf-8') + """ + Generates and returns ODK Token key encrypted with the Fernet cryptography + scheme. + """ + key = binascii.hexlify(os.urandom(ODK_TOKEN_LENGTH)).decode("utf-8") self._generate_partial_digest(key) return _encrypt_key(key) @@ -77,7 +77,7 @@ def raw_key(self): Decrypts the key and returns it in its Raw Form """ fernet = Fernet(ODK_TOKEN_FERNET_KEY) - return fernet.decrypt(self.key.encode('utf-8')) + return fernet.decrypt(self.key.encode("utf-8")) def _encrypt_key(raw_key): @@ -86,23 +86,23 @@ def _encrypt_key(raw_key): the fernet cryptography scheme """ fernet = Fernet(ODK_TOKEN_FERNET_KEY) - return fernet.encrypt(raw_key.encode('utf-8')).decode('utf-8') + return fernet.encrypt(raw_key.encode("utf-8")).decode("utf-8") +# pylint: disable=unused-argument def _post_save_persist_partial_digests(sender, instance=None, **kwargs): if instance: _persist_partial_digests(instance.user) +# pylint: disable=unused-argument def _post_save_set_expiry_date(sender, instance=None, **kwargs): if instance and not instance.expires: expiry_date = instance.created + timedelta(days=ODK_TOKEN_LIFETIME) - instance.expires = expiry_date.astimezone( - instance.created.tzinfo) + instance.expires = expiry_date.astimezone(instance.created.tzinfo) instance.save() -post_save.connect( - _post_save_persist_partial_digests, sender=ODKToken) +post_save.connect(_post_save_persist_partial_digests, sender=ODKToken) post_save.connect(_post_save_set_expiry_date, sender=ODKToken) diff --git a/onadata/apps/api/models/organization_profile.py b/onadata/apps/api/models/organization_profile.py index d0dd1a3dad..cd6625bebf 100644 --- a/onadata/apps/api/models/organization_profile.py +++ b/onadata/apps/api/models/organization_profile.py @@ -2,17 +2,22 @@ """ OrganizationProfile module. """ -from django.contrib.auth.models import Permission, User +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models.signals import post_delete, post_save from guardian.models import GroupObjectPermissionBase, UserObjectPermissionBase from guardian.shortcuts import assign_perm, get_perms_for_model +from multidb.pinning import use_master from onadata.apps.api.models.team import Team -from onadata.apps.main.models import UserProfile +from onadata.apps.main.models.user_profile import UserProfile from onadata.libs.utils.cache_tools import IS_ORG, safe_delete +from onadata.libs.utils.common_tags import MEMBERS + +User = get_user_model() # pylint: disable=invalid-name,unused-argument @@ -22,7 +27,7 @@ def org_profile_post_delete_callback(sender, instance, **kwargs): """ # delete the org_user too instance.user.delete() - safe_delete("{}{}".format(IS_ORG, instance.pk)) + safe_delete(f"{IS_ORG}{instance.pk}") def create_owner_team_and_assign_permissions(org): @@ -37,7 +42,7 @@ def create_owner_team_and_assign_permissions(org): # pylint: disable=unpacking-non-sequence permission, _ = Permission.objects.get_or_create( codename="is_org_owner", name="Organization Owner", content_type=content_type - ) # pylint: disable= + ) team.permissions.add(permission) org.creator.groups.add(team) @@ -63,6 +68,82 @@ def create_owner_team_and_assign_permissions(org): return team +# pylint: disable=invalid-name +def get_or_create_organization_owners_team(org): + """ + Get the owners team of an organization + :param org: organization + :return: Owners team of the organization + """ + team_name = f"{org.user.username}#{Team.OWNER_TEAM_NAME}" + try: + team = Team.objects.get(name=team_name, organization=org.user) + except Team.DoesNotExist: + with use_master: + queryset = Team.objects.filter(name=team_name, organization=org.user) + if queryset.count() > 0: + return queryset.first() # pylint: disable=no-member + return create_owner_team_and_assign_permissions(org) + return team + + +def add_user_to_team(team, user): + """ + Adds a user to a team and assigns them team permissions. + """ + user.groups.add(team) + + # give the user perms to view the team + assign_perm("view_team", user, team) + + # if team is owners team assign more perms + if team.name.find(Team.OWNER_TEAM_NAME) > 0: + _assign_organization_team_perms(team.organization, user) + + +def _assign_organization_team_perms(organization, user): + owners_team = get_or_create_organization_owners_team(organization.profile) + members_team = get_organization_members_team(organization.profile) + for perm in get_perms_for_model(Team): + assign_perm(perm.codename, user, owners_team) + assign_perm(perm.codename, user, members_team) + + +def create_organization_team(organization, name, permission_names=None): + """ + Creates an organization team with the given permissions as defined in + permission_names. + """ + organization = ( + organization.user + if isinstance(organization, OrganizationProfile) + else organization + ) + team = Team.objects.create(organization=organization, name=name) + content_type = ContentType.objects.get(app_label="api", model="organizationprofile") + if permission_names: + # get permission objects + perms = Permission.objects.filter( + codename__in=permission_names, content_type=content_type + ) + if perms: + team.permissions.add(*tuple(perms)) + return team + + +def get_organization_members_team(organization): + """Get organization members team + create members team if it does not exist and add organization owner + to the members team""" + try: + team = Team.objects.get(name=f"{organization.user.username}#{MEMBERS}") + except Team.DoesNotExist: + team = create_organization_team(organization, MEMBERS) + add_user_to_team(team, organization.user) + + return team + + def _post_save_create_owner_team(sender, instance, created, **kwargs): """ Signal handler that creates the Owner team and assigns group and user @@ -97,17 +178,17 @@ class Meta: creator = models.ForeignKey(User, on_delete=models.CASCADE) def __str__(self): - return "%s[%s]" % (self.name, self.user.username) + return f"{self.name}[{self.user.username}]" def save(self, *args, **kwargs): # pylint: disable=arguments-differ - super(OrganizationProfile, self).save(*args, **kwargs) + super().save(*args, **kwargs) def remove_user_from_organization(self, user): """Removes a user from all teams/groups in the organization. :param user: The user to remove from this organization. """ - for group in user.groups.filter("%s#" % self.user.username): + for group in user.groups.filter(name=f"{self.user.username}#"): user.groups.remove(group) def is_organization_owner(self, user): @@ -118,9 +199,9 @@ def is_organization_owner(self, user): :returns: Boolean whether user has organization level permissions. """ has_owner_group = user.groups.filter( - name="%s#%s" % (self.user.username, Team.OWNER_TEAM_NAME) + name=f"{self.user.username}#{Team.OWNER_TEAM_NAME}" ) - return True if has_owner_group else False + return has_owner_group.count() > 0 post_save.connect( diff --git a/onadata/apps/api/models/team.py b/onadata/apps/api/models/team.py index ad4a8e21ce..0d80c728d6 100644 --- a/onadata/apps/api/models/team.py +++ b/onadata/apps/api/models/team.py @@ -45,7 +45,7 @@ def __str__(self): @property def team_name(self): """Return the team name.""" - return self.__str__() + return str(self) def save(self, *args, **kwargs): # allow use of same name in different organizations/users diff --git a/onadata/apps/api/models/temp_token.py b/onadata/apps/api/models/temp_token.py index c9ded3c28e..231ce75317 100644 --- a/onadata/apps/api/models/temp_token.py +++ b/onadata/apps/api/models/temp_token.py @@ -29,7 +29,7 @@ def save(self, *args, **kwargs): self.key = self.generate_key() return super().save(*args, **kwargs) - def generate_key(self): # pylint: disable=no-self-use + def generate_key(self): """Generates a token key.""" return binascii.hexlify(os.urandom(20)).decode() diff --git a/onadata/apps/api/permissions.py b/onadata/apps/api/permissions.py index 7d359427b9..8f7a8d3672 100644 --- a/onadata/apps/api/permissions.py +++ b/onadata/apps/api/permissions.py @@ -1,31 +1,42 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ API permissions module. """ -from django.contrib.auth.models import User +from django.conf import settings +from django.contrib.auth import get_user_model from django.http import Http404 from django.shortcuts import get_object_or_404 -from django.conf import settings from rest_framework import exceptions from rest_framework.permissions import ( - BasePermission, DjangoModelPermissionsOrAnonReadOnly, - DjangoObjectPermissions, IsAuthenticated) - -from onadata.apps.api.tools import (check_inherit_permission_from_project, - get_instance_xform_or_none, - get_user_profile_or_none) + BasePermission, + DjangoModelPermissionsOrAnonReadOnly, + DjangoObjectPermissions, + IsAuthenticated, +) + +from onadata.apps.api.tools import ( + check_inherit_permission_from_project, + get_instance_xform_or_none, + get_user_profile_or_none, +) from onadata.apps.logger.models import DataView, Instance, Project, XForm from onadata.apps.main.models.user_profile import UserProfile -from onadata.libs.permissions import (CAN_ADD_XFORM_TO_PROFILE, - CAN_CHANGE_XFORM, CAN_DELETE_SUBMISSION, - ReadOnlyRoleNoDownload, - OwnerRole, ManagerRole) +from onadata.libs.permissions import ( + CAN_ADD_XFORM_TO_PROFILE, + CAN_CHANGE_XFORM, + CAN_DELETE_SUBMISSION, + ManagerRole, + OwnerRole, + ReadOnlyRoleNoDownload, +) -SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') +SAFE_METHODS = ("GET", "HEAD", "OPTIONS") +User = get_user_model() -class AlternateHasObjectPermissionMixin(object): # pylint: disable=R0903 + +class AlternateHasObjectPermissionMixin: # pylint: disable=too-few-public-methods """ AlternateHasObjectPermissionMixin - checks if user has read permissions. """ @@ -43,7 +54,7 @@ def _has_object_permission(self, request, model_cls, user, obj): # to make another lookup. raise Http404 - read_perms = self.get_required_object_permissions('GET', model_cls) + read_perms = self.get_required_object_permissions("GET", model_cls) if not user.has_perms(read_perms, obj): raise Http404 @@ -58,58 +69,61 @@ class ViewDjangoObjectPermissions(DjangoObjectPermissions): View DjangoObjectPermissions - applies view_ permissions for GET requests. """ + perms_map = { - 'GET': ['%(app_label)s.view_%(model_name)s'], - 'OPTIONS': [], - 'HEAD': [], - 'POST': ['%(app_label)s.add_%(model_name)s'], - 'PUT': ['%(app_label)s.change_%(model_name)s'], - 'PATCH': ['%(app_label)s.change_%(model_name)s'], - 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + "GET": ["%(app_label)s.view_%(model_name)s"], + "OPTIONS": [], + "HEAD": [], + "POST": ["%(app_label)s.add_%(model_name)s"], + "PUT": ["%(app_label)s.change_%(model_name)s"], + "PATCH": ["%(app_label)s.change_%(model_name)s"], + "DELETE": ["%(app_label)s.delete_%(model_name)s"], } -class ExportDjangoObjectPermission(AlternateHasObjectPermissionMixin, - ViewDjangoObjectPermissions): +class ExportDjangoObjectPermission( + AlternateHasObjectPermissionMixin, ViewDjangoObjectPermissions +): """ Export DjangoObjectPermission - checks XForm permissions for export permissions. """ + authenticated_users_only = False perms_map = { - 'GET': ['logger.view_xform'], - 'OPTIONS': [], - 'HEAD': [], - 'POST': ['logger.add_xform'], - 'PUT': ['logger.change_xform'], - 'PATCH': ['logger.change_xform'], - 'DELETE': ['logger.delete_xform'], + "GET": ["logger.view_xform"], + "OPTIONS": [], + "HEAD": [], + "POST": ["logger.add_xform"], + "PUT": ["logger.change_xform"], + "PATCH": ["logger.change_xform"], + "DELETE": ["logger.delete_xform"], } def has_permission(self, request, view): - is_authenticated = (request and request.user and - request.user.is_authenticated) + is_authenticated = request and request.user and request.user.is_authenticated if not is_authenticated: - view._ignore_model_permissions = True # pylint: disable=W0212 + view._ignore_model_permissions = True # pylint: disable=protected-access - if view.action == 'destroy' and is_authenticated: - return request.user.has_perms(['logger.delete_xform']) + if view.action == "destroy" and is_authenticated: + return request.user.has_perms(["logger.delete_xform"]) - return super(ExportDjangoObjectPermission, self).has_permission( - request, view) + return super().has_permission(request, view) def has_object_permission(self, request, view, obj): model_cls = XForm user = request.user - return (obj.xform.shared_data or obj.xform.project.shared) or\ - self._has_object_permission(request, model_cls, user, obj.xform) + return ( + obj.xform.shared_data or obj.xform.project.shared + ) or self._has_object_permission(request, model_cls, user, obj.xform) class DjangoObjectPermissionsAllowAnon(DjangoObjectPermissions): """ DjangoObjectPermissionsAllowAnon - allow anonymous access permission. """ + authenticated_users_only = False @@ -117,57 +131,58 @@ class XFormPermissions(DjangoObjectPermissions): """ XFormPermissions - custom permissions check on XForm viewset. """ + authenticated_users_only = False def has_permission(self, request, view): - owner = view.kwargs.get('owner') + owner = view.kwargs.get("owner") is_authenticated = request and request.user.is_authenticated - if 'pk' in view.kwargs: - check_inherit_permission_from_project(view.kwargs['pk'], - request.user) + if "pk" in view.kwargs: + check_inherit_permission_from_project(view.kwargs["pk"], request.user) - if is_authenticated and view.action == 'create': + if is_authenticated and view.action == "create": owner = owner or request.user.username - return request.user.has_perm(CAN_ADD_XFORM_TO_PROFILE, - get_user_profile_or_none(owner)) + return request.user.has_perm( + CAN_ADD_XFORM_TO_PROFILE, get_user_profile_or_none(owner) + ) - return super(XFormPermissions, self).has_permission(request, view) + return super().has_permission(request, view) def has_object_permission(self, request, view, obj): - if hasattr(obj, 'shared') and obj.shared and view.action == 'clone': + if hasattr(obj, "shared") and obj.shared and view.action == "clone": return obj - if request.method == 'DELETE' and view.action == 'labels': + if request.method == "DELETE" and view.action == "labels": user = request.user return user.has_perm(CAN_CHANGE_XFORM, obj) - if request.method == 'DELETE' and view.action == 'destroy': + if request.method == "DELETE" and view.action == "destroy": return request.user.has_perm(CAN_DELETE_SUBMISSION, obj) - return super(XFormPermissions, self).has_object_permission( - request, view, obj) + return super().has_object_permission(request, view, obj) class SubmissionReviewPermissions(XFormPermissions): """ Custom Permission Checks for SubmissionReviews """ + perms_map = { - 'GET': [], - 'OPTIONS': [], - 'HEAD': [], - 'POST': ['logger.add_xform'], - 'PUT': ['logger.change_xform'], - 'PATCH': ['logger.change_xform'], - 'DELETE': ['logger.delete_xform'], + "GET": [], + "OPTIONS": [], + "HEAD": [], + "POST": ["logger.add_xform"], + "PUT": ["logger.change_xform"], + "PATCH": ["logger.change_xform"], + "DELETE": ["logger.delete_xform"], } - def _check_is_admin_or_manager( # pylint: disable=no-self-use - self, user: User, xform: XForm) -> bool: - return OwnerRole.user_has_role( - user, xform) or ManagerRole.user_has_role(user, xform) + def _check_is_admin_or_manager(self, user: User, xform: XForm) -> bool: + return OwnerRole.user_has_role(user, xform) or ManagerRole.user_has_role( + user, xform + ) def has_permission(self, request, view): """ @@ -175,40 +190,43 @@ def has_permission(self, request, view): """ is_authenticated = request and request.user.is_authenticated - if is_authenticated and view.action == 'create': + if is_authenticated and view.action == "create": # Handle bulk create # if doing a bulk create we will fail the entire process if the # user lacks permissions for even one instance if isinstance(request.data, list): - instance_ids = list(set([_['instance'] for _ in request.data])) - instances = Instance.objects.filter( - id__in=instance_ids).only('xform').order_by().distinct() + instance_ids = list(set(_["instance"] for _ in request.data)) + instances = ( + Instance.objects.filter(id__in=instance_ids) + .only("xform") + .order_by() + .distinct() + ) for instance in instances: if not self._check_is_admin_or_manager( - request.user, instance.xform): + request.user, instance.xform + ): return False return True # everything is okay # Handle single create like normal - instance_id = request.data.get('instance') + instance_id = request.data.get("instance") xform = get_instance_xform_or_none(instance_id) return self._check_is_admin_or_manager(request.user, xform) - return super(SubmissionReviewPermissions, self).has_permission( - request, view) + return super().has_permission(request, view) def has_object_permission(self, request, view, obj): """ Custom has_object_permission method """ - if (request.method == 'DELETE' and view.action == 'destroy') or ( - request.method == 'PATCH' and view.action == 'partial_update'): - return self._check_is_admin_or_manager( - request.user, obj.instance.xform) + if (request.method == "DELETE" and view.action == "destroy") or ( + request.method == "PATCH" and view.action == "partial_update" + ): + return self._check_is_admin_or_manager(request.user, obj.instance.xform) - return super(SubmissionReviewPermissions, self).has_object_permission( - request, view, obj) + return super().has_object_permission(request, view, obj) class UserProfilePermissions(DjangoObjectPermissions): @@ -220,22 +238,20 @@ class UserProfilePermissions(DjangoObjectPermissions): def has_permission(self, request, view): # allow anonymous users to create new profiles - if request.user.is_anonymous and view.action == 'create': + if request.user.is_anonymous and view.action == "create": return True - if view.action in ['send_verification_email', 'verify_email']: + if view.action in ["send_verification_email", "verify_email"]: enable_email_verification = getattr( - settings, 'ENABLE_EMAIL_VERIFICATION', False + settings, "ENABLE_EMAIL_VERIFICATION", False ) - if enable_email_verification is None or\ - not enable_email_verification: + if enable_email_verification is None or not enable_email_verification: return False - if view.action == 'send_verification_email': - return request.user.username == request.data.get('username') + if view.action == "send_verification_email": + return request.user.username == request.data.get("username") - return \ - super(UserProfilePermissions, self).has_permission(request, view) + return super().has_permission(request, view) class ProjectPermissions(DjangoObjectPermissions): @@ -247,27 +263,25 @@ class ProjectPermissions(DjangoObjectPermissions): def has_permission(self, request, view): # allow anonymous users to view public projects - if request.user.is_anonymous and view.action == 'list': + if request.user.is_anonymous and view.action == "list": return True - if not request.user.is_anonymous and view.action == 'star': + if not request.user.is_anonymous and view.action == "star": return True - return \ - super(ProjectPermissions, self).has_permission(request, view) + return super().has_permission(request, view) def has_object_permission(self, request, view, obj): - if view.action == 'share' and request.method == 'PUT': - remove = request.data.get('remove') - username = request.data.get('username', '') + if view.action == "share" and request.method == "PUT": + remove = request.data.get("remove") + username = request.data.get("username", "") if remove and request.user.username.lower() == username.lower(): return True - return super(ProjectPermissions, self).has_object_permission( - request, view, obj) + return super().has_object_permission(request, view, obj) -class AbstractHasPermissionMixin(object): # pylint: disable=R0903 +class AbstractHasPermissionMixin: # pylint: disable=too-few-public-methods """ Checks that the requesting user has permissions to access each of the models in the `model_classes` instance variable. @@ -280,24 +294,25 @@ def has_permission(self, request, view): # Workaround to ensure DjangoModelPermissions are not applied # to the root view when using DefaultRouter. - if getattr(view, '_ignore_model_permissions', False): + if getattr(view, "_ignore_model_permissions", False): return True perms = [] for model_class in self.model_classes: - perms.extend( - self.get_required_permissions(request.method, model_class)) + perms.extend(self.get_required_permissions(request.method, model_class)) - if (request.user and (request.user.is_authenticated - or not self.authenticated_users_only) - and request.user.has_perms(perms)): + if ( + request.user + and (request.user.is_authenticated or not self.authenticated_users_only) + and request.user.has_perms(perms) + ): return True return False -# pylint: disable=R0903 +# pylint: disable=too-few-public-methods class HasMetadataPermissionMixin(AbstractHasPermissionMixin): """ Use the Project, XForm, or both model classes to check permissions based @@ -312,13 +327,14 @@ def has_permission(self, request, view): else: self.model_classes = [Project, XForm] - return super(HasMetadataPermissionMixin, self).has_permission( - request, view) + return super().has_permission(request, view) -class MetaDataObjectPermissions(AlternateHasObjectPermissionMixin, - HasMetadataPermissionMixin, - DjangoObjectPermissions): +class MetaDataObjectPermissions( + AlternateHasObjectPermissionMixin, + HasMetadataPermissionMixin, + DjangoObjectPermissions, +): """ MetaData ObjectPermissions - apply Xform permision for given response. """ @@ -332,26 +348,25 @@ def has_object_permission(self, request, view, obj): model_cls = XForm xform_obj = obj.content_object.xform - return self._has_object_permission(request, model_cls, user, - xform_obj) + return self._has_object_permission(request, model_cls, user, xform_obj) - return self._has_object_permission(request, model_cls, user, - obj.content_object) + return self._has_object_permission(request, model_cls, user, obj.content_object) -class AttachmentObjectPermissions(AlternateHasObjectPermissionMixin, - DjangoObjectPermissions): +class AttachmentObjectPermissions( + AlternateHasObjectPermissionMixin, DjangoObjectPermissions +): """ Attachment ObjectPermissions - apply XForm model options. """ + authenticated_users_only = False def has_object_permission(self, request, view, obj): model_cls = XForm user = request.user - return self._has_object_permission(request, model_cls, user, - obj.instance.xform) + return self._has_object_permission(request, model_cls, user, obj.instance.xform) class ConnectViewsetPermissions(IsAuthenticated): @@ -360,11 +375,10 @@ class ConnectViewsetPermissions(IsAuthenticated): """ def has_permission(self, request, view): - if view.action == 'reset': + if view.action == "reset": return True - return super(ConnectViewsetPermissions, self)\ - .has_permission(request, view) + return super().has_permission(request, view) class UserViewSetPermissions(DjangoModelPermissionsOrAnonReadOnly): @@ -374,17 +388,19 @@ class UserViewSetPermissions(DjangoModelPermissionsOrAnonReadOnly): def has_permission(self, request, view): - if request.user.is_anonymous and view.action == 'list': - if request.GET.get('search'): + if request.user.is_anonymous and view.action == "list": + if request.GET.get("search"): raise exceptions.NotAuthenticated() - return \ - super(UserViewSetPermissions, self).has_permission(request, view) + return super().has_permission(request, view) class DataViewViewsetPermissions( - AlternateHasObjectPermissionMixin, ViewDjangoObjectPermissions, - AbstractHasPermissionMixin, DjangoObjectPermissions): + AlternateHasObjectPermissionMixin, + ViewDjangoObjectPermissions, + AbstractHasPermissionMixin, + DjangoObjectPermissions, +): """ DataView ViewSetPermissions - applies projet permissions to a filtered dataset. @@ -396,7 +412,7 @@ def has_permission(self, request, view): # To allow individual public dataviews to be visible on # `api/v1/dataviews/` but stop retreival of all dataviews when # the dataviews endpoint is queried `api/v1/dataviews` - return not (request.user.is_anonymous and view.action == 'list') + return not (request.user.is_anonymous and view.action == "list") def has_object_permission(self, request, view, obj): model_cls = Project @@ -404,13 +420,14 @@ def has_object_permission(self, request, view, obj): if obj.project.shared: return True - return self._has_object_permission(request, model_cls, user, - obj.project) + return self._has_object_permission(request, model_cls, user, obj.project) -class RestServiceObjectPermissions(AlternateHasObjectPermissionMixin, - HasMetadataPermissionMixin, - DjangoObjectPermissions): +class RestServiceObjectPermissions( + AlternateHasObjectPermissionMixin, + HasMetadataPermissionMixin, + DjangoObjectPermissions, +): """ RestService ObjectPermissions - apply XForm permisions for a RestService model. @@ -424,8 +441,11 @@ def has_object_permission(self, request, view, obj): class WidgetViewSetPermissions( - AlternateHasObjectPermissionMixin, ViewDjangoObjectPermissions, - AbstractHasPermissionMixin, DjangoObjectPermissions): + AlternateHasObjectPermissionMixin, + ViewDjangoObjectPermissions, + AbstractHasPermissionMixin, + DjangoObjectPermissions, +): """ Widget ViewSetPermissions - apply project permissions check. """ @@ -435,11 +455,10 @@ class WidgetViewSetPermissions( def has_permission(self, request, view): # User can access the widget with key - if 'key' in request.query_params or view.action == 'list': + if "key" in request.query_params or view.action == "list": return True - return super(WidgetViewSetPermissions, self).has_permission( - request, view) + return super().has_permission(request, view) def has_object_permission(self, request, view, obj): model_cls = Project @@ -448,16 +467,21 @@ def has_object_permission(self, request, view, obj): if not isinstance(obj.content_object, (XForm, DataView)): return False - xform = obj.content_object if isinstance(obj.content_object, XForm) \ + xform = ( + obj.content_object + if isinstance(obj.content_object, XForm) else obj.content_object.xform + ) - if view.action == 'partial_update' and \ - ReadOnlyRoleNoDownload.user_has_role(user, xform): + if view.action == "partial_update" and ReadOnlyRoleNoDownload.user_has_role( + user, xform + ): # allow readonlynodownload and above roles to edit widget return True - return self._has_object_permission(request, model_cls, user, - obj.content_object.project) + return self._has_object_permission( + request, model_cls, user, obj.content_object.project + ) __permissions__ = [DjangoObjectPermissions, IsAuthenticated] @@ -469,37 +493,36 @@ class OrganizationProfilePermissions(DjangoObjectPermissionsAllowAnon): """ def has_object_permission(self, request, view, obj): - is_authenticated = request and request.user.is_authenticated and \ - request.user.username == request.data.get( - 'username') - if is_authenticated and request.method == 'DELETE': + is_authenticated = ( + request + and request.user.is_authenticated + and request.user.username == request.data.get("username") + ) + if is_authenticated and request.method == "DELETE": return True - return super(OrganizationProfilePermissions, self)\ - .has_object_permission(request=request, view=view, obj=obj) + return super().has_object_permission(request=request, view=view, obj=obj) -class OpenDataViewSetPermissions(IsAuthenticated, - AlternateHasObjectPermissionMixin, - DjangoObjectPermissionsAllowAnon): +class OpenDataViewSetPermissions( + IsAuthenticated, AlternateHasObjectPermissionMixin, DjangoObjectPermissionsAllowAnon +): """ OpenDataViewSetPermissions - allow anonymous access to schema and data end-points of an open dataset. """ def has_permission(self, request, view): - if request.user.is_anonymous and view.action in ['schema', 'data']: + if request.user.is_anonymous and view.action in ["schema", "data"]: return True - return super(OpenDataViewSetPermissions, self).has_permission( - request, view) + return super().has_permission(request, view) def has_object_permission(self, request, view, obj): model_cls = XForm user = request.user - return self._has_object_permission(request, model_cls, user, - obj.content_object) + return self._has_object_permission(request, model_cls, user, obj.content_object) class IsAuthenticatedSubmission(BasePermission): @@ -509,9 +532,9 @@ class IsAuthenticatedSubmission(BasePermission): """ def has_permission(self, request, view): - username = view.kwargs.get('username') - form_pk = view.kwargs.get('xform_pk') - if request.method in ['HEAD', 'POST'] and request.user.is_anonymous: + username = view.kwargs.get("username") + form_pk = view.kwargs.get("xform_pk") + if request.method in ["HEAD", "POST"] and request.user.is_anonymous: if username: user = get_object_or_404(User, username__iexact=username) elif form_pk: diff --git a/onadata/apps/api/storage.py b/onadata/apps/api/storage.py index 5cf19e3d1a..f00a0c0e46 100644 --- a/onadata/apps/api/storage.py +++ b/onadata/apps/api/storage.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Storage module for the api app """ @@ -16,7 +17,7 @@ _l = logging.getLogger(__name__) _l.setLevel(logging.WARNING) -ODK_KEY_LIFETIME_IN_SEC = getattr(settings, 'ODK_KEY_LIFETIME', 7) * 86400 +ODK_KEY_LIFETIME_IN_SEC = getattr(settings, "ODK_KEY_LIFETIME", 7) * 86400 class ODKTokenAccountStorage(AccountStorage): @@ -27,6 +28,7 @@ class ODKTokenAccountStorage(AccountStorage): Digest Authentication set the DIGEST_ACCOUNT_BACKEND variable in your local_settings to 'onadata.apps.api.storage.ODKTokenAccountStorage' """ + GET_PARTIAL_DIGEST_QUERY = f""" SELECT django_digest_partialdigest.login, django_digest_partialdigest.partial_digest @@ -41,7 +43,7 @@ class ODKTokenAccountStorage(AccountStorage): AND api_odktoken.status='{ODKToken.ACTIVE}' """ - def get_partial_digest(self, login): + def get_partial_digest(self, username): """ Checks that the returned partial digest is associated with a Token that isn't past it's expire date. @@ -50,23 +52,23 @@ def get_partial_digest(self, login): its expiry date """ cursor = connection.cursor() - cursor.execute(self.GET_PARTIAL_DIGEST_QUERY, [login]) + cursor.execute(self.GET_PARTIAL_DIGEST_QUERY, [username]) # In MySQL, string comparison is case-insensitive by default. # Therefore a second round of filtering is required. - partial_digest = [(row[1]) for row in cursor.fetchall() - if row[0] == login] + partial_digest = [(row[1]) for row in cursor.fetchall() if row[0] == username] if not partial_digest: return None try: - token = ODKToken.objects.get(Q(user__username=login) - | Q(user__email=login), - status=ODKToken.ACTIVE) + token = ODKToken.objects.get( + Q(user__username=username) | Q(user__email=username), + status=ODKToken.ACTIVE, + ) except MultipleObjectsReturned: - _l.warn(f'User {login} has multiple ODK Tokens') + _l.error("User %s has multiple ODK Tokens", username) return None except ODKToken.DoesNotExist: - _l.warn(f'User {login} has no active ODK Token') + _l.error("User %s has no active ODK Token", username) return None else: if timezone.now() > token.expires: diff --git a/onadata/apps/api/tasks.py b/onadata/apps/api/tasks.py index 6f965e7943..d74cf87521 100644 --- a/onadata/apps/api/tasks.py +++ b/onadata/apps/api/tasks.py @@ -1,21 +1,28 @@ +# -*- coding: utf-8 -*- +""" +Celery api.tasks module. +""" import os import sys -from builtins import str from celery.result import AsyncResult from django.core.files.uploadedfile import TemporaryUploadedFile from django.core.files.storage import default_storage -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.utils.datastructures import MultiValueDict from onadata.apps.api import tools from onadata.libs.utils.email import send_generic_email from onadata.apps.logger.models.xform import XForm -from onadata.celery import app +from onadata.celeryapp import app + +User = get_user_model() def recreate_tmp_file(name, path, mime_type): + """Creates a TemporaryUploadedFile from a file path with given name""" tmp_file = TemporaryUploadedFile(name, mime_type, 0, None) + # pylint: disable=consider-using-with,unspecified-encoding tmp_file.file = open(path) tmp_file.size = os.fstat(tmp_file.fileno()).st_size return tmp_file @@ -23,6 +30,7 @@ def recreate_tmp_file(name, path, mime_type): @app.task(bind=True) def publish_xlsform_async(self, user_id, post_data, owner_id, file_data): + """Publishes an XLSForm""" try: files = MultiValueDict() files["xls_file"] = default_storage.open(file_data.get("path")) @@ -39,7 +47,7 @@ def publish_xlsform_async(self, user_id, post_data, owner_id, file_data): return {"pk": survey.pk} return survey - except Exception as exc: + except Exception as exc: # pylint: disable=broad-except if isinstance(exc, MemoryError): if self.request.retries < 3: self.retry(exc=exc, countdown=1) @@ -96,4 +104,5 @@ def send_verification_email(email, message_txt, subject): @app.task() def send_account_lockout_email(email, message_txt, subject): + """Sends account locked email.""" send_generic_email(email, message_txt, subject) diff --git a/onadata/apps/api/tests/mocked_data.py b/onadata/apps/api/tests/mocked_data.py index af76c1f064..fdc737a5ae 100644 --- a/onadata/apps/api/tests/mocked_data.py +++ b/onadata/apps/api/tests/mocked_data.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Contians mock functions used in some tests for example enketo urls and exports urls. diff --git a/onadata/apps/api/tests/models/test_team.py b/onadata/apps/api/tests/models/test_team.py index 771d642bf2..65408ffea8 100644 --- a/onadata/apps/api/tests/models/test_team.py +++ b/onadata/apps/api/tests/models/test_team.py @@ -1,46 +1,50 @@ from django.contrib.auth.models import Permission + from guardian.shortcuts import get_perms from onadata.apps.api import tools -from onadata.apps.logger.models.project import Project +from onadata.apps.api.models.organization_profile import create_organization_team from onadata.apps.api.models.team import Team -from onadata.apps.api.tests.models.test_abstract_models import ( - TestAbstractModels) +from onadata.apps.api.tests.models.test_abstract_models import TestAbstractModels +from onadata.apps.logger.models.project import Project from onadata.libs.permissions import ( - DataEntryRole, - CAN_VIEW_PROJECT, - CAN_ADD_XFORM, CAN_ADD_SUBMISSIONS_PROJECT, + CAN_ADD_XFORM, CAN_EXPORT_PROJECT, - CAN_VIEW_PROJECT_DATA, + CAN_VIEW_PROJECT, CAN_VIEW_PROJECT_ALL, - get_team_project_default_permissions) + CAN_VIEW_PROJECT_DATA, + DataEntryRole, + get_team_project_default_permissions, +) class TestTeam(TestAbstractModels): - def test_create_organization_team(self): profile = tools.create_organization_object("modilabs", self.user) profile.save() organization = profile.user - team_name = 'dev' - perms = ['is_org_owner', ] - tools.create_organization_team(organization, team_name, perms) + team_name = "dev" + perms = [ + "is_org_owner", + ] + create_organization_team(organization, team_name, perms) team_name = "modilabs#%s" % team_name dev_team = Team.objects.get(organization=organization, name=team_name) self.assertIsInstance(dev_team, Team) self.assertIsInstance( - dev_team.permissions.get(codename='is_org_owner'), Permission) + dev_team.permissions.get(codename="is_org_owner"), Permission + ) def test_assign_user_to_team(self): # create the organization organization = self._create_organization("modilabs", self.user) - user_deno = self._create_user('deno', 'deno') + user_deno = self._create_user("deno", "deno") # create another team - team_name = 'managers' - team = tools.create_organization_team(organization, team_name) + team_name = "managers" + team = create_organization_team(organization, team_name) tools.add_user_to_team(team, user_deno) self.assertIn(team.group_ptr, user_deno.groups.all()) @@ -50,7 +54,7 @@ def test_add_team_to_project(self): project_name = "demo" team_name = "enumerators" project = self._create_project(organization, project_name, self.user) - team = tools.create_organization_team(organization, team_name) + team = create_organization_team(organization, team_name) result = tools.add_team_to_project(team, project) self.assertTrue(result) @@ -59,32 +63,41 @@ def test_add_team_to_project(self): def test_add_project_perms_to_team(self): # create an org, user, team organization = self._create_organization("test org", self.user) - user_deno = self._create_user('deno', 'deno') + user_deno = self._create_user("deno", "deno") # add a member to the team - team = tools.create_organization_team(organization, "test team") + team = create_organization_team(organization, "test team") tools.add_user_to_team(team, user_deno) - project = Project.objects.create(name="Test Project", - organization=organization, - created_by=user_deno, - metadata='{}') + project = Project.objects.create( + name="Test Project", + organization=organization, + created_by=user_deno, + metadata="{}", + ) # confirm that the team has no permissions on project self.assertFalse(get_perms(team, project)) # set DataEntryRole role of project on team DataEntryRole.add(team, project) - self.assertEqual([CAN_EXPORT_PROJECT, CAN_ADD_SUBMISSIONS_PROJECT, - CAN_VIEW_PROJECT, CAN_VIEW_PROJECT_ALL, - CAN_VIEW_PROJECT_DATA], - sorted(get_perms(team, project))) - - self.assertEqual(get_team_project_default_permissions(team, project), - DataEntryRole.name) + self.assertEqual( + [ + CAN_EXPORT_PROJECT, + CAN_ADD_SUBMISSIONS_PROJECT, + CAN_VIEW_PROJECT, + CAN_VIEW_PROJECT_ALL, + CAN_VIEW_PROJECT_DATA, + ], + sorted(get_perms(team, project)), + ) + + self.assertEqual( + get_team_project_default_permissions(team, project), DataEntryRole.name + ) # Add a new user - user_sam = self._create_user('Sam', 'sammy_') + user_sam = self._create_user("Sam", "sammy_") self.assertFalse(user_sam.has_perm(CAN_VIEW_PROJECT, project)) self.assertFalse(user_sam.has_perm(CAN_ADD_XFORM, project)) diff --git a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py index b3092c9bb0..27c1fbd2d4 100644 --- a/onadata/apps/api/tests/viewsets/test_abstract_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_abstract_viewset.py @@ -5,15 +5,15 @@ import json import os import re +import warnings from tempfile import NamedTemporaryFile -import requests - from django.conf import settings from django.contrib.auth import authenticate, get_user_model from django.contrib.auth.models import Permission from django.test import TestCase +import requests from django_digest.test import Client as DigestClient from django_digest.test import DigestAuth from httmock import HTTMock @@ -41,10 +41,11 @@ from onadata.libs.utils.common_tools import merge_dicts from onadata.libs.utils.user_auth import get_user_default_project - # pylint: disable=invalid-name User = get_user_model() +warnings.simplefilter("ignore") + def _set_api_permissions(user): add_userprofile = Permission.objects.get( @@ -471,7 +472,8 @@ def _post_metadata(self, data, test=True): "/", data=data, **self.extra, - format='json' if 'extra_data' in data else None) + format="json" if "extra_data" in data else None, + ) response = view(request) @@ -494,11 +496,7 @@ def _add_form_metadata( test=True, extra_data=None, ): - data = { - "data_type": data_type, - "data_value": data_value, - "xform": xform.id - } + data = {"data_type": data_type, "data_value": data_value, "xform": xform.id} if extra_data: data.update({"extra_data": extra_data}) diff --git a/onadata/apps/api/tests/viewsets/test_connect_viewset.py b/onadata/apps/api/tests/viewsets/test_connect_viewset.py index ae3053e51d..ab4911f442 100644 --- a/onadata/apps/api/tests/viewsets/test_connect_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_connect_viewset.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Test /user API endpoint """ diff --git a/onadata/apps/api/tests/viewsets/test_floip_viewset.py b/onadata/apps/api/tests/viewsets/test_floip_viewset.py index 773dabc890..579d9feee4 100644 --- a/onadata/apps/api/tests/viewsets/test_floip_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_floip_viewset.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Test FloipViewset module. """ diff --git a/onadata/apps/api/tests/viewsets/test_media_viewset.py b/onadata/apps/api/tests/viewsets/test_media_viewset.py index edc94e5987..771d13b1c5 100644 --- a/onadata/apps/api/tests/viewsets/test_media_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_media_viewset.py @@ -2,57 +2,56 @@ import urllib from mock import MagicMock, patch -from onadata.apps.api.tests.viewsets.test_abstract_viewset import \ - TestAbstractViewSet +from onadata.apps.api.tests.viewsets.test_abstract_viewset import TestAbstractViewSet from onadata.apps.api.viewsets.media_viewset import MediaViewSet from onadata.apps.logger.models import Attachment def attachment_url(attachment, suffix=None): - url = u'http://testserver/api/v1/files/{}?filename={}'.format( - attachment.pk, attachment.media_file.name) + url = "http://testserver/api/v1/files/{}?filename={}".format( + attachment.pk, attachment.media_file.name + ) if suffix: - url += u'?suffix={}'.format(suffix) + url += "?suffix={}".format(suffix) return url class TestMediaViewSet(TestAbstractViewSet): - def setUp(self): super(TestMediaViewSet, self).setUp() - self.retrieve_view = MediaViewSet.as_view({ - 'get': 'retrieve' - }) + self.retrieve_view = MediaViewSet.as_view({"get": "retrieve"}) self._publish_xls_form_to_project() self._submit_transport_instance_w_attachment() def test_retrieve_view(self): - request = self.factory.get('/', { - 'filename': self.attachment.media_file.name}, **self.extra) + request = self.factory.get( + "/", {"filename": self.attachment.media_file.name}, **self.extra + ) response = self.retrieve_view(request, self.attachment.pk) - self.assertEqual(response.status_code, 302) + self.assertEqual(response.status_code, 200) self.assertEqual(type(response.content), bytes) - @patch('onadata.libs.utils.image_tools.get_storage_class') - @patch('onadata.libs.utils.image_tools.boto3.client') - def test_retrieve_view_from_s3( - self, mock_presigned_urls, mock_get_storage_class): + @patch("onadata.libs.utils.image_tools.get_storage_class") + @patch("onadata.libs.utils.image_tools.boto3.client") + def test_retrieve_view_from_s3(self, mock_presigned_urls, mock_get_storage_class): expected_url = ( - 'https://testing.s3.amazonaws.com/doe/attachments/' - '4_Media_file/media.png?' - 'response-content-disposition=attachment%3Bfilename%3media.png&' - 'response-content-type=application%2Foctet-stream&' - 'AWSAccessKeyId=AKIAJ3XYHHBIJDL7GY7A' - '&Signature=aGhiK%2BLFVeWm%2Fmg3S5zc05g8%3D&Expires=1615554960') + "https://testing.s3.amazonaws.com/doe/attachments/" + "4_Media_file/media.png?" + "response-content-disposition=attachment%3Bfilename%3media.png&" + "response-content-type=application%2Foctet-stream&" + "AWSAccessKeyId=AKIAJ3XYHHBIJDL7GY7A" + "&Signature=aGhiK%2BLFVeWm%2Fmg3S5zc05g8%3D&Expires=1615554960" + ) mock_presigned_urls().generate_presigned_url = MagicMock( return_value=expected_url ) - mock_get_storage_class()().bucket.name = 'onadata' - request = self.factory.get('/', { - 'filename': self.attachment.media_file.name}, **self.extra) + mock_get_storage_class()().bucket.name = "onadata" + request = self.factory.get( + "/", {"filename": self.attachment.media_file.name}, **self.extra + ) response = self.retrieve_view(request, self.attachment.pk) self.assertEqual(response.status_code, 302, response.url) @@ -60,87 +59,104 @@ def test_retrieve_view_from_s3( self.assertTrue(mock_presigned_urls.called) filename = self.attachment.media_file.name.split("/")[-1] mock_presigned_urls().generate_presigned_url.assert_called_with( - 'get_object', + "get_object", Params={ - 'Bucket': 'onadata', - 'Key': self.attachment.media_file.name, - 'ResponseContentDisposition': urllib.parse.quote( - f'attachment; filename={filename}'), - 'ResponseContentType': 'application/octet-stream'}, - ExpiresIn=3600) + "Bucket": "onadata", + "Key": self.attachment.media_file.name, + "ResponseContentDisposition": urllib.parse.quote( + f"attachment; filename={filename}" + ), + "ResponseContentType": "application/octet-stream", + }, + ExpiresIn=3600, + ) def test_retrieve_view_with_suffix(self): - request = self.factory.get('/', { - 'filename': self.attachment.media_file.name, 'suffix': 'large'}, - **self.extra) + request = self.factory.get( + "/", + {"filename": self.attachment.media_file.name, "suffix": "large"}, + **self.extra, + ) response = self.retrieve_view(request, self.attachment.pk) self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'], attachment_url(self.attachment)) + self.assertTrue(response["Location"], attachment_url(self.attachment)) - @patch('onadata.apps.api.viewsets.media_viewset.image_url') + @patch("onadata.apps.api.viewsets.media_viewset.image_url") def test_handle_image_exception(self, mock_image_url): mock_image_url.side_effect = Exception() request = self.factory.get( - '/', - {'filename': self.attachment.media_file.name, 'suffix': 'large'}, - **self.extra + "/", + {"filename": self.attachment.media_file.name, "suffix": "large"}, + **self.extra, ) response = self.retrieve_view(request, self.attachment.pk) self.assertEqual(response.status_code, 400) def test_retrieve_view_small(self): request = self.factory.get( - '/', - {'filename': self.attachment.media_file.name, 'suffix': 'small'}, - **self.extra + "/", + {"filename": self.attachment.media_file.name, "suffix": "small"}, + **self.extra, ) response = self.retrieve_view(request, self.attachment.pk) self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'], - attachment_url(self.attachment, 'small')) + self.assertTrue(response["Location"], attachment_url(self.attachment, "small")) def test_retrieve_view_invalid_suffix(self): request = self.factory.get( - '/', - {'filename': self.attachment.media_file.name, 'suffix': 'TK'}, - **self.extra + "/", + {"filename": self.attachment.media_file.name, "suffix": "TK"}, + **self.extra, ) response = self.retrieve_view(request, self.attachment.pk) self.assertEqual(response.status_code, 404) def test_retrieve_view_invalid_pk(self): request = self.factory.get( - '/', - {'filename': self.attachment.media_file.name, 'suffix': 'small'}, - **self.extra + "/", + {"filename": self.attachment.media_file.name, "suffix": "small"}, + **self.extra, ) - response = self.retrieve_view(request, 'INVALID') + response = self.retrieve_view(request, "INVALID") self.assertEqual(response.status_code, 404) def test_retrieve_view_no_filename_param(self): - request = self.factory.get('/', **self.extra) + request = self.factory.get("/", **self.extra) response = self.retrieve_view(request, self.attachment.pk) self.assertEqual(response.status_code, 404) def test_retrieve_small_png(self): """Test retrieve png images""" - s = 'transport_2011-07-25_19-05-49_1' + s = "transport_2011-07-25_19-05-49_1" media_file = "ona_png_image.png" - path = os.path.join(self.main_directory, 'fixtures', - 'transportation', 'instances', s, media_file) - with open(path, 'rb') as f: - self._make_submission(os.path.join( - self.main_directory, 'fixtures', - 'transportation', 'instances', s, s + '.xml'), media_file=f) + path = os.path.join( + self.main_directory, + "fixtures", + "transportation", + "instances", + s, + media_file, + ) + with open(path, "rb") as f: + self._make_submission( + os.path.join( + self.main_directory, + "fixtures", + "transportation", + "instances", + s, + s + ".xml", + ), + media_file=f, + ) attachment = Attachment.objects.all().reverse()[0] self.attachment = attachment request = self.factory.get( - '/', - {'filename': self.attachment.media_file.name, 'suffix': 'small'}, - **self.extra + "/", + {"filename": self.attachment.media_file.name, "suffix": "small"}, + **self.extra, ) response = self.retrieve_view(request, self.attachment.pk) self.assertEqual(response.status_code, 302) - self.assertTrue(response['Location'], - attachment_url(self.attachment, 'small')) + self.assertTrue(response["Location"], attachment_url(self.attachment, "small")) diff --git a/onadata/apps/api/tests/viewsets/test_project_viewset.py b/onadata/apps/api/tests/viewsets/test_project_viewset.py index 77b0973846..4fd9546d2a 100644 --- a/onadata/apps/api/tests/viewsets/test_project_viewset.py +++ b/onadata/apps/api/tests/viewsets/test_project_viewset.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Test ProjectViewSet module. """ diff --git a/onadata/apps/api/tools.py b/onadata/apps/api/tools.py index 7d27ad3938..18da1b990e 100644 --- a/onadata/apps/api/tools.py +++ b/onadata/apps/api/tools.py @@ -1,6 +1,6 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ -API util functions. +API utility functions. """ import os import tempfile @@ -9,8 +9,6 @@ from django import forms from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.auth.models import Permission -from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site from django.core.files.storage import get_storage_class from django.core.files.uploadedfile import InMemoryUploadedFile @@ -23,7 +21,7 @@ from django.utils.module_loading import import_string from django.utils.translation import gettext as _ -from guardian.shortcuts import assign_perm, get_perms, get_perms_for_model, remove_perm +from guardian.shortcuts import get_perms, get_perms_for_model, remove_perm from kombu.exceptions import OperationalError from multidb.pinning import use_master from registration.models import RegistrationProfile @@ -33,7 +31,9 @@ from onadata.apps.api.models.organization_profile import ( OrganizationProfile, - create_owner_team_and_assign_permissions, + add_user_to_team, + get_or_create_organization_owners_team, + get_organization_members_team, ) from onadata.apps.api.models.team import Team from onadata.apps.logger.models import DataView, Instance, Project, XForm @@ -55,8 +55,10 @@ get_role_in_org, is_organization, ) +from onadata.libs.serializers.project_serializer import ProjectSerializer from onadata.libs.utils.api_export_tools import ( - custom_response_handler, get_metadata_format + custom_response_handler, + get_metadata_format, ) from onadata.libs.utils.cache_tools import ( PROJ_BASE_FORMS_CACHE, @@ -181,60 +183,6 @@ def create_organization_object(org_name, creator, attrs=None): return profile -def create_organization_team(organization, name, permission_names=None): - """ - Creates an organization team with the given permissions as defined in - permission_names. - """ - organization = ( - organization.user - if isinstance(organization, OrganizationProfile) - else organization - ) - team = Team.objects.create(organization=organization, name=name) - content_type = ContentType.objects.get(app_label="api", model="organizationprofile") - if permission_names: - # get permission objects - perms = Permission.objects.filter( - codename__in=permission_names, content_type=content_type - ) - if perms: - team.permissions.add(*tuple(perms)) - return team - - -def get_organization_members_team(organization): - """Get organization members team - create members team if it does not exist and add organization owner - to the members team""" - try: - team = Team.objects.get(name=f"{organization.user.username}#{MEMBERS}") - except Team.DoesNotExist: - team = create_organization_team(organization, MEMBERS) - add_user_to_team(team, organization.user) - - return team - - -# pylint: disable=invalid-name -def get_or_create_organization_owners_team(org): - """ - Get the owners team of an organization - :param org: organization - :return: Owners team of the organization - """ - team_name = f"{org.user.username}#{Team.OWNER_TEAM_NAME}" - try: - team = Team.objects.get(name=team_name, organization=org.user) - except Team.DoesNotExist: - with use_master: - queryset = Team.objects.filter(name=team_name, organization=org.user) - if queryset.count() > 0: - return queryset.first() # pylint: disable=no-member - return create_owner_team_and_assign_permissions(org) - return team - - def remove_user_from_organization(organization, user): """Remove a user from an organization""" team = get_organization_members_team(organization) @@ -281,28 +229,6 @@ def add_user_to_organization(organization, user): add_user_to_team(team, user) -def add_user_to_team(team, user): - """ - Adds a user to a team and assigns them team permissions. - """ - user.groups.add(team) - - # give the user perms to view the team - assign_perm("view_team", user, team) - - # if team is owners team assign more perms - if team.name.find(Team.OWNER_TEAM_NAME) > 0: - _assign_organization_team_perms(team.organization, user) - - -def _assign_organization_team_perms(organization, user): - owners_team = get_or_create_organization_owners_team(organization.profile) - members_team = get_organization_members_team(organization.profile) - for perm in get_perms_for_model(Team): - assign_perm(perm.codename, user, owners_team) - assign_perm(perm.codename, user, members_team) - - def get_organization_members(organization): """Get members team user queryset""" team = get_organization_members_team(organization) @@ -491,7 +417,7 @@ def id_string_exists_in_account(): # Ensure the cached project is the updated version. # Django lazy loads related objects as such we need to # ensure the project retrieved is up to date. - reset_project_cache(xform.project, request) + reset_project_cache(xform.project, request, ProjectSerializer) return xform diff --git a/onadata/apps/api/urls/v1_urls.py b/onadata/apps/api/urls/v1_urls.py index 15d3a830a9..e4bcb249ad 100644 --- a/onadata/apps/api/urls/v1_urls.py +++ b/onadata/apps/api/urls/v1_urls.py @@ -1,9 +1,10 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Custom rest_framework Router - MultiLookupRouter. """ -from django.urls import re_path from django.contrib import admin +from django.urls import re_path + from rest_framework import routers from rest_framework.urlpatterns import format_suffix_patterns @@ -11,8 +12,7 @@ from onadata.apps.api.viewsets.briefcase_viewset import BriefcaseViewset from onadata.apps.api.viewsets.charts_viewset import ChartsViewSet from onadata.apps.api.viewsets.connect_viewset import ConnectViewSet -from onadata.apps.api.viewsets.data_viewset import (AuthenticatedDataViewSet, - DataViewSet) +from onadata.apps.api.viewsets.data_viewset import AuthenticatedDataViewSet, DataViewSet from onadata.apps.api.viewsets.dataview_viewset import DataViewViewSet from onadata.apps.api.viewsets.export_viewset import ExportViewSet from onadata.apps.api.viewsets.floip_viewset import FloipViewSet @@ -21,26 +21,23 @@ from onadata.apps.api.viewsets.metadata_viewset import MetaDataViewSet from onadata.apps.api.viewsets.note_viewset import NoteViewSet from onadata.apps.api.viewsets.open_data_viewset import OpenDataViewSet -from onadata.apps.api.viewsets.organization_profile_viewset import \ - OrganizationProfileViewSet +from onadata.apps.api.viewsets.organization_profile_viewset import ( + OrganizationProfileViewSet, +) from onadata.apps.api.viewsets.osm_viewset import OsmViewSet from onadata.apps.api.viewsets.project_viewset import ProjectViewSet from onadata.apps.api.viewsets.stats_viewset import StatsViewSet -from onadata.apps.api.viewsets.submission_review_viewset import \ - SubmissionReviewViewSet -from onadata.apps.api.viewsets.submissionstats_viewset import \ - SubmissionStatsViewSet +from onadata.apps.api.viewsets.submission_review_viewset import SubmissionReviewViewSet +from onadata.apps.api.viewsets.submissionstats_viewset import SubmissionStatsViewSet from onadata.apps.api.viewsets.team_viewset import TeamViewSet from onadata.apps.api.viewsets.user_profile_viewset import UserProfileViewSet from onadata.apps.api.viewsets.user_viewset import UserViewSet from onadata.apps.api.viewsets.widget_viewset import WidgetViewSet from onadata.apps.api.viewsets.xform_list_viewset import XFormListViewSet -from onadata.apps.api.viewsets.xform_submission_viewset import \ - XFormSubmissionViewSet +from onadata.apps.api.viewsets.xform_submission_viewset import XFormSubmissionViewSet from onadata.apps.api.viewsets.xform_viewset import XFormViewSet from onadata.apps.messaging.viewsets import MessagingViewSet -from onadata.apps.restservice.viewsets.restservices_viewset import \ - RestServicesViewSet +from onadata.apps.restservice.viewsets.restservices_viewset import RestServicesViewSet admin.autodiscover() @@ -49,21 +46,20 @@ class MultiLookupRouter(routers.DefaultRouter): """ Support multiple lookup keys e.g. /parent_pk/pk """ + multi = False - def get_lookup_regex(self, viewset, lookup_prefix=''): + def get_lookup_regex(self, viewset, lookup_prefix=""): """ Returns a lookup regex, this extends the default to allow for multiple lookup keys as defined by a viewset.lookup_fields property. """ - result = super(MultiLookupRouter, self).get_lookup_regex( - viewset, lookup_prefix) - lookup_fields = getattr(viewset, 'lookup_fields', None) + result = super().get_lookup_regex(viewset, lookup_prefix) + lookup_fields = getattr(viewset, "lookup_fields", None) if lookup_fields and not self.multi: - lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+') + lookup_value = getattr(viewset, "lookup_value_regex", "[^/.]+") for lookup_field in lookup_fields[1:]: - result += '/(?P<{lookup_url_kwarg}>{lookup_value})'.format( - lookup_url_kwarg=lookup_field, lookup_value=lookup_value) + result += f"/(?P<{lookup_field}>{lookup_value})" return result @@ -72,39 +68,41 @@ def get_urls(self): Return a list of URL regexs, this extends the default by adding a {prefix}-list route that accepts a lookup url kwarg. """ - urls = super(MultiLookupRouter, self).get_urls() + urls = super().get_urls() extra_urls = [] for prefix, viewset, basename in self.registry: - lookup_fields = getattr(viewset, 'lookup_fields', None) + lookup_fields = getattr(viewset, "lookup_fields", None) if lookup_fields: route = routers.Route( - url=r'^{prefix}/{lookup}{trailing_slash}$', + url=r"^{prefix}/{lookup}{trailing_slash}$", mapping={ - 'delete': 'destroy', - 'get': 'list', - 'post': 'create', + "delete": "destroy", + "get": "list", + "post": "create", }, - name='{basename}-list', + name="{basename}-list", detail=False, - initkwargs={'suffix': 'List'}) + initkwargs={"suffix": "List"}, + ) self.multi = True lookup = self.get_lookup_regex(viewset) # reset self.multi = False regex = route.url.format( - prefix=prefix, - lookup=lookup, - trailing_slash=self.trailing_slash) + prefix=prefix, lookup=lookup, trailing_slash=self.trailing_slash + ) mapping = self.get_method_map(viewset, route.mapping) if not mapping: continue initkwargs = route.initkwargs.copy() - initkwargs.update({ - 'basename': basename, - 'detail': route.detail, - }) + initkwargs.update( + { + "basename": basename, + "detail": route.detail, + } + ) view = viewset.as_view(mapping, **initkwargs) name = route.name.format(basename=basename) extra_urls.append(re_path(regex, view, name=name)) @@ -115,39 +113,37 @@ def get_urls(self): return urls -router = MultiLookupRouter(trailing_slash=False) # pylint: disable=c0103 -router.register(r'briefcase', BriefcaseViewset, basename='briefcase') -router.register(r'charts', ChartsViewSet, basename='chart') -router.register(r'data', DataViewSet, basename='data') -router.register(r'dataviews', DataViewViewSet, basename='dataviews') -router.register(r'export', ExportViewSet, basename='export') -router.register(r'files', MediaViewSet, basename='files') -router.register( - r'flow-results/packages', FloipViewSet, basename='flow-results') -router.register(r'formlist', XFormListViewSet, basename='formlist') -router.register(r'forms', XFormViewSet) -router.register(r'media', AttachmentViewSet, basename='attachment') -router.register( - r'merged-datasets', MergedXFormViewSet, basename='merged-xform') -router.register(r'messaging', MessagingViewSet, basename="messaging") -router.register(r'metadata', MetaDataViewSet, basename='metadata') -router.register(r'notes', NoteViewSet) -router.register(r'open-data', OpenDataViewSet, basename='open-data') -router.register(r'orgs', OrganizationProfileViewSet) -router.register(r'osm', OsmViewSet, basename='osm') -router.register( - r'private-data', AuthenticatedDataViewSet, basename='private-data') -router.register(r'profiles', UserProfileViewSet) -router.register(r'projects', ProjectViewSet) -router.register(r'restservices', RestServicesViewSet, basename='restservices') -router.register(r'stats', StatsViewSet, basename='stats') -router.register( - r'submissionreview', SubmissionReviewViewSet, basename='submissionreview') +router = MultiLookupRouter(trailing_slash=False) # pylint: disable=invalid-name +router.register(r"briefcase", BriefcaseViewset, basename="briefcase") +router.register(r"charts", ChartsViewSet, basename="chart") +router.register(r"data", DataViewSet, basename="data") +router.register(r"dataviews", DataViewViewSet, basename="dataviews") +router.register(r"export", ExportViewSet, basename="export") +router.register(r"files", MediaViewSet, basename="files") +router.register(r"flow-results/packages", FloipViewSet, basename="flow-results") +router.register(r"formlist", XFormListViewSet, basename="formlist") +router.register(r"forms", XFormViewSet) +router.register(r"media", AttachmentViewSet, basename="attachment") +router.register(r"merged-datasets", MergedXFormViewSet, basename="merged-xform") +router.register(r"messaging", MessagingViewSet, basename="messaging") +router.register(r"metadata", MetaDataViewSet, basename="metadata") +router.register(r"notes", NoteViewSet) +router.register(r"open-data", OpenDataViewSet, basename="open-data") +router.register(r"orgs", OrganizationProfileViewSet) +router.register(r"osm", OsmViewSet, basename="osm") +router.register(r"private-data", AuthenticatedDataViewSet, basename="private-data") +router.register(r"profiles", UserProfileViewSet) +router.register(r"projects", ProjectViewSet) +router.register(r"restservices", RestServicesViewSet, basename="restservices") +router.register(r"stats", StatsViewSet, basename="stats") router.register( - r'stats/submissions', SubmissionStatsViewSet, basename='submissionstats') + r"submissionreview", SubmissionReviewViewSet, basename="submissionreview" +) router.register( - r'submissions', XFormSubmissionViewSet, basename='submissions') -router.register(r'teams', TeamViewSet) -router.register(r'user', ConnectViewSet) -router.register(r'users', UserViewSet, basename='user') -router.register(r'widgets', WidgetViewSet, basename='widgets') + r"stats/submissions", SubmissionStatsViewSet, basename="submissionstats" +) +router.register(r"submissions", XFormSubmissionViewSet, basename="submissions") +router.register(r"teams", TeamViewSet) +router.register(r"user", ConnectViewSet) +router.register(r"users", UserViewSet, basename="user") +router.register(r"widgets", WidgetViewSet, basename="widgets") diff --git a/onadata/apps/api/urls/v2_urls.py b/onadata/apps/api/urls/v2_urls.py index 34da69ad81..0751997480 100644 --- a/onadata/apps/api/urls/v2_urls.py +++ b/onadata/apps/api/urls/v2_urls.py @@ -1,9 +1,10 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Custom rest_framework Router V2 """ -from .v1_urls import MultiLookupRouter from onadata.apps.api.viewsets.v2.tableau_viewset import TableauViewSet +from .v1_urls import MultiLookupRouter + router = MultiLookupRouter(trailing_slash=False) -router.register(r'open-data', TableauViewSet, basename='open-data') +router.register(r"open-data", TableauViewSet, basename="open-data") diff --git a/onadata/apps/api/viewsets/attachment_viewset.py b/onadata/apps/api/viewsets/attachment_viewset.py index ac5af6a90f..2a92b1ecd8 100644 --- a/onadata/apps/api/viewsets/attachment_viewset.py +++ b/onadata/apps/api/viewsets/attachment_viewset.py @@ -1,11 +1,13 @@ -from builtins import str as text - +# -*- coding: utf-8 -*- +""" +The /api/v1/attachments API implementation. +""" +from django.conf import settings +from django.core.files.storage import default_storage from django.http import Http404 from django.utils.translation import gettext as _ -from django.core.files.storage import default_storage -from django.conf import settings -from rest_framework import renderers -from rest_framework import viewsets + +from rest_framework import renderers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import ParseError from rest_framework.response import Response @@ -14,41 +16,47 @@ from onadata.apps.logger.models.attachment import Attachment from onadata.apps.logger.models.xform import XForm from onadata.libs import filters -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.libs.pagination import StandardPageNumberPagination +from onadata.libs.renderers.renderers import ( + MediaFileContentNegotiation, + MediaFileRenderer, +) from onadata.libs.serializers.attachment_serializer import AttachmentSerializer -from onadata.libs.renderers.renderers import MediaFileContentNegotiation, \ - MediaFileRenderer from onadata.libs.utils.image_tools import image_url from onadata.libs.utils.viewer_tools import get_path def get_attachment_data(attachment, suffix): + """Returns attachment file contents.""" if suffix in list(settings.THUMB_CONF): image_url(attachment, suffix) - suffix = settings.THUMB_CONF.get(suffix).get('suffix') - f = default_storage.open( - get_path(attachment.media_file.name, suffix)) - data = f.read() - else: - data = attachment.media_file.read() + suffix = settings.THUMB_CONF.get(suffix).get("suffix") + f = default_storage.open(get_path(attachment.media_file.name, suffix)) + return f.read() - return data + return attachment.media_file.read() -class AttachmentViewSet(AuthenticateHeaderMixin, CacheControlMixin, ETagsMixin, - viewsets.ReadOnlyModelViewSet): +# pylint: disable=too-many-ancestors +class AttachmentViewSet( + AuthenticateHeaderMixin, + CacheControlMixin, + ETagsMixin, + viewsets.ReadOnlyModelViewSet, +): """ - List attachments of viewsets. + GET, List attachments implementation. """ + content_negotiation_class = MediaFileContentNegotiation filter_backends = (filters.AttachmentFilter, filters.AttachmentTypeFilter) - lookup_field = 'pk' + lookup_field = "pk" queryset = Attachment.objects.filter( - instance__deleted_at__isnull=True, deleted_at__isnull=True) + instance__deleted_at__isnull=True, deleted_at__isnull=True + ) permission_classes = (AttachmentObjectPermissions,) serializer_class = AttachmentSerializer pagination_class = StandardPageNumberPagination @@ -59,48 +67,51 @@ class AttachmentViewSet(AuthenticateHeaderMixin, CacheControlMixin, ETagsMixin, ) def retrieve(self, request, *args, **kwargs): + # pylint: disable=attribute-defined-outside-init self.object = self.get_object() - if isinstance(request.accepted_renderer, MediaFileRenderer) \ - and self.object.media_file is not None: - suffix = request.query_params.get('suffix') + if ( + isinstance(request.accepted_renderer, MediaFileRenderer) + and self.object.media_file is not None + ): + suffix = request.query_params.get("suffix") try: data = get_attachment_data(self.object, suffix) except IOError as e: - if text(e).startswith('File does not exist'): - raise Http404() + if str(e).startswith("File does not exist"): + raise Http404() from e - raise ParseError(e) + raise ParseError(e) from e else: return Response(data, content_type=self.object.mimetype) - filename = request.query_params.get('filename') + filename = request.query_params.get("filename") serializer = self.get_serializer(self.object) if filename: if filename == self.object.media_file.name: return Response(serializer.get_download_url(self.object)) - else: - raise Http404(_("Filename '%s' not found." % filename)) + + raise Http404(_(f"Filename '{filename}' not found.")) return Response(serializer.data) - @action(methods=['GET'], detail=False) + @action(methods=["GET"], detail=False) def count(self, request, *args, **kwargs): - data = { - "count": self.filter_queryset(self.get_queryset()).count() - } + """Returns the number of attachments the user has access to.""" + data = {"count": self.filter_queryset(self.get_queryset()).count()} return Response(data=data) def list(self, request, *args, **kwargs): if request.user.is_anonymous: - xform = request.query_params.get('xform') + xform = request.query_params.get("xform") if xform: xform = XForm.objects.get(id=xform) if not xform.shared_data: raise Http404(_("Not Found")) + # pylint: disable=attribute-defined-outside-init self.object_list = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(self.object_list) if page is not None: @@ -108,4 +119,4 @@ def list(self, request, *args, **kwargs): return Response(serializer.data) - return super(AttachmentViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) diff --git a/onadata/apps/api/viewsets/briefcase_viewset.py b/onadata/apps/api/viewsets/briefcase_viewset.py index b6fa26753c..7056d80163 100644 --- a/onadata/apps/api/viewsets/briefcase_viewset.py +++ b/onadata/apps/api/viewsets/briefcase_viewset.py @@ -45,7 +45,7 @@ def _extract_uuid(text): if isinstance(text, six.string_types): form_id_parts = text.split("/") - if form_id_parts.__len__() < 2: + if len(form_id_parts) < 2: raise ValidationError(_(f"Invalid formId {text}.")) text = form_id_parts[1] @@ -111,7 +111,9 @@ def get_object(self, queryset=None): # pylint: disable=too-many-branches def filter_queryset(self, queryset): - """Filters an XForm submission instances using ODK Aggregate query parameters.""" + """ + Filters an XForm submission instances using ODK Aggregate query parameters. + """ username = self.kwargs.get("username") if username is None and self.request.user.is_anonymous: # raises a permission denied exception, forces authentication diff --git a/onadata/apps/api/viewsets/charts_viewset.py b/onadata/apps/api/viewsets/charts_viewset.py index a6833ff546..0251f4f7f4 100644 --- a/onadata/apps/api/viewsets/charts_viewset.py +++ b/onadata/apps/api/viewsets/charts_viewset.py @@ -3,37 +3,42 @@ /charts api endpoint for chart data and chart widgets """ -from django.core.exceptions import ImproperlyConfigured -from django.core.cache import cache from django.conf import settings +from django.core.cache import cache +from django.core.exceptions import ImproperlyConfigured from rest_framework import viewsets from rest_framework.exceptions import ParseError -from rest_framework.renderers import (BrowsableAPIRenderer, JSONRenderer, - TemplateHTMLRenderer) +from rest_framework.renderers import ( + BrowsableAPIRenderer, + JSONRenderer, + TemplateHTMLRenderer, +) from rest_framework.response import Response from onadata.apps.api.permissions import XFormPermissions from onadata.apps.logger.models.xform import XForm from onadata.libs import filters -from onadata.libs.utils.common_tools import str_to_bool -from onadata.libs.mixins.anonymous_user_public_forms_mixin import \ - AnonymousUserPublicFormsMixin -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin +from onadata.libs.mixins.anonymous_user_public_forms_mixin import ( + AnonymousUserPublicFormsMixin, +) +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.libs.renderers.renderers import DecimalJSONRenderer -from onadata.libs.serializers.chart_serializer import (ChartSerializer, - FieldsChartSerializer) -from onadata.libs.utils.chart_tools import get_chart_data_for_field +from onadata.libs.serializers.chart_serializer import ( + ChartSerializer, + FieldsChartSerializer, +) from onadata.libs.utils.cache_tools import XFORM_CHARTS +from onadata.libs.utils.chart_tools import get_chart_data_for_field +from onadata.libs.utils.common_tools import str_to_bool def get_form_field_chart_url(url, field): """Append 'field_name' to a given url""" - return u'%s?field_name=%s' % (url, field) + return f"{url}?field_name={field}" class ChartBrowsableAPIRenderer(BrowsableAPIRenderer): @@ -47,60 +52,70 @@ def get_default_renderer(self, view): (Don't use another documenting renderer.) """ renderers = [ - renderer for renderer in view.renderer_classes + renderer + for renderer in view.renderer_classes if not issubclass(renderer, BrowsableAPIRenderer) ] if not renderers: return None return renderers[0]() - def get_content(self, renderer, data, accepted_media_type, - renderer_context): + def get_content(self, renderer, data, accepted_media_type, renderer_context): try: - content = super(ChartBrowsableAPIRenderer, self).get_content( - renderer, data, accepted_media_type, renderer_context) + content = super().get_content( + renderer, data, accepted_media_type, renderer_context + ) except ImproperlyConfigured: - content = super(ChartBrowsableAPIRenderer, self).get_content( - JSONRenderer(), data, accepted_media_type, renderer_context) + content = super().get_content( + JSONRenderer(), data, accepted_media_type, renderer_context + ) return content -# pylint: disable=R0901 -class ChartsViewSet(AnonymousUserPublicFormsMixin, AuthenticateHeaderMixin, - CacheControlMixin, ETagsMixin, - viewsets.ReadOnlyModelViewSet): +# pylint: disable=too-many-ancestors +class ChartsViewSet( + AnonymousUserPublicFormsMixin, + AuthenticateHeaderMixin, + CacheControlMixin, + ETagsMixin, + viewsets.ReadOnlyModelViewSet, +): """ ChartsViewSet: /charts api endpoint for chart data and chart widgets """ - filter_backends = (filters.AnonDjangoObjectPermissionFilter, ) + filter_backends = (filters.AnonDjangoObjectPermissionFilter,) queryset = XForm.objects.all() serializer_class = ChartSerializer - lookup_field = 'pk' - renderer_classes = (DecimalJSONRenderer, ChartBrowsableAPIRenderer, - TemplateHTMLRenderer, ) + lookup_field = "pk" + renderer_classes = ( + DecimalJSONRenderer, + ChartBrowsableAPIRenderer, + TemplateHTMLRenderer, + ) permission_classes = [ XFormPermissions, ] + # pylint: disable=too-many-locals def retrieve(self, request, *args, **kwargs): - field_name = request.query_params.get('field_name') - field_xpath = request.query_params.get('field_xpath') - fields = request.query_params.get('fields') - group_by = request.query_params.get('group_by') - fmt = kwargs.get('format') - refresh_cache = str_to_bool(request.query_params.get('refresh')) + field_name = request.query_params.get("field_name") + field_xpath = request.query_params.get("field_xpath") + fields = request.query_params.get("fields") + group_by = request.query_params.get("group_by") + fmt = kwargs.get("format") + refresh_cache = str_to_bool(request.query_params.get("refresh")) xform = self.get_object() serializer = self.get_serializer(xform) # Default format to JSON if format is not specified if fmt is None and request.accepted_media_type == "application/json": - fmt = 'json' + fmt = "json" if fields: - if fmt is not None and fmt != 'json': + if fmt is not None and fmt != "json": raise ParseError("Error: only JSON format supported.") xform = self.get_object() @@ -110,21 +125,22 @@ def retrieve(self, request, *args, **kwargs): return Response(serializer.data) if field_name or field_xpath: - cache_key = '{}{}{}{}{}{}'.format(XFORM_CHARTS, xform.pk, - field_xpath, field_name, - group_by, fmt) + cache_key = ( + f"{XFORM_CHARTS}{xform.pk}{field_xpath}{field_name}{group_by}{fmt}" + ) data = cache.get(cache_key) if not data or refresh_cache: - data = get_chart_data_for_field(field_name, xform, fmt, - group_by, field_xpath) + data = get_chart_data_for_field( + field_name, xform, fmt, group_by, field_xpath + ) cache.set(cache_key, data, settings.XFORM_CHARTS_CACHE_TIME) - return Response(data, template_name='chart_detail.html') + return Response(data, template_name="chart_detail.html") - if fmt != 'json' and field_name is None: + if fmt != "json" and field_name is None: raise ParseError("Not supported") data = serializer.data diff --git a/onadata/apps/api/viewsets/connect_viewset.py b/onadata/apps/api/viewsets/connect_viewset.py index a1c0f3817f..549a26fe55 100644 --- a/onadata/apps/api/viewsets/connect_viewset.py +++ b/onadata/apps/api/viewsets/connect_viewset.py @@ -1,113 +1,131 @@ +# -*- coding: utf-8 -*- +""" +The /api/v1/user API implementation + +User authentication API support to access API tokens. +""" from django.core.exceptions import MultipleObjectsReturned from django.utils import timezone from django.utils.decorators import classonlymethod from django.utils.translation import gettext as _ from django.views.decorators.cache import never_cache -from rest_framework import status, viewsets + +from multidb.pinning import use_master +from rest_framework import mixins, status, viewsets from rest_framework.authtoken.models import Token from rest_framework.decorators import action from rest_framework.exceptions import ParseError from rest_framework.response import Response -from rest_framework import mixins -from multidb.pinning import use_master from onadata.apps.api.models.odk_token import ODKToken from onadata.apps.api.models.temp_token import TempToken from onadata.apps.api.permissions import ConnectViewsetPermissions -from onadata.apps.api.viewsets.user_profile_viewset import \ - serializer_from_settings +from onadata.apps.api.viewsets.user_profile_viewset import serializer_from_settings from onadata.apps.main.models.user_profile import UserProfile -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.libs.mixins.object_lookup_mixin import ObjectLookupMixin from onadata.libs.serializers.password_reset_serializer import ( - PasswordResetChangeSerializer, PasswordResetSerializer, get_user_from_uid) + PasswordResetChangeSerializer, + PasswordResetSerializer, + get_user_from_uid, +) from onadata.libs.serializers.project_serializer import ProjectSerializer -from onadata.libs.serializers.user_profile_serializer import \ - UserProfileWithTokenSerializer +from onadata.libs.serializers.user_profile_serializer import ( + UserProfileWithTokenSerializer, +) +from onadata.libs.utils.cache_tools import USER_PROFILE_PREFIX, cache from onadata.settings.common import DEFAULT_SESSION_EXPIRY_TIME -from onadata.libs.utils.cache_tools import (cache, - USER_PROFILE_PREFIX) -def user_profile_w_token_response(request, status): - """ Returns authenticated user profile""" +def user_profile_w_token_response(request, status_code): + """Returns authenticated user profile""" if request and not request.user.is_anonymous: session = getattr(request, "session") if not session.session_key: # login user to create session token - # TODO cannot call this without calling authenticate first or - # setting the backend, commented for now. - # login(request, request.user) session.set_expiry(DEFAULT_SESSION_EXPIRY_TIME) try: user_profile = request.user.profile except UserProfile.DoesNotExist: - user_profile = cache.get( - f'{USER_PROFILE_PREFIX}{request.user.username}') + user_profile = cache.get(f"{USER_PROFILE_PREFIX}{request.user.username}") if not user_profile: with use_master: - user_profile, _ = UserProfile.objects.get_or_create( - user=request.user) + user_profile, _ = UserProfile.objects.get_or_create(user=request.user) serializer = serializer_from_settings()( - user_profile, - context={'request': request}) + user_profile, context={"request": request} + ) cache.set( - f'{USER_PROFILE_PREFIX}{request.user.username}', - serializer.data) + f"{USER_PROFILE_PREFIX}{request.user.username}", serializer.data + ) serializer = UserProfileWithTokenSerializer( - instance=user_profile, context={"request": request}) + instance=user_profile, context={"request": request} + ) - return Response(serializer.data, status=status) + return Response(serializer.data, status=status_code) -class ConnectViewSet(mixins.CreateModelMixin, AuthenticateHeaderMixin, - CacheControlMixin, ETagsMixin, ObjectLookupMixin, - viewsets.GenericViewSet): +# pylint: disable=too-many-ancestors +class ConnectViewSet( + mixins.CreateModelMixin, + AuthenticateHeaderMixin, + CacheControlMixin, + ETagsMixin, + ObjectLookupMixin, + viewsets.GenericViewSet, +): """ This endpoint allows you retrieve the authenticated user's profile info. """ - lookup_field = 'user' + + lookup_field = "user" queryset = UserProfile.objects.all() - permission_classes = (ConnectViewsetPermissions, ) + permission_classes = (ConnectViewsetPermissions,) serializer_class = UserProfileWithTokenSerializer - # pylint: disable=R0201 def create(self, request, *args, **kwargs): return user_profile_w_token_response(request, status.HTTP_201_CREATED) def list(self, request, *args, **kwargs): + """ + Implements the List endpoint - returns authentication tokens for current user. + """ return user_profile_w_token_response(request, status.HTTP_200_OK) - @action(methods=['GET'], detail=True) + @action(methods=["GET"], detail=True) def starred(self, request, *args, **kwargs): """Return projects starred for this user.""" user_profile = self.get_object() user = user_profile.user projects = user.project_stars.all() serializer = ProjectSerializer( - projects, context={'request': request}, many=True) + projects, context={"request": request}, many=True + ) return Response(data=serializer.data) - @action(methods=['POST'], detail=False) + @action(methods=["POST"], detail=False) def reset(self, request, *args, **kwargs): - context = {'request': request} + """ + Implements the /reset endpoint + + Allows a user to reset and change their password. + """ + context = {"request": request} data = request.data if request.data is not None else {} - if 'token' in request.data: - serializer = PasswordResetChangeSerializer( - data=data, context=context) + if "token" in request.data: + serializer = PasswordResetChangeSerializer(data=data, context=context) if serializer.is_valid(): serializer.save() - user = get_user_from_uid(serializer.data['uid']) - return Response(data={'username': user.username}, - status=status.HTTP_200_OK) + user = get_user_from_uid(serializer.data["uid"]) + return Response( + data={"username": user.username}, status=status.HTTP_200_OK + ) else: serializer = PasswordResetSerializer(data=data, context=context) if serializer.is_valid(): @@ -116,63 +134,78 @@ def reset(self, request, *args, **kwargs): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @action(methods=['DELETE'], detail=False) + @action(methods=["DELETE"], detail=False) def expire(self, request, *args, **kwargs): + """ + Implements the /expire endpoint + + Allows a user to expire a TempToken. + """ try: TempToken.objects.get(user=request.user).delete() - except TempToken.DoesNotExist: - raise ParseError(_(u"Temporary token not found!")) + except TempToken.DoesNotExist as exc: + raise ParseError(_("Temporary token not found!")) from exc return Response(status=status.HTTP_204_NO_CONTENT) - @action(methods=['GET'], detail=False) + @action(methods=["GET"], detail=False) def regenerate_auth_token(self, request, *args, **kwargs): + """ + Implements the /regenerate_auth_token endpoint + + Allows a user to expire and create a new API Token. + """ try: Token.objects.get(user=request.user).delete() - except Token.DoesNotExist: - raise ParseError(_(u" Token not found!")) + except Token.DoesNotExist as exc: + raise ParseError(_(" Token not found!")) from exc new_token = Token.objects.create(user=request.user) return Response(data=new_token.key, status=status.HTTP_201_CREATED) - @action(methods=['GET', 'POST'], detail=False) + @action(methods=["GET", "POST"], detail=False) def odk_token(self, request, *args, **kwargs): + """ + Implements the /odk_token endpoint + + Allows a user to get or create or expire an ODKToken for use with ODK Collect. + """ user = request.user + status_code = status.HTTP_200_OK - if request.method == 'GET': + if request.method == "GET": try: token, created = ODKToken.objects.get_or_create( - user=user, status=ODKToken.ACTIVE) + user=user, status=ODKToken.ACTIVE + ) if not created and timezone.now() > token.expires: token.status = ODKToken.INACTIVE token.save() token = ODKToken.objects.create(user=user) except MultipleObjectsReturned: - ODKToken.objects.filter( - user=user, status=ODKToken.ACTIVE).update( - status=ODKToken.INACTIVE) + ODKToken.objects.filter(user=user, status=ODKToken.ACTIVE).update( + status=ODKToken.INACTIVE + ) token = ODKToken.objects.create(user=user) - return Response(data={ - 'odk_token': token.raw_key, - 'expires': token.expires}, status=status.HTTP_200_OK) - elif request.method == 'POST': + if request.method == "POST": # Regenerates the ODK Token if one is already existant - ODKToken.objects.filter( - user=user, status=ODKToken.ACTIVE).update( - status=ODKToken.INACTIVE) + ODKToken.objects.filter(user=user, status=ODKToken.ACTIVE).update( + status=ODKToken.INACTIVE + ) token = ODKToken.objects.create(user=user) + status_code = status.HTTP_201_CREATED - return Response(data={ - 'odk_token': token.raw_key, - 'expires': token.expires - }, status=status.HTTP_201_CREATED) + return Response( + data={"odk_token": token.raw_key, "expires": token.expires}, + status=status_code, + ) @classonlymethod - def as_view(cls, actions=None, **initkwargs): + def as_view(cls, actions=None, **initkwargs): # noqa view = super(ConnectViewSet, cls).as_view(actions, **initkwargs) return never_cache(view) diff --git a/onadata/apps/api/viewsets/data_viewset.py b/onadata/apps/api/viewsets/data_viewset.py index fe7cda9e52..18cadc0a49 100644 --- a/onadata/apps/api/viewsets/data_viewset.py +++ b/onadata/apps/api/viewsets/data_viewset.py @@ -102,6 +102,7 @@ def delete_instance(instance, user): raise ParseError(str(e)) from e +# pylint: disable=http-response-with-content-type-json # pylint: disable=too-many-ancestors class DataViewSet( AnonymousUserPublicFormsMixin, @@ -401,7 +402,7 @@ def destroy(self, request, *args, **kwargs): message_verb=SUBMISSION_DELETED, ) else: - raise PermissionDenied(_("You do not have delete " "permissions.")) + raise PermissionDenied(_("You do not have delete permissions.")) return Response(status=status.HTTP_204_NO_CONTENT) @@ -757,7 +758,6 @@ def get_json_string(item): content_type="application/xml", ) else: - # pylint: disable=http-response-with-content-type-json response = StreamingHttpResponse( json_stream(self.object_list, get_json_string), content_type="application/json", diff --git a/onadata/apps/api/viewsets/export_viewset.py b/onadata/apps/api/viewsets/export_viewset.py index f154eb989f..cb6230c61f 100644 --- a/onadata/apps/api/viewsets/export_viewset.py +++ b/onadata/apps/api/viewsets/export_viewset.py @@ -1,21 +1,35 @@ +# -*- coding: utf-8 -*- +""" +The /api/v1/exports API implementation. + +List, Create, Update, Destroy Export model objects. +""" import os +from rest_framework.mixins import DestroyModelMixin from rest_framework.settings import api_settings from rest_framework.viewsets import ReadOnlyModelViewSet -from rest_framework.mixins import DestroyModelMixin -from onadata.apps.viewer.models.export import Export from onadata.apps.api.permissions import ExportDjangoObjectPermission +from onadata.apps.viewer.models.export import Export +from onadata.libs import filters +from onadata.libs.authentication import TempTokenURLParameterAuthentication from onadata.libs.renderers import renderers from onadata.libs.serializers.export_serializer import ExportSerializer -from onadata.libs.authentication import TempTokenURLParameterAuthentication from onadata.libs.utils.logger_tools import response_with_mimetype_and_name -from onadata.libs import filters +# pylint: disable=too-many-ancestors class ExportViewSet(DestroyModelMixin, ReadOnlyModelViewSet): + """ + The /api/v1/exports API implementation. + + List, Create, Update, Destroy Export model objects. + """ + authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES + [ - TempTokenURLParameterAuthentication] + TempTokenURLParameterAuthentication + ] queryset = Export.objects.all() renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [ renderers.CSVRenderer, @@ -25,7 +39,7 @@ class ExportViewSet(DestroyModelMixin, ReadOnlyModelViewSet): renderers.SAVZIPRenderer, renderers.XLSRenderer, renderers.XLSXRenderer, - renderers.ZipRenderer + renderers.ZipRenderer, ] serializer_class = ExportSerializer filter_backends = (filters.ExportFilter,) @@ -41,4 +55,5 @@ def retrieve(self, request, *args, **kwargs): filename, extension=extension, file_path=export.filepath, - show_date=False) + show_date=False, + ) diff --git a/onadata/apps/api/viewsets/floip_viewset.py b/onadata/apps/api/viewsets/floip_viewset.py index 6a989b3c15..b00948f010 100644 --- a/onadata/apps/api/viewsets/floip_viewset.py +++ b/onadata/apps/api/viewsets/floip_viewset.py @@ -6,6 +6,7 @@ from django.db.models import Q from django.shortcuts import get_object_or_404 + from rest_framework import mixins, status, viewsets from rest_framework.decorators import action from rest_framework.response import Response @@ -15,11 +16,14 @@ from rest_framework_json_api.renderers import JSONRenderer from onadata.apps.api.permissions import XFormPermissions -from onadata.apps.logger.models import XForm, Instance +from onadata.apps.logger.models import Instance, XForm from onadata.libs import filters from onadata.libs.renderers.renderers import floip_list from onadata.libs.serializers.floip_serializer import ( - FloipListSerializer, FloipSerializer, FlowResultsResponseSerializer) + FloipListSerializer, + FloipSerializer, + FlowResultsResponseSerializer, +) class FlowResultsJSONRenderer(JSONRenderer): @@ -30,39 +34,57 @@ class FlowResultsJSONRenderer(JSONRenderer): # pylint: disable=too-many-arguments @classmethod def build_json_resource_obj( - cls, fields, resource, resource_instance, - resource_name, serializer, force_type_resolution=False): + cls, + fields, + resource, + resource_instance, + resource_name, + serializer, + force_type_resolution=False, + ): """ Build a JSON resource object using the id as it appears in the resource. """ - obj = super(FlowResultsJSONRenderer, cls).build_json_resource_obj( - fields, resource, resource_instance, resource_name, - serializer, force_type_resolution) - obj['id'] = resource['id'] + obj = super().build_json_resource_obj( + fields, + resource, + resource_instance, + resource_name, + serializer, + force_type_resolution, + ) + obj["id"] = resource["id"] return obj # pylint: disable=too-many-ancestors -class FloipViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, - mixins.ListModelMixin, mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, viewsets.GenericViewSet): +class FloipViewSet( + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet, +): """ FloipViewSet: create, list, retrieve, destroy """ - filter_backends = (filters.AnonDjangoObjectPermissionFilter, - filters.PublicDatasetsFilter) + filter_backends = ( + filters.AnonDjangoObjectPermissionFilter, + filters.PublicDatasetsFilter, + ) permission_classes = [XFormPermissions] queryset = XForm.objects.filter(deleted_at__isnull=True) serializer_class = FloipSerializer pagination_class = PageNumberPagination - parser_classes = (JSONParser, ) - renderer_classes = (FlowResultsJSONRenderer, ) + parser_classes = (JSONParser,) + renderer_classes = (FlowResultsJSONRenderer,) - lookup_field = 'uuid' + lookup_field = "uuid" def get_object(self): queryset = self.filter_queryset(self.get_queryset()) @@ -76,24 +98,25 @@ def get_object(self): return obj def get_serializer_class(self): - if self.action == 'list': + if self.action == "list": return FloipListSerializer - if self.action == 'responses': + if self.action == "responses": return FlowResultsResponseSerializer - return super(FloipViewSet, self).get_serializer_class() + return super().get_serializer_class() def get_success_headers(self, data): - headers = super(FloipViewSet, self).get_success_headers(data) - headers['Content-Type'] = 'application/vnd.api+json' - uuid = str(UUID(data['id'])) - headers['Location'] = self.request.build_absolute_uri( - reverse('flow-results-detail', kwargs={'uuid': uuid})) + headers = super().get_success_headers(data) + headers["Content-Type"] = "application/vnd.api+json" + uuid = str(UUID(data["id"])) + headers["Location"] = self.request.build_absolute_uri( + reverse("flow-results-detail", kwargs={"uuid": uuid}) + ) return headers - @action(methods=['GET', 'POST'], detail=True) + @action(methods=["GET", "POST"], detail=True) def responses(self, request, uuid=None): """ Flow Results Responses endpoint. @@ -101,23 +124,21 @@ def responses(self, request, uuid=None): status_code = status.HTTP_200_OK xform = self.get_object() uuid = str(UUID(uuid or xform.uuid, version=4)) - data = { - "id": uuid, - "type": "flow-results-data", - "attributes": {} - } + data = {"id": uuid, "type": "flow-results-data", "attributes": {}} headers = { - 'Content-Type': 'application/vnd.api+json', - 'Location': self.request.build_absolute_uri( - reverse('flow-results-responses', kwargs={'uuid': uuid})) + "Content-Type": "application/vnd.api+json", + "Location": self.request.build_absolute_uri( + reverse("flow-results-responses", kwargs={"uuid": uuid}) + ), } # yapf: disable - if request.method == 'POST': + if request.method == "POST": serializer = FlowResultsResponseSerializer( - data=request.data, context={'request': request}) + data=request.data, context={"request": request} + ) serializer.is_valid(raise_exception=True) serializer.save() - data['response'] = serializer.data['responses'] - if serializer.data['duplicates']: + data["response"] = serializer.data["responses"] + if serializer.data["duplicates"]: status_code = status.HTTP_202_ACCEPTED else: status_code = status.HTTP_201_CREATED @@ -125,22 +146,22 @@ def responses(self, request, uuid=None): if xform.is_merged_dataset: pks = xform.mergedxform.xforms.filter( deleted_at__isnull=True - ).values_list('pk', flat=True) + ).values_list("pk", flat=True) queryset = Instance.objects.filter( - xform_id__in=pks, - deleted_at__isnull=True).values_list('json', flat=True) + xform_id__in=pks, deleted_at__isnull=True + ).values_list("json", flat=True) else: - queryset = xform.instances.values_list('json', flat=True) + queryset = xform.instances.values_list("json", flat=True) paginate_queryset = self.paginate_queryset(queryset) if paginate_queryset: - data['attributes']['responses'] = floip_list(paginate_queryset) + data["attributes"]["responses"] = floip_list(paginate_queryset) response = self.get_paginated_response(data) for key, value in headers.items(): response[key] = value return response - data['attributes']['responses'] = floip_list(queryset) + data["attributes"]["responses"] = floip_list(queryset) return Response(data, headers=headers, status=status_code) diff --git a/onadata/apps/api/viewsets/media_viewset.py b/onadata/apps/api/viewsets/media_viewset.py index a902b37e04..422296c88f 100644 --- a/onadata/apps/api/viewsets/media_viewset.py +++ b/onadata/apps/api/viewsets/media_viewset.py @@ -1,3 +1,9 @@ +# -*- coding: utf-8 -*- +""" +The /api/v1/media API implementation. + +List, Create, Update, Delete MetaData objects. +""" from django.conf import settings from django.http import Http404 from django.http import HttpResponseRedirect @@ -9,23 +15,28 @@ from rest_framework.exceptions import ParseError from onadata.apps.logger.models import Attachment -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin -from onadata.libs.utils.image_tools import \ - image_url, generate_media_download_url +from onadata.libs.utils.image_tools import image_url, generate_media_download_url from onadata.apps.api.tools import get_baseviewset_class BaseViewset = get_baseviewset_class() -class MediaViewSet(AuthenticateHeaderMixin, - CacheControlMixin, ETagsMixin, BaseViewset, - viewsets.ViewSet): +# pylint: disable=too-many-ancestors +class MediaViewSet( + AuthenticateHeaderMixin, + CacheControlMixin, + ETagsMixin, + BaseViewset, + viewsets.ViewSet, +): """A view to redirect to actual attachments url""" - permission_classes = (AllowAny, ) + permission_classes = (AllowAny,) + + # pylint: disable=invalid-name def retrieve(self, request, pk=None): """ Redirect to final attachment url @@ -40,10 +51,10 @@ def retrieve(self, request, pk=None): """ try: int(pk) - except ValueError: - raise Http404() + except ValueError as exc: + raise Http404() from exc else: - filename = request.query_params.get('filename') + filename = request.query_params.get("filename") attachments = Attachment.objects.all() obj = get_object_or_404(attachments, pk=pk) @@ -52,15 +63,15 @@ def retrieve(self, request, pk=None): url = None - if obj.mimetype.startswith('image'): - suffix = request.query_params.get('suffix') + if obj.mimetype.startswith("image"): + suffix = request.query_params.get("suffix") if suffix: if suffix in list(settings.THUMB_CONF): try: url = image_url(obj, suffix) except Exception as e: - raise ParseError(e) + raise ParseError(e) from e else: raise Http404() @@ -75,7 +86,7 @@ def retrieve(self, request, pk=None): def list(self, request, *args, **kwargs): """ - Action NOT IMPLEMENTED, only needed because of the automatic url - routing in /api/v1/ + Action NOT IMPLEMENTED, only needed because of the automatic url + routing in /api/v1/ """ return Response(data=[]) diff --git a/onadata/apps/api/viewsets/metadata_viewset.py b/onadata/apps/api/viewsets/metadata_viewset.py index 0ce33444ec..7e1299ba3b 100644 --- a/onadata/apps/api/viewsets/metadata_viewset.py +++ b/onadata/apps/api/viewsets/metadata_viewset.py @@ -1,3 +1,9 @@ +# -*- coding: utf-8 -*- +""" +The /api/v1/metadata API implementation. + +List, Create, Update, Delete MetaData objects. +""" from rest_framework import renderers from rest_framework import viewsets from rest_framework.response import Response @@ -7,42 +13,49 @@ from onadata.apps.main.models.meta_data import MetaData from onadata.libs.serializers.metadata_serializer import MetaDataSerializer from onadata.libs import filters -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin -from onadata.libs.renderers.renderers import MediaFileContentNegotiation, \ - MediaFileRenderer +from onadata.libs.renderers.renderers import ( + MediaFileContentNegotiation, + MediaFileRenderer, +) from onadata.apps.api.tools import get_baseviewset_class BaseViewset = get_baseviewset_class() -class MetaDataViewSet(AuthenticateHeaderMixin, - CacheControlMixin, - ETagsMixin, - BaseViewset, - viewsets.ModelViewSet): +# pylint: disable=too-many-ancestors +class MetaDataViewSet( + AuthenticateHeaderMixin, + CacheControlMixin, + ETagsMixin, + BaseViewset, + viewsets.ModelViewSet, +): """ - This endpoint provides access to form metadata. + List, Create, Update, Delete MetaData objects. """ content_negotiation_class = MediaFileContentNegotiation filter_backends = (filters.MetaDataFilter,) - queryset = MetaData.objects.filter( - deleted_at__isnull=True).select_related() + queryset = MetaData.objects.filter(deleted_at__isnull=True).select_related() permission_classes = (MetaDataObjectPermissions,) renderer_classes = ( renderers.JSONRenderer, renderers.BrowsableAPIRenderer, - MediaFileRenderer) + MediaFileRenderer, + ) serializer_class = MetaDataSerializer def retrieve(self, request, *args, **kwargs): + # pylint: disable=attribute-defined-outside-init self.object = self.get_object() - if isinstance(request.accepted_renderer, MediaFileRenderer) \ - and self.object.data_file is not None: + if ( + isinstance(request.accepted_renderer, MediaFileRenderer) + and self.object.data_file is not None + ): return get_media_file_response(self.object, request) diff --git a/onadata/apps/api/viewsets/note_viewset.py b/onadata/apps/api/viewsets/note_viewset.py index cf5d210ec7..eaf1f26de8 100644 --- a/onadata/apps/api/viewsets/note_viewset.py +++ b/onadata/apps/api/viewsets/note_viewset.py @@ -1,39 +1,49 @@ +# -*- coding: utf-8 -*- +""" +The /api/v1/notes API implementation. + +List, Create, Update, Delete Note objects. +""" from rest_framework import status -from rest_framework_guardian.filters import ObjectPermissionsFilter from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet +from rest_framework_guardian.filters import ObjectPermissionsFilter from onadata.apps.api import permissions -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin - +from onadata.apps.api.tools import get_baseviewset_class +from onadata.apps.logger.models import Note from onadata.libs import filters +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.libs.serializers.note_serializer import NoteSerializer -from onadata.apps.logger.models import Note -from onadata.apps.api.tools import get_baseviewset_class BaseViewset = get_baseviewset_class() -class NoteViewSet(AuthenticateHeaderMixin, - CacheControlMixin, - ETagsMixin, - BaseViewset, - ModelViewSet): +# pylint: disable=too-many-ancestors +class NoteViewSet( + AuthenticateHeaderMixin, CacheControlMixin, ETagsMixin, BaseViewset, ModelViewSet +): + """ + The /api/v1/notes API implementation. + + List, Create, Update, Delete Note objects.""" + queryset = Note.objects.all() filter_backends = (filters.NoteFilter, ObjectPermissionsFilter) serializer_class = NoteSerializer - permission_classes = [permissions.ViewDjangoObjectPermissions, - permissions.IsAuthenticated, ] + permission_classes = [ + permissions.ViewDjangoObjectPermissions, + permissions.IsAuthenticated, + ] def get_object(self): obj = [] queryset = self.filter_queryset(self.get_queryset()) if queryset: - obj = super(NoteViewSet, self).get_object() + obj = super().get_object() return obj @@ -43,8 +53,7 @@ def retrieve(self, request, *args, **kwargs): if instance: serializer = self.get_serializer(instance) return Response(serializer.data) - else: - return Response(data=[]) + return Response(data=[]) def destroy(self, request, *args, **kwargs): obj = self.get_object() diff --git a/onadata/apps/api/viewsets/open_data_viewset.py b/onadata/apps/api/viewsets/open_data_viewset.py index b634d50909..aec1a4a8c2 100644 --- a/onadata/apps/api/viewsets/open_data_viewset.py +++ b/onadata/apps/api/viewsets/open_data_viewset.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ The /api/v1/open-data implementation. """ @@ -67,7 +67,7 @@ def process_tableau_data(data, xform): # noqa C901 def get_xpath(key, nested_key, index): val = nested_key.split("/") - start_index = key.split("/").__len__() + start_index = len(key.split("/")) nested_key_diff = val[start_index:] xpaths = key + f"[{index}]/" + "/".join(nested_key_diff) return xpaths @@ -176,7 +176,6 @@ class OpenDataViewSet(ETagsMixin, CacheControlMixin, BaseViewset, ModelViewSet): MAX_INSTANCES_PER_REQUEST = 1000 pagination_class = StandardPageNumberPagination - # pylint: disable=no-self-use def get_tableau_type(self, xform_type): """ Returns a tableau-supported type based on a xform type. @@ -290,7 +289,6 @@ def data(self, request, **kwargs): return Response(data) - # pylint: disable=no-self-use def get_streaming_response(self, data): """Get a StreamingHttpResponse response object""" @@ -340,7 +338,6 @@ def schema(self, request, **kwargs): return Response(status=status.HTTP_404_NOT_FOUND) - # pylint: disable=no-self-use @action(methods=["GET"], detail=False) def uuid(self, request, *args, **kwargs): """Respond with the OpenData uuid.""" diff --git a/onadata/apps/api/viewsets/organization_profile_viewset.py b/onadata/apps/api/viewsets/organization_profile_viewset.py index 129d674bbd..28edf3d094 100644 --- a/onadata/apps/api/viewsets/organization_profile_viewset.py +++ b/onadata/apps/api/viewsets/organization_profile_viewset.py @@ -1,127 +1,134 @@ +# -*- coding: utf-8 -*- +""" +The /api/v1/orgs API implementation + +List, Retrieve, Update, Create/Register Organizations. +""" import json + from django.conf import settings -from django.utils.module_loading import import_string from django.core.cache import cache +from django.utils.module_loading import import_string from rest_framework import status -from rest_framework.viewsets import ModelViewSet from rest_framework.decorators import action from rest_framework.response import Response - -from onadata.apps.api.models.organization_profile import OrganizationProfile +from rest_framework.viewsets import ModelViewSet from onadata.apps.api import permissions +from onadata.apps.api.models.organization_profile import OrganizationProfile from onadata.apps.api.tools import get_baseviewset_class -from onadata.libs.utils.common_tools import merge_dicts -from onadata.libs.filters import (OrganizationPermissionFilter, - OrganizationsSharedWithUserFilter) -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin +from onadata.libs.filters import ( + OrganizationPermissionFilter, + OrganizationsSharedWithUserFilter, +) +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.libs.mixins.object_lookup_mixin import ObjectLookupMixin -from onadata.libs.serializers.organization_member_serializer import \ - OrganizationMemberSerializer -from onadata.libs.serializers.organization_serializer import ( - OrganizationSerializer) -from onadata.libs.utils.cache_tools import ( - safe_delete, - ORG_PROFILE_CACHE) - +from onadata.libs.serializers.organization_member_serializer import ( + OrganizationMemberSerializer, +) +from onadata.libs.serializers.organization_serializer import OrganizationSerializer +from onadata.libs.utils.cache_tools import ORG_PROFILE_CACHE, safe_delete +from onadata.libs.utils.common_tools import merge_dicts BaseViewset = get_baseviewset_class() def serializer_from_settings(): + """Return the OrganizationSerializer either from settings or the default.""" if settings.ORG_PROFILE_SERIALIZER: return import_string(settings.ORG_PROFILE_SERIALIZER) return OrganizationSerializer -class OrganizationProfileViewSet(AuthenticateHeaderMixin, - CacheControlMixin, - ETagsMixin, - ObjectLookupMixin, - BaseViewset, - ModelViewSet): +# pylint: disable=too-many-ancestors +class OrganizationProfileViewSet( + AuthenticateHeaderMixin, + CacheControlMixin, + ETagsMixin, + ObjectLookupMixin, + BaseViewset, + ModelViewSet, +): """ List, Retrieve, Update, Create/Register Organizations. """ + queryset = OrganizationProfile.objects.filter(user__is_active=True) serializer_class = serializer_from_settings() - lookup_field = 'user' + lookup_field = "user" permission_classes = [permissions.OrganizationProfilePermissions] - filter_backends = (OrganizationPermissionFilter, - OrganizationsSharedWithUserFilter) + filter_backends = (OrganizationPermissionFilter, OrganizationsSharedWithUserFilter) def retrieve(self, request, *args, **kwargs): - """ Get organization from cache or db """ - username = kwargs.get('user') - cached_org = cache.get(f'{ORG_PROFILE_CACHE}{username}') + """Get organization from cache or db""" + username = kwargs.get("user") + cached_org = cache.get(f"{ORG_PROFILE_CACHE}{username}") if cached_org: return Response(cached_org) - response = super(OrganizationProfileViewSet, self)\ - .retrieve(request, *args, **kwargs) - cache.set(f'{ORG_PROFILE_CACHE}{username}', response.data) + response = super().retrieve(request, *args, **kwargs) + cache.set(f"{ORG_PROFILE_CACHE}{username}", response.data) return response def create(self, request, *args, **kwargs): - """ Create and cache organization """ - response = super(OrganizationProfileViewSet, self)\ - .create(request, *args, **kwargs) + """Create and cache organization""" + response = super().create(request, *args, **kwargs) organization = response.data - username = organization.get('org') - cache.set(f'{ORG_PROFILE_CACHE}{username}', organization) + username = organization.get("org") + cache.set(f"{ORG_PROFILE_CACHE}{username}", organization) return response def destroy(self, request, *args, **kwargs): - """ Clear cache and destroy organization """ - username = kwargs.get('user') - safe_delete(f'{ORG_PROFILE_CACHE}{username}') - return super(OrganizationProfileViewSet, self)\ - .destroy(request, *args, **kwargs) + """Clear cache and destroy organization""" + username = kwargs.get("user") + safe_delete(f"{ORG_PROFILE_CACHE}{username}") + return super().destroy(request, *args, **kwargs) def update(self, request, *args, **kwargs): - """ Update org in cache and db""" - username = kwargs.get('user') - response = super(OrganizationProfileViewSet, self)\ - .update(request, *args, **kwargs) - cache.set(f'{ORG_PROFILE_CACHE}{username}', response.data) + """Update org in cache and db""" + username = kwargs.get("user") + response = super().update(request, *args, **kwargs) + cache.set(f"{ORG_PROFILE_CACHE}{username}", response.data) return response - @action(methods=['DELETE', 'GET', 'POST', 'PUT'], detail=True) + @action(methods=["DELETE", "GET", "POST", "PUT"], detail=True) def members(self, request, *args, **kwargs): + """Return organization members.""" organization = self.get_object() - data = merge_dicts(request.data, - request.query_params.dict(), - {'organization': organization.pk}) + data = merge_dicts( + request.data, request.query_params.dict(), {"organization": organization.pk} + ) - if request.method == 'POST' and 'username' not in data: - data['username'] = None + if request.method == "POST" and "username" not in data: + data["username"] = None - if request.method == 'DELETE': - data['remove'] = True + if request.method == "DELETE": + data["remove"] = True - if request.method == 'PUT' and 'role' not in data: - data['role'] = None + if request.method == "PUT" and "role" not in data: + data["role"] = None serializer = OrganizationMemberSerializer(data=data) - username = kwargs.get('user') + username = kwargs.get("user") if serializer.is_valid(): serializer.save() - organization = serializer.validated_data.get('organization') - data = OrganizationSerializer(organization, - context={'request': request}).data - cache.set(f'{ORG_PROFILE_CACHE}{username}', data) + organization = serializer.validated_data.get("organization") + data = OrganizationSerializer( + organization, context={"request": request} + ).data + cache.set(f"{ORG_PROFILE_CACHE}{username}", data) else: - return Response(data=serializer.errors, - status=status.HTTP_400_BAD_REQUEST) + return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) + # pylint: disable=attribute-defined-outside-init self.etag_data = json.dumps(data) - resp_status = status.HTTP_201_CREATED if request.method == 'POST' \ - else status.HTTP_200_OK + resp_status = ( + status.HTTP_201_CREATED if request.method == "POST" else status.HTTP_200_OK + ) - return Response(status=resp_status, data=serializer.data()) + return Response(status=resp_status, data=serializer.data) diff --git a/onadata/apps/api/viewsets/project_viewset.py b/onadata/apps/api/viewsets/project_viewset.py index 3c0da0d1c0..872fea5579 100644 --- a/onadata/apps/api/viewsets/project_viewset.py +++ b/onadata/apps/api/viewsets/project_viewset.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ The /projects API endpoint implementation. """ diff --git a/onadata/apps/api/viewsets/stats_viewset.py b/onadata/apps/api/viewsets/stats_viewset.py index aad56aaf1d..bf203ffa4a 100644 --- a/onadata/apps/api/viewsets/stats_viewset.py +++ b/onadata/apps/api/viewsets/stats_viewset.py @@ -1,41 +1,51 @@ +# -*- coding: utf-8 -*- +""" +The /api/v1/stats API endpoint implementaion. +""" from rest_framework import viewsets from onadata.apps.api.permissions import XFormPermissions +from onadata.apps.api.tools import get_baseviewset_class from onadata.apps.logger.models.xform import XForm - from onadata.libs import filters from onadata.libs.mixins.anonymous_user_public_forms_mixin import ( - AnonymousUserPublicFormsMixin) -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin + AnonymousUserPublicFormsMixin, +) +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.libs.serializers.stats_serializer import ( - StatsSerializer, StatsInstanceSerializer) -from onadata.apps.api.tools import get_baseviewset_class + StatsInstanceSerializer, + StatsSerializer, +) BaseViewset = get_baseviewset_class() -class StatsViewSet(AuthenticateHeaderMixin, - CacheControlMixin, - ETagsMixin, - AnonymousUserPublicFormsMixin, - BaseViewset, - viewsets.ReadOnlyModelViewSet): - - lookup_field = 'pk' +# pylint: disable=too-many-ancestors +class StatsViewSet( + AuthenticateHeaderMixin, + CacheControlMixin, + ETagsMixin, + AnonymousUserPublicFormsMixin, + BaseViewset, + viewsets.ReadOnlyModelViewSet, +): + """ + The /api/v1/stats API endpoint implementaion. + """ + + lookup_field = "pk" queryset = XForm.objects.all() - filter_backends = (filters.AnonDjangoObjectPermissionFilter, ) - permission_classes = [XFormPermissions, ] + filter_backends = (filters.AnonDjangoObjectPermissionFilter,) + permission_classes = [ + XFormPermissions, + ] serializer_class = StatsSerializer def get_serializer_class(self): lookup = self.kwargs.get(self.lookup_field) if lookup is not None: - serializer_class = StatsInstanceSerializer - else: - serializer_class = \ - super(StatsViewSet, self).get_serializer_class() + return StatsInstanceSerializer - return serializer_class + return super().get_serializer_class() diff --git a/onadata/apps/api/viewsets/submissionstats_viewset.py b/onadata/apps/api/viewsets/submissionstats_viewset.py index 2b80e28fc9..6959a3d139 100644 --- a/onadata/apps/api/viewsets/submissionstats_viewset.py +++ b/onadata/apps/api/viewsets/submissionstats_viewset.py @@ -1,72 +1,83 @@ +# -*- coding: utf-8 -*- +""" +The /api/v1/stats/submissions API endpoint implementation. +""" from rest_framework import viewsets from onadata.apps.api.permissions import XFormPermissions from onadata.apps.logger.models.xform import XForm from onadata.libs import filters from onadata.libs.mixins.anonymous_user_public_forms_mixin import ( - AnonymousUserPublicFormsMixin) -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin + AnonymousUserPublicFormsMixin, +) +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.libs.serializers.stats_serializer import ( - SubmissionStatsSerializer, SubmissionStatsInstanceSerializer) + SubmissionStatsSerializer, + SubmissionStatsInstanceSerializer, +) from onadata.apps.api.tools import get_baseviewset_class BaseViewset = get_baseviewset_class() -class SubmissionStatsViewSet(AnonymousUserPublicFormsMixin, - AuthenticateHeaderMixin, - CacheControlMixin, - ETagsMixin, - BaseViewset, - viewsets.ReadOnlyModelViewSet): +# pylint: disable=too-many-ancestors +class SubmissionStatsViewSet( + AnonymousUserPublicFormsMixin, + AuthenticateHeaderMixin, + CacheControlMixin, + ETagsMixin, + BaseViewset, + viewsets.ReadOnlyModelViewSet, +): """ -Provides submissions counts grouped by a specified field. -It accepts query parameters `group` and `name`. Default result -is grouped by `_submission_time`, hence you get submission counts per day. -If a date field is used as the group, the result will be grouped by day. + Provides submissions counts grouped by a specified field. + It accepts query parameters `group` and `name`. Default result + is grouped by `_submission_time`, hence you get submission counts per day. + If a date field is used as the group, the result will be grouped by day. -* *group* - field to group submission counts by -* *name* - name to be applied to the group on results + * *group* - field to group submission counts by + * *name* - name to be applied to the group on results -Example: + Example: - GET /api/v1/stats/submissions/1? - group=_submission_time&name=day_of_submission + GET /api/v1/stats/submissions/1? + group=_submission_time&name=day_of_submission -Response:: + Response:: - [ - { - "count": 8, - "day_of_submission": "2013-11-15", - }, - { - "count": 99, - "day_of_submission": "2013-11-16", - }, - { - "count": 133, - "day_of_submission": "2013-11-17", - }, - { - "count": 162, - "day_of_submission": "2013-11-18", - }, - { - "count": 102, - "day_of_submission": "2013-11-19", - } - ] + [ + { + "count": 8, + "day_of_submission": "2013-11-15", + }, + { + "count": 99, + "day_of_submission": "2013-11-16", + }, + { + "count": 133, + "day_of_submission": "2013-11-17", + }, + { + "count": 162, + "day_of_submission": "2013-11-18", + }, + { + "count": 102, + "day_of_submission": "2013-11-19", + } + ] + """ -""" - lookup_field = 'pk' + lookup_field = "pk" queryset = XForm.objects.all() - filter_backends = (filters.AnonDjangoObjectPermissionFilter, ) - permission_classes = [XFormPermissions, ] + filter_backends = (filters.AnonDjangoObjectPermissionFilter,) + permission_classes = [ + XFormPermissions, + ] serializer_class = SubmissionStatsSerializer def get_serializer_class(self): @@ -74,7 +85,6 @@ def get_serializer_class(self): if lookup is not None: serializer_class = SubmissionStatsInstanceSerializer else: - serializer_class = \ - super(SubmissionStatsViewSet, self).get_serializer_class() + serializer_class = super().get_serializer_class() return serializer_class diff --git a/onadata/apps/api/viewsets/team_viewset.py b/onadata/apps/api/viewsets/team_viewset.py index 2127746cb6..1b59309b31 100644 --- a/onadata/apps/api/viewsets/team_viewset.py +++ b/onadata/apps/api/viewsets/team_viewset.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ The /teams API endpoint implementation. """ diff --git a/onadata/apps/api/viewsets/user_profile_viewset.py b/onadata/apps/api/viewsets/user_profile_viewset.py index f02ffa851a..846b810a81 100644 --- a/onadata/apps/api/viewsets/user_profile_viewset.py +++ b/onadata/apps/api/viewsets/user_profile_viewset.py @@ -15,8 +15,6 @@ from django.utils.module_loading import import_string from django.utils.translation import gettext as _ -from six.moves.urllib.parse import urlencode - from multidb.pinning import use_master from registration.models import RegistrationProfile from rest_framework import serializers, status @@ -26,6 +24,7 @@ from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet +from six.moves.urllib.parse import urlencode from onadata.apps.api.permissions import UserProfilePermissions from onadata.apps.api.tasks import send_verification_email @@ -328,7 +327,6 @@ def monthly_submissions(self, request, *args, **kwargs): serializer = MonthlySubmissionsSerializer(instance_count, many=True) return Response(serializer.data[0]) - # pylint: disable=no-self-use @action(detail=False) def verify_email(self, request, *args, **kwargs): """Accpet's email verification token and marks the profile as verified.""" @@ -378,7 +376,6 @@ def verify_email(self, request, *args, **kwargs): return HttpResponseBadRequest(response_message) - # pylint: disable=no-self-use @action(methods=["POST"], detail=False) def send_verification_email(self, request, *args, **kwargs): """Sends verification email on user profile registration.""" @@ -398,12 +395,13 @@ def send_verification_email(self, request, *args, **kwargs): except RegistrationProfile.DoesNotExist: pass else: - set_is_email_verified(registration_profile.user.profile, False) + user = registration_profile.user + set_is_email_verified(user.profile, False) verification_key = registration_profile.activation_key if verification_key == verified_key_text: verification_key = ( - registration_profile.user.registrationprofile.create_new_activation_key() + user.registrationprofile.create_new_activation_key() ) verification_url = get_verification_url( @@ -411,8 +409,8 @@ def send_verification_email(self, request, *args, **kwargs): ) email_data = get_verification_email_data( - registration_profile.user.email, - registration_profile.user.username, + user.email, + user.username, verification_url, request, ) diff --git a/onadata/apps/api/viewsets/user_viewset.py b/onadata/apps/api/viewsets/user_viewset.py index e7e28b1a85..33add40dae 100644 --- a/onadata/apps/api/viewsets/user_viewset.py +++ b/onadata/apps/api/viewsets/user_viewset.py @@ -1,16 +1,15 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Users /users API endpoint. """ from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from rest_framework.generics import get_object_or_404 from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework import filters from onadata.libs.filters import UserNoOrganizationsFilter -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.libs.serializers.user_serializer import UserSerializer @@ -18,21 +17,32 @@ from onadata.apps.api.tools import get_baseviewset_class BaseViewset = get_baseviewset_class() # pylint: disable=invalid-name +User = get_user_model() # pylint: disable=too-many-ancestors -class UserViewSet(AuthenticateHeaderMixin, BaseViewset, CacheControlMixin, - ETagsMixin, ReadOnlyModelViewSet): +class UserViewSet( + AuthenticateHeaderMixin, + BaseViewset, + CacheControlMixin, + ETagsMixin, + ReadOnlyModelViewSet, +): """ This endpoint allows you to list and retrieve user's first and last names. """ + queryset = User.objects.filter(is_active=True).exclude( - username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME, ) + username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME, + ) serializer_class = UserSerializer - lookup_field = 'username' + lookup_field = "username" permission_classes = [permissions.UserViewSetPermissions] - filter_backends = (filters.SearchFilter, UserNoOrganizationsFilter, ) - search_fields = ('=email', ) + filter_backends = ( + filters.SearchFilter, + UserNoOrganizationsFilter, + ) + search_fields = ("=email",) def get_object(self): """Lookup a username by pk else use lookup_field""" @@ -44,9 +54,9 @@ def get_object(self): try: user_id = int(username) except ValueError: - filter_kwargs = {'%s__iexact' % self.lookup_field: username} + filter_kwargs = {f"{self.lookup_field}__iexact": username} else: - filter_kwargs = {'pk': user_id} + filter_kwargs = {"pk": user_id} user = get_object_or_404(queryset, **filter_kwargs) diff --git a/onadata/apps/api/viewsets/v2/tableau_viewset.py b/onadata/apps/api/viewsets/v2/tableau_viewset.py index 158d0ce055..80de83a8c5 100644 --- a/onadata/apps/api/viewsets/v2/tableau_viewset.py +++ b/onadata/apps/api/viewsets/v2/tableau_viewset.py @@ -3,33 +3,33 @@ Implements the /api/v2/tableau endpoint """ import re +from collections import defaultdict from typing import List from rest_framework import status -from collections import defaultdict from rest_framework.decorators import action from rest_framework.response import Response -from onadata.libs.data import parse_int -from onadata.libs.renderers.renderers import pairing -from onadata.apps.logger.models import Instance -from onadata.apps.logger.models.xform import XForm from onadata.apps.api.tools import replace_attachment_name_with_url from onadata.apps.api.viewsets.open_data_viewset import OpenDataViewSet +from onadata.apps.logger.models import Instance +from onadata.apps.logger.models.xform import XForm +from onadata.libs.data import parse_int +from onadata.libs.renderers.renderers import pairing from onadata.libs.serializers.data_serializer import TableauDataSerializer from onadata.libs.utils.common_tags import ( ID, MULTIPLE_SELECT_TYPE, - REPEAT_SELECT_TYPE, - PARENT_TABLE, PARENT_ID, + PARENT_TABLE, + REPEAT_SELECT_TYPE, ) - DEFAULT_TABLE_NAME = "data" GPS_PARTS = ["latitude", "longitude", "altitude", "precision"] +# pylint: disable=too-many-locals def process_tableau_data( data, xform, @@ -37,7 +37,9 @@ def process_tableau_data( parent_id: int = None, current_table: str = DEFAULT_TABLE_NAME, ): + """Returns data formatted for Tableau.""" result = [] + # pylint: disable=too-many-nested-blocks if data: for idx, row in enumerate(data, start=1): flat_dict = defaultdict(list) @@ -96,6 +98,7 @@ def process_tableau_data( def unpack_select_multiple_data(picked_choices, list_name, choice_names, prefix): + """Unpacks select multiple data and returns a dictionary of selected choices.""" unpacked_data = {} for choice in choice_names: qstn_name = f"{list_name}_{choice}" @@ -111,6 +114,7 @@ def unpack_select_multiple_data(picked_choices, list_name, choice_names, prefix) def unpack_repeat_data(repeat_data, flat_dict): + """Prepares repeat data.""" # Pop any list within the returned repeat data. # Lists represent a repeat group which should be in a # separate field. @@ -128,6 +132,7 @@ def unpack_repeat_data(repeat_data, flat_dict): def unpack_gps_data(value, qstn_name, prefix): + """Prepares GPS data.""" value_parts = value.split(" ") gps_xpath_parts = [] for part in GPS_PARTS: @@ -138,9 +143,11 @@ def unpack_gps_data(value, qstn_name, prefix): if len(value_parts) == 4: gps_parts = dict(zip(dict(gps_xpath_parts), value_parts)) return gps_parts + return {} def clean_xform_headers(headers: list) -> list: + """Prepare valid headers for Tableau.""" ret = [] for header in headers: if re.search(r"\[+\d+\]", header): @@ -154,9 +161,15 @@ def clean_xform_headers(headers: list) -> list: return ret +# pylint: disable=too-many-ancestors class TableauViewSet(OpenDataViewSet): + """ + TableauViewSet - the /api/v2/tableau API endpoin implementation. + """ + @action(methods=["GET"], detail=True) def data(self, request, **kwargs): + # pylint: disable=attribute-defined-outside-init self.object = self.get_object() # get greater than value and cast it to an int gt_id = request.query_params.get("gt_id") @@ -167,7 +180,7 @@ def data(self, request, **kwargs): self.paginator.page_size_query_param, ] query_param_keys = request.query_params - should_paginate = any([k in query_param_keys for k in pagination_keys]) + should_paginate = any(k in query_param_keys for k in pagination_keys) data = [] if isinstance(self.object.content_object, XForm): @@ -207,7 +220,7 @@ def data(self, request, **kwargs): return Response(data) - # pylint: disable=arguments-differ + # pylint: disable=arguments-differ,too-many-locals def flatten_xform_columns( self, json_of_columns_fields, table: str = None, field_prefix: str = None ): @@ -230,8 +243,8 @@ def flatten_xform_columns( columns = self.flatten_xform_columns( field.get("children"), table=table_name, field_prefix=prefix ) - for key in columns.keys(): - ret[key].extend(columns[key]) + for key, val in columns.items(): + ret[key].extend(val) elif field_type == MULTIPLE_SELECT_TYPE: for option in field.get("children"): list_name = field.get("list_name") @@ -288,6 +301,7 @@ def get_tableau_column_headers(self): return tableau_column_headers def get_tableau_table_schemas(self) -> List[dict]: + """Return a list of Tableau table schemas.""" ret = [] project = self.xform.project_id id_str = self.xform.id_string @@ -304,6 +318,7 @@ def get_tableau_table_schemas(self) -> List[dict]: @action(methods=["GET"], detail=True) def schema(self, request, **kwargs): + # pylint: disable=attribute-defined-outside-init self.object = self.get_object() if isinstance(self.object.content_object, XForm): self.xform = self.object.content_object diff --git a/onadata/apps/api/viewsets/widget_viewset.py b/onadata/apps/api/viewsets/widget_viewset.py index 8c28a3bc70..1a73b427fb 100644 --- a/onadata/apps/api/viewsets/widget_viewset.py +++ b/onadata/apps/api/viewsets/widget_viewset.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Expose and persist charts and corresponding data. +""" from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from django.contrib.contenttypes.models import ContentType @@ -7,8 +11,7 @@ from rest_framework.exceptions import ParseError from onadata.libs import filters -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.cache_control_mixin import CacheControlMixin from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.apps.logger.models.widget import Widget @@ -20,37 +23,44 @@ BaseViewset = get_baseviewset_class() -class WidgetViewSet(AuthenticateHeaderMixin, - CacheControlMixin, ETagsMixin, BaseViewset, ModelViewSet): +# pylint: disable=too-many-ancestors +class WidgetViewSet( + AuthenticateHeaderMixin, CacheControlMixin, ETagsMixin, BaseViewset, ModelViewSet +): + """ + Expose and persist charts and corresponding data. + """ + queryset = Widget.objects.all() serializer_class = WidgetSerializer permission_classes = [WidgetViewSetPermissions] - lookup_field = 'pk' + lookup_field = "pk" filter_backends = (filters.WidgetFilter,) def filter_queryset(self, queryset): - dataviewid = self.request.query_params.get('dataview') + dataviewid = self.request.query_params.get("dataview") if dataviewid: try: int(dataviewid) - except ValueError: - raise ParseError( - u"Invalid value for dataview %s." % dataviewid) + except ValueError as exc: + raise ParseError(f"Invalid value for dataview {dataviewid}.") from exc dataview = get_object_or_404(DataView, pk=dataviewid) dataview_ct = ContentType.objects.get_for_model(dataview) - dataview_qs = Widget.objects.filter(object_id=dataview.pk, - content_type=dataview_ct) + dataview_qs = Widget.objects.filter( + object_id=dataview.pk, content_type=dataview_ct + ) return dataview_qs - return super(WidgetViewSet, self).filter_queryset(queryset) + return super().filter_queryset(queryset) + # pylint: disable=unused-argument def get_object(self, queryset=None): - pk = self.kwargs.get('pk') + widget_pk = self.kwargs.get("pk") - if pk is not None: - obj = get_object_or_404(Widget, pk=pk) + if widget_pk is not None: + obj = get_object_or_404(Widget, pk=widget_pk) self.check_object_permissions(self.request, obj) else: raise ParseError(_("'pk' required for this action")) @@ -58,12 +68,12 @@ def get_object(self, queryset=None): return obj def list(self, request, *args, **kwargs): - if 'key' in request.GET: - key = request.GET['key'] + if "key" in request.GET: + key = request.GET["key"] obj = get_object_or_404(Widget, key=key) serializer = self.get_serializer(obj) return Response(serializer.data) - return super(WidgetViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) diff --git a/onadata/apps/api/viewsets/xform_list_viewset.py b/onadata/apps/api/viewsets/xform_list_viewset.py index 3f9a406709..3fe92295de 100644 --- a/onadata/apps/api/viewsets/xform_list_viewset.py +++ b/onadata/apps/api/viewsets/xform_list_viewset.py @@ -1,58 +1,70 @@ +# -*- coding: utf-8 -*- +""" +OpenRosa Form List API - https://docs.getodk.org/openrosa-form-list/ +""" from django.conf import settings from django.http import Http404 from django.shortcuts import get_object_or_404 from django.views.decorators.cache import never_cache -from django_filters import rest_framework as django_filter_filters -from rest_framework import viewsets -from rest_framework import permissions +from django_filters import rest_framework as django_filter_filters +from rest_framework import permissions, viewsets from rest_framework.decorators import action from rest_framework.response import Response -from onadata.apps.api.tools import get_media_file_response +from onadata.apps.api.tools import get_baseviewset_class, get_media_file_response from onadata.apps.logger.models.xform import XForm, get_forms_shared_with_user from onadata.apps.main.models.meta_data import MetaData from onadata.apps.main.models.user_profile import UserProfile from onadata.libs import filters -from onadata.libs.authentication import DigestAuthentication -from onadata.libs.authentication import EnketoTokenAuthentication +from onadata.libs.authentication import DigestAuthentication, EnketoTokenAuthentication from onadata.libs.mixins.etags_mixin import ETagsMixin from onadata.libs.mixins.openrosa_headers_mixin import get_openrosa_headers -from onadata.libs.renderers.renderers import MediaFileContentNegotiation -from onadata.libs.renderers.renderers import XFormListRenderer -from onadata.libs.renderers.renderers import XFormManifestRenderer -from onadata.libs.serializers.xform_serializer import XFormListSerializer -from onadata.libs.serializers.xform_serializer import XFormManifestSerializer -from onadata.apps.api.tools import get_baseviewset_class -from onadata.libs.utils.export_tools import ExportBuilder -from onadata.libs.utils.common_tags import (GROUP_DELIMETER_TAG, - REPEAT_INDEX_TAGS) - +from onadata.libs.renderers.renderers import ( + MediaFileContentNegotiation, + XFormListRenderer, + XFormManifestRenderer, +) +from onadata.libs.serializers.xform_serializer import ( + XFormListSerializer, + XFormManifestSerializer, +) +from onadata.libs.utils.common_tags import GROUP_DELIMETER_TAG, REPEAT_INDEX_TAGS +from onadata.libs.utils.export_builder import ExportBuilder BaseViewset = get_baseviewset_class() # 10,000,000 bytes -DEFAULT_CONTENT_LENGTH = getattr(settings, 'DEFAULT_CONTENT_LENGTH', 10000000) +DEFAULT_CONTENT_LENGTH = getattr(settings, "DEFAULT_CONTENT_LENGTH", 10000000) -class XFormListViewSet(ETagsMixin, BaseViewset, - viewsets.ReadOnlyModelViewSet): - authentication_classes = (DigestAuthentication, - EnketoTokenAuthentication,) +# pylint: disable=too-many-ancestors +class XFormListViewSet(ETagsMixin, BaseViewset, viewsets.ReadOnlyModelViewSet): + """ + OpenRosa Form List API - https://docs.getodk.org/openrosa-form-list/ + """ + + authentication_classes = ( + DigestAuthentication, + EnketoTokenAuthentication, + ) content_negotiation_class = MediaFileContentNegotiation filter_class = filters.FormIDFilter - filter_backends = (filters.XFormListObjectPermissionFilter, - filters.XFormListXFormPKFilter, - django_filter_filters.DjangoFilterBackend) + filter_backends = ( + filters.XFormListObjectPermissionFilter, + filters.XFormListXFormPKFilter, + django_filter_filters.DjangoFilterBackend, + ) queryset = XForm.objects.filter( - downloadable=True, deleted_at=None, - is_merged_dataset=False).only('id_string', 'title', 'version', 'uuid', - 'description', 'user__username', 'hash') + downloadable=True, deleted_at=None, is_merged_dataset=False + ).only( + "id_string", "title", "version", "uuid", "description", "user__username", "hash" + ) permission_classes = (permissions.AllowAny,) renderer_classes = (XFormListRenderer,) serializer_class = XFormListSerializer - template_name = 'api/xformsList.xml' + template_name = "api/xformsList.xml" def get_object(self): queryset = self.filter_queryset(self.get_queryset()) @@ -66,24 +78,24 @@ def get_object(self): return obj def get_renderers(self): - if self.action and self.action == 'manifest': + if self.action and self.action == "manifest": return [XFormManifestRenderer()] - return super(XFormListViewSet, self).get_renderers() + return super().get_renderers() def filter_queryset(self, queryset): - username = self.kwargs.get('username') - form_pk = self.kwargs.get('xform_pk') - project_pk = self.kwargs.get('project_pk') - if (not username and not form_pk and not project_pk) and \ - self.request.user.is_anonymous: + username = self.kwargs.get("username") + form_pk = self.kwargs.get("xform_pk") + project_pk = self.kwargs.get("project_pk") + if ( + not username and not form_pk and not project_pk + ) and self.request.user.is_anonymous: # raises a permission denied exception, forces authentication self.permission_denied(self.request) profile = None if username: - profile = get_object_or_404( - UserProfile, user__username__iexact=username) + profile = get_object_or_404(UserProfile, user__username__iexact=username) elif form_pk: queryset = queryset.filter(pk=form_pk) if queryset.first(): @@ -97,78 +109,96 @@ def filter_queryset(self, queryset): # raises a permission denied exception, forces authentication self.permission_denied(self.request) else: - queryset = queryset.filter( - user=profile.user, downloadable=True) + queryset = queryset.filter(user=profile.user, downloadable=True) - queryset = super(XFormListViewSet, self).filter_queryset(queryset) + queryset = super().filter_queryset(queryset) if not self.request.user.is_anonymous: - xform_pk = self.kwargs.get('xform_pk') - if self.action == 'list' and profile and xform_pk is None\ - and project_pk is None: - forms_shared_with_user = get_forms_shared_with_user( - profile.user) - id_string = self.request.query_params.get('formID') - forms_shared_with_user = forms_shared_with_user.filter( - id_string=id_string) if id_string else \ - forms_shared_with_user + xform_pk = self.kwargs.get("xform_pk") + if ( + self.action == "list" + and profile + and xform_pk is None + and project_pk is None + ): + forms_shared_with_user = get_forms_shared_with_user(profile.user) + id_string = self.request.query_params.get("formID") + forms_shared_with_user = ( + forms_shared_with_user.filter(id_string=id_string) + if id_string + else forms_shared_with_user + ) queryset = queryset | forms_shared_with_user if self.request.user != profile.user: public_forms = profile.user.xforms.filter( - downloadable=True, shared=True) + downloadable=True, shared=True + ) queryset = queryset | public_forms return queryset @never_cache def list(self, request, *args, **kwargs): + # pylint: disable=attribute-defined-outside-init self.object_list = self.filter_queryset(self.get_queryset()) headers = get_openrosa_headers(request, location=False) serializer = self.get_serializer(self.object_list, many=True) - if request.method in ['HEAD']: - return Response('', headers=headers, status=204) + if request.method in ["HEAD"]: + return Response("", headers=headers, status=204) return Response(serializer.data, headers=headers) def retrieve(self, request, *args, **kwargs): + # pylint: disable=attribute-defined-outside-init self.object = self.get_object() - return Response(self.object.xml, - headers=get_openrosa_headers(request, location=False)) + return Response( + self.object.xml, headers=get_openrosa_headers(request, location=False) + ) - @action(methods=['GET', 'HEAD'], detail=True) + @action(methods=["GET", "HEAD"], detail=True) def manifest(self, request, *args, **kwargs): + """A manifest defining additional supporting objects.""" + # pylint: disable=attribute-defined-outside-init self.object = self.get_object() - object_list = MetaData.objects.filter(data_type='media', - object_id=self.object.pk) + object_list = MetaData.objects.filter( + data_type="media", object_id=self.object.pk + ) context = self.get_serializer_context() context[GROUP_DELIMETER_TAG] = ExportBuilder.GROUP_DELIMITER_DOT - context[REPEAT_INDEX_TAGS] = '_,_' - serializer = XFormManifestSerializer(object_list, many=True, - context=context) + context[REPEAT_INDEX_TAGS] = "_,_" + serializer = XFormManifestSerializer(object_list, many=True, context=context) - return Response(serializer.data, - headers=get_openrosa_headers(request, location=False)) + return Response( + serializer.data, headers=get_openrosa_headers(request, location=False) + ) - @action(methods=['GET', 'HEAD'], detail=True) + @action(methods=["GET", "HEAD"], detail=True) def media(self, request, *args, **kwargs): + """Returns the media file contents.""" + # pylint: disable=attribute-defined-outside-init self.object = self.get_object() - pk = kwargs.get('metadata') + media_pk = kwargs.get("metadata") - if not pk: + if not media_pk: raise Http404() meta_obj = get_object_or_404( - MetaData, data_type='media', object_id=self.object.pk, pk=pk) + MetaData, data_type="media", object_id=self.object.pk, pk=media_pk + ) response = get_media_file_response(meta_obj, request) if response.status_code == 403 and request.user.is_anonymous: # raises a permission denied exception, forces authentication self.permission_denied(request) - else: - return response + + return response class PreviewXFormListViewSet(XFormListViewSet): + """ + OpenRosa Form List API - for preview purposes only + """ + filter_backends = (filters.AnonDjangoObjectPermissionFilter,) permission_classes = (permissions.AllowAny,) diff --git a/onadata/apps/api/viewsets/xform_submission_viewset.py b/onadata/apps/api/viewsets/xform_submission_viewset.py index c05d3df4f8..df1629ebb7 100644 --- a/onadata/apps/api/viewsets/xform_submission_viewset.py +++ b/onadata/apps/api/viewsets/xform_submission_viewset.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ XFormSubmissionViewSet module """ @@ -7,8 +7,7 @@ from django.utils.translation import gettext as _ from rest_framework import mixins, permissions, status, viewsets -from rest_framework.authentication import (BasicAuthentication, - TokenAuthentication) +from rest_framework.authentication import BasicAuthentication, TokenAuthentication from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer from rest_framework.response import Response @@ -17,51 +16,67 @@ from onadata.apps.api.tools import get_baseviewset_class from onadata.apps.logger.models import Instance from onadata.libs import filters -from onadata.libs.authentication import (DigestAuthentication, - EnketoTokenAuthentication) -from onadata.libs.mixins.authenticate_header_mixin import \ - AuthenticateHeaderMixin +from onadata.libs.authentication import DigestAuthentication, EnketoTokenAuthentication +from onadata.libs.mixins.authenticate_header_mixin import AuthenticateHeaderMixin from onadata.libs.mixins.openrosa_headers_mixin import OpenRosaHeadersMixin from onadata.libs.renderers.renderers import FLOIPRenderer, TemplateXMLRenderer from onadata.libs.serializers.data_serializer import ( - FLOIPSubmissionSerializer, JSONSubmissionSerializer, - RapidProSubmissionSerializer, SubmissionSerializer, - RapidProJSONSubmissionSerializer) -from onadata.libs.utils.logger_tools import (OpenRosaResponseBadRequest, - OpenRosaNotAuthenticated) - -BaseViewset = get_baseviewset_class() # pylint: disable=C0103 + FLOIPSubmissionSerializer, + JSONSubmissionSerializer, + RapidProSubmissionSerializer, + SubmissionSerializer, + RapidProJSONSubmissionSerializer, +) +from onadata.libs.utils.logger_tools import ( + OpenRosaResponseBadRequest, + OpenRosaNotAuthenticated, +) + +BaseViewset = get_baseviewset_class() # pylint: disable=invalid-name # 10,000,000 bytes -DEFAULT_CONTENT_LENGTH = getattr(settings, 'DEFAULT_CONTENT_LENGTH', 10000000) -FLOIP_RESULTS_CONTENT_TYPE = 'application/vnd.org.flowinterop.results+json' +DEFAULT_CONTENT_LENGTH = getattr(settings, "DEFAULT_CONTENT_LENGTH", 10000000) +FLOIP_RESULTS_CONTENT_TYPE = "application/vnd.org.flowinterop.results+json" class FLOIPParser(JSONParser): # pylint: disable=too-few-public-methods """ Flow Results JSON parser. """ + media_type = FLOIP_RESULTS_CONTENT_TYPE renderer_classes = FLOIPRenderer -class XFormSubmissionViewSet(AuthenticateHeaderMixin, # pylint: disable=R0901 - OpenRosaHeadersMixin, - mixins.CreateModelMixin, - BaseViewset, - viewsets.GenericViewSet): +# pylint: disable=too-many-ancestors +class XFormSubmissionViewSet( + AuthenticateHeaderMixin, # pylint: disable=too-many-ancestors + OpenRosaHeadersMixin, + mixins.CreateModelMixin, + BaseViewset, + viewsets.GenericViewSet, +): """ XFormSubmissionViewSet class """ - authentication_classes = (DigestAuthentication, BasicAuthentication, - TokenAuthentication, EnketoTokenAuthentication) - filter_backends = (filters.AnonDjangoObjectPermissionFilter, ) + + authentication_classes = ( + DigestAuthentication, + BasicAuthentication, + TokenAuthentication, + EnketoTokenAuthentication, + ) + filter_backends = (filters.AnonDjangoObjectPermissionFilter,) model = Instance permission_classes = (permissions.AllowAny, IsAuthenticatedSubmission) - renderer_classes = (TemplateXMLRenderer, JSONRenderer, - BrowsableAPIRenderer, FLOIPRenderer) + renderer_classes = ( + TemplateXMLRenderer, + JSONRenderer, + BrowsableAPIRenderer, + FLOIPRenderer, + ) serializer_class = SubmissionSerializer - template_name = 'submission.xml' + template_name = "submission.xml" parser_classes = (FLOIPParser, JSONParser, FormParser, MultiPartParser) def get_serializer(self, *args, **kwargs): @@ -71,12 +86,10 @@ def get_serializer(self, *args, **kwargs): data = kwargs.get("data") content_type = self.request.content_type.lower() - if (isinstance(data, list) - and FLOIP_RESULTS_CONTENT_TYPE in content_type): + if isinstance(data, list) and FLOIP_RESULTS_CONTENT_TYPE in content_type: kwargs["many"] = True - return super(XFormSubmissionViewSet, self).get_serializer( - *args, **kwargs) + return super().get_serializer(*args, **kwargs) def get_serializer_class(self): """ @@ -84,16 +97,15 @@ def get_serializer_class(self): """ content_type = self.request.content_type.lower() - if 'application/json' in content_type: - if 'RapidProMailroom' in self.request.headers.get( - 'User-Agent', ''): + if "application/json" in content_type: + if "RapidProMailroom" in self.request.headers.get("User-Agent", ""): return RapidProJSONSubmissionSerializer self.request.accepted_renderer = JSONRenderer() - self.request.accepted_media_type = 'application/json' + self.request.accepted_media_type = "application/json" return JSONSubmissionSerializer - if 'application/x-www-form-urlencoded' in content_type: + if "application/x-www-form-urlencoded" in content_type: return RapidProSubmissionSerializer if FLOIP_RESULTS_CONTENT_TYPE in content_type: @@ -104,32 +116,32 @@ def get_serializer_class(self): return SubmissionSerializer def create(self, request, *args, **kwargs): - if request.method.upper() == 'HEAD': + if request.method.upper() == "HEAD": return Response( - status=status.HTTP_204_NO_CONTENT, - template_name=self.template_name) + status=status.HTTP_204_NO_CONTENT, template_name=self.template_name + ) - return super(XFormSubmissionViewSet, self).create( - request, *args, **kwargs) + return super().create(request, *args, **kwargs) def handle_exception(self, exc): """ Handles exceptions thrown by handler method and returns appropriate error response. """ - if hasattr(exc, 'response'): + if hasattr(exc, "response"): return exc.response if isinstance(exc, UnreadablePostError): return OpenRosaResponseBadRequest( - _(u"Unable to read submitted file, please try re-submitting.")) + _("Unable to read submitted file, please try re-submitting.") + ) try: if exc.status_code == 401: auth_header = self.get_authenticate_header(self.request) response = OpenRosaNotAuthenticated( data=exc.detail, - headers={'WWW-Authenticate': auth_header}, + headers={"WWW-Authenticate": auth_header}, ) response.exception = True return response @@ -137,4 +149,4 @@ def handle_exception(self, exc): # 'Http404' object has no attribute 'status_code' pass - return super(XFormSubmissionViewSet, self).handle_exception(exc) + return super().handle_exception(exc) diff --git a/onadata/apps/api/viewsets/xform_viewset.py b/onadata/apps/api/viewsets/xform_viewset.py index a55135c8e2..2735b4cc8d 100644 --- a/onadata/apps/api/viewsets/xform_viewset.py +++ b/onadata/apps/api/viewsets/xform_viewset.py @@ -450,7 +450,6 @@ def form(self, request, **kwargs): return response - # pylint: disable=no-self-use # pylint: disable=unused-argument @action(methods=["GET"], detail=False) def login(self, request, **kwargs): diff --git a/onadata/apps/logger/factory.py b/onadata/apps/logger/factory.py index 7ce6529203..9236a67ca8 100644 --- a/onadata/apps/logger/factory.py +++ b/onadata/apps/logger/factory.py @@ -95,7 +95,6 @@ def create_registration_xform(self): return xform - # pylint: disable=no-self-use def get_registration_xform(self): """ Gets a registration xform. (currently loaded in from fixture) diff --git a/onadata/apps/logger/import_tools.py b/onadata/apps/logger/import_tools.py index f25d5b8a71..099ba76810 100644 --- a/onadata/apps/logger/import_tools.py +++ b/onadata/apps/logger/import_tools.py @@ -12,7 +12,7 @@ from django.http.response import Http404 from onadata.apps.logger.xform_fs import XFormInstanceFS -from onadata.celery import app +from onadata.celeryapp import app from onadata.libs.utils.logger_tools import create_instance # odk @@ -110,6 +110,7 @@ def iterate_through_instances( success_count = 0 errors = [] + # pylint: disable=too-many-nested-blocks for directory, _subdirs, subfiles in os.walk(dirpath): for filename in subfiles: filepath = os.path.join(directory, filename) diff --git a/onadata/apps/logger/management/commands/add_id.py b/onadata/apps/logger/management/commands/add_id.py index fe8651a7d8..f05434176f 100644 --- a/onadata/apps/logger/management/commands/add_id.py +++ b/onadata/apps/logger/management/commands/add_id.py @@ -1,4 +1,8 @@ -from django.contrib.auth.models import User +# -*- coding: utf-8 -*- +""" +Sync account with '_id' +""" +from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy from django.core.management.base import BaseCommand from django.conf import settings @@ -7,7 +11,12 @@ from onadata.libs.utils.model_tools import queryset_iterator +User = get_user_model() + + class Command(BaseCommand): + """Sync account with '_id'""" + args = "" help = gettext_lazy("Sync account with '_id'") @@ -18,7 +27,7 @@ def handle(self, *args, **kwargs): users = User.objects.filter(username__contains=args[0]) else: # All the accounts - self.stdout.write("Fetching all the account {}", ending="\n") + self.stdout.write("Fetching all the accounts.", ending="\n") users = User.objects.exclude( username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME ) @@ -27,7 +36,8 @@ def handle(self, *args, **kwargs): self.add_id(user) def add_id(self, user): - self.stdout.write("Syncing for account {}".format(user.username), ending="\n") + """Append _id in submissions for the specifing ``user``.""" + self.stdout.write(f"Syncing for account {user.username}", ending="\n") xforms = XForm.objects.filter(user=user) count = 0 @@ -40,14 +50,13 @@ def add_id(self, user): try: instance.save() count += 1 + # pylint: disable=broad-except except Exception as e: failed += 1 self.stdout.write(str(e), ending="\n") - pass self.stdout.write( - "Syncing for account {}. Done. Success {}, Fail {}".format( - user.username, count, failed - ), + f"Syncing for account {user.username}. Done. " + f"Success {count}, Fail {failed}", ending="\n", ) diff --git a/onadata/apps/logger/management/commands/change_s3_media_permissions.py b/onadata/apps/logger/management/commands/change_s3_media_permissions.py index 7fa04adc28..1956f03447 100644 --- a/onadata/apps/logger/management/commands/change_s3_media_permissions.py +++ b/onadata/apps/logger/management/commands/change_s3_media_permissions.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ change_s3_media_permissions - makes all s3 files private. """ diff --git a/onadata/apps/logger/management/commands/create_image_thumbnails.py b/onadata/apps/logger/management/commands/create_image_thumbnails.py index 5c77b21d8b..eeb6b80575 100644 --- a/onadata/apps/logger/management/commands/create_image_thumbnails.py +++ b/onadata/apps/logger/management/commands/create_image_thumbnails.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- """ create_image_thumbnails - creates thumbnails for all form images and stores them. @@ -18,6 +17,8 @@ THUMB_CONF = settings.THUMB_CONF +User = get_user_model() + class Command(BaseCommand): """Creates thumbnails for all form images and stores them""" @@ -43,8 +44,6 @@ def handle(self, *args, **options): attachments_qs = Attachment.objects.select_related( "instance", "instance__xform" ) - # pylint: disable=invalid-name - User = get_user_model() if options.get("username"): username = options.get("username") try: @@ -61,19 +60,23 @@ def handle(self, *args, **options): f"Error: Form with id_string {id_string} does not exist" ) from e attachments_qs = attachments_qs.filter(instance__xform=xform) - fs = get_storage_class("django.core.files.storage.FileSystemStorage")() + file_storage = get_storage_class( + "django.core.files.storage.FileSystemStorage" + )() for att in queryset_iterator(attachments_qs): filename = att.media_file.name default_storage = get_storage_class()() full_path = get_path(filename, settings.THUMB_CONF["small"]["suffix"]) if options.get("force") is not None: - for s in ["small", "medium", "large"]: - fp = get_path(filename, settings.THUMB_CONF[s]["suffix"]) - if default_storage.exists(fp): - default_storage.delete(fp) + for suffix in ["small", "medium", "large"]: + file_path = get_path( + filename, settings.THUMB_CONF[suffix]["suffix"] + ) + if default_storage.exists(file_path): + default_storage.delete(file_path) if not default_storage.exists(full_path): try: - if default_storage.__class__ != fs.__class__: + if default_storage.__class__ != file_storage.__class__: resize(filename, att.extension) else: resize_local_env(filename, att.extension) diff --git a/onadata/apps/logger/management/commands/export_gps_points.py b/onadata/apps/logger/management/commands/export_gps_points.py index fec5259d0a..a1a51666f5 100644 --- a/onadata/apps/logger/management/commands/export_gps_points.py +++ b/onadata/apps/logger/management/commands/export_gps_points.py @@ -1,4 +1,7 @@ -#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Export all gps points with their timestamps +""" import csv from django.core.management.base import BaseCommand @@ -9,10 +12,12 @@ class Command(BaseCommand): + """Export all gps points with their timestamps""" + help = gettext_lazy("Export all gps points with their timestamps") def handle(self, *args, **kwargs): - with open("gps_points_export.csv", "w") as csvfile: + with open("gps_points_export.csv", "w", encoding="utf-8") as csvfile: fieldnames = ["longitude", "latitude", "date_created"] writer = csv.writer(csvfile) writer.writerow(fieldnames) diff --git a/onadata/apps/logger/management/commands/export_xforms_and_instances.py b/onadata/apps/logger/management/commands/export_xforms_and_instances.py index 28c7088473..3a36285c59 100644 --- a/onadata/apps/logger/management/commands/export_xforms_and_instances.py +++ b/onadata/apps/logger/management/commands/export_xforms_and_instances.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ export_xformx_and_instances - exports XForms and submission instances into JSON files. """ diff --git a/onadata/apps/logger/management/commands/fix_attachments_counts.py b/onadata/apps/logger/management/commands/fix_attachments_counts.py index edc2b203a6..747cdc7418 100644 --- a/onadata/apps/logger/management/commands/fix_attachments_counts.py +++ b/onadata/apps/logger/management/commands/fix_attachments_counts.py @@ -1,10 +1,10 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Fix submission media count command. """ import os -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy @@ -16,6 +16,9 @@ from onadata.libs.utils.model_tools import queryset_iterator +User = get_user_model() + + def update_attachments(instance): """ Takes an Instance object and updates attachment tracking fields @@ -42,15 +45,15 @@ def add_arguments(self, parser): def handle(self, *args, **options): try: username = options["username"] - except KeyError: + except KeyError as exc: raise CommandError( _("You must provide the username to publish the form to.") - ) + ) from exc # make sure user exists try: user = User.objects.get(username=username) - except User.DoesNotExist: - raise CommandError(_("The user '%s' does not exist.") % username) + except User.DoesNotExist as exc: + raise CommandError(_(f"The user '{username}' does not exist.")) from exc self.process_attachments(user) @@ -70,6 +73,6 @@ def process_attachments(self, user): update_attachments(submission) not_processed = xform.instances.filter(media_all_received=False).count() self.stdout.write( - "%s to process %s - %s = %s processed" - % (xform, to_process, not_processed, (to_process - not_processed)) + f"{xform} to process {to_process} - {not_processed} " + f"= {to_process - not_processed} processed" ) diff --git a/onadata/apps/logger/management/commands/fix_duplicate_instances.py b/onadata/apps/logger/management/commands/fix_duplicate_instances.py index 2dfab73a58..d309b2d7b5 100644 --- a/onadata/apps/logger/management/commands/fix_duplicate_instances.py +++ b/onadata/apps/logger/management/commands/fix_duplicate_instances.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 fileencoding=utf-8 - +""" +Fix duplicate instances by merging the attachments. +""" from django.core.management.base import BaseCommand from django.db import connection from django.utils.translation import gettext_lazy @@ -9,14 +11,18 @@ class Command(BaseCommand): + """Fix duplicate instances by merging the attachments.""" + help = gettext_lazy("Fix duplicate instances by merging the attachments.") def query_data(self, sql): + """Return results of given ``sql`` query.""" cursor = connection.cursor() cursor.execute(sql) for row in cursor.fetchall(): yield row + # pylint: disable=too-many-locals def handle(self, *args, **kwargs): sql = ( "select xform_id, uuid, COUNT(xform_id || uuid) " @@ -42,8 +48,8 @@ def handle(self, *args, **kwargs): # is a match, let's maintain the submission with the # correct xform.uuid. if is_mspray_form and not all_matches: - first = instances.filter(xml__contains=i.xform.uuid).first() - to_delete = instances.exclude(xml__contains=i.xform.uuid) + first = instances.filter(xml__contains=xform.uuid).first() + to_delete = instances.exclude(xml__contains=xform.uuid) else: to_delete = instances.exclude(pk=first.pk) @@ -58,14 +64,14 @@ def handle(self, *args, **kwargs): if delete_count >= dupes_count: raise AssertionError( - "# of records to delete %d should be less than total # of " - "duplicates %d." % (delete_count, dupes_count) + f"# of records to delete {delete_count} should be less than" + f" total # of duplicates {dupes_count}." ) to_delete.delete() total_count += dupes_count total_deleted += delete_count self.stdout.write( - "deleted %d: %s (%d of %d)." % (xform, uuid, delete_count, dupes_count) + f"deleted {xform}: {uuid} ({delete_count} of {dupes_count})." ) - self.stdout.write("done: deleted %d of %d" % (total_deleted, total_count)) + self.stdout.write(f"done: deleted {total_deleted} of {total_count}") diff --git a/onadata/apps/logger/management/commands/fix_submission_count.py b/onadata/apps/logger/management/commands/fix_submission_count.py index 5cc99a0c17..93faa9c593 100644 --- a/onadata/apps/logger/management/commands/fix_submission_count.py +++ b/onadata/apps/logger/management/commands/fix_submission_count.py @@ -1,5 +1,8 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 fileencoding=utf-8 +""" +Fix num of submissions +""" from django.core.management.base import BaseCommand from django.db import transaction @@ -11,6 +14,8 @@ class Command(BaseCommand): + """Fix num of submissions""" + help = gettext_lazy("Fix num of submissions") def handle(self, *args, **kwargs): @@ -23,9 +28,7 @@ def handle(self, *args, **kwargs): xform.save(update_fields=["num_of_submissions"]) i += 1 self.stdout.write( - "Processing {} of {}: {} ({})".format( - i, xform_count, xform.id_string, instance_count - ) + f"Processing {i} of {xform_count}: {xform.id_string} ({instance_count})" ) i = 0 @@ -39,7 +42,6 @@ def handle(self, *args, **kwargs): profile.save(update_fields=["num_of_submissions"]) i += 1 self.stdout.write( - "Processing {} of {}: {} ({})".format( - i, profile_count, profile.user.username, instance_count - ) + f"Processing {i} of {profile_count}: {profile.user.username} " + f"({instance_count})" ) diff --git a/onadata/apps/logger/management/commands/generate_platform_stats.py b/onadata/apps/logger/management/commands/generate_platform_stats.py index a00dab92c0..e6b23afec0 100644 --- a/onadata/apps/logger/management/commands/generate_platform_stats.py +++ b/onadata/apps/logger/management/commands/generate_platform_stats.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Management command used to generate platform statistics containing information about the number of organizations, users, projects @@ -5,11 +6,13 @@ """ import calendar import csv -from datetime import date, datetime +import os.path +from datetime import datetime -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand from django.db.models import Q +from django.utils import timezone from django.utils.translation import gettext as _ from multidb.pinning import use_master @@ -18,54 +21,62 @@ from onadata.apps.logger.models import Instance, XForm from onadata.libs.permissions import is_organization +User = get_user_model() -def _write_stats_to_file(month: int, year: int, include_extra: bool): - out_file = open(f"/tmp/platform_statistics_{month}_{year}.csv", "w") # nosec - writer = csv.writer(out_file) - headers = ["Username", "Project Name", "Form Title", "No. of submissions"] - form_fields = ["id", "project__name", "project__organization__username", "title"] - if include_extra: - headers += ["Is Organization", "Organization Created By", "User last login"] - form_fields += ["project__organization__last_login"] - writer.writerow(headers) - _, last_day = calendar.monthrange(year, month) - date_obj = date(year, month, last_day) +# pylint: disable=too-many-locals +def _write_stats_to_file(month: int, year: int, include_extra: bool, filename: str): + with open(filename, "w", encoding="utf-8") as out_file: + writer = csv.writer(out_file) + headers = ["Username", "Project Name", "Form Title", "No. of submissions"] + form_fields = [ + "id", + "project__name", + "project__organization__username", + "title", + ] + if include_extra: + headers += ["Is Organization", "Organization Created By", "User last login"] + form_fields += ["project__organization__last_login"] - forms = XForm.objects.filter( - Q(deleted_at__isnull=True) | Q(deleted_at__gt=date_obj), - date_created__lte=date_obj, - ).values(*form_fields) - with use_master: - for form in forms: - instance_count = Instance.objects.filter( - Q(deleted_at__isnull=True) | Q(deleted_at__gt=date_obj), - xform_id=form.get("id"), - date_created__lte=date_obj, - ).count() - row = [ - form.get("project__organization__username"), - form.get("project__name"), - form.get("title"), - instance_count, - ] - if include_extra: - user = User.objects.get( - username=form.get("project__organization__username") - ) - is_org = is_organization(user.profile) - if is_org: - created_by = OrganizationProfile.objects.get( - user=user - ).creator.username - else: - created_by = "N/A" - row += [ - is_org, - created_by, - form.get("project__organization__last_login"), + writer.writerow(headers) + _, last_day = calendar.monthrange(year, month) + date_obj = timezone.make_aware(datetime(year, month, last_day), timezone.utc) + + forms = XForm.objects.filter( + Q(deleted_at__isnull=True) | Q(deleted_at__gt=date_obj), + date_created__lte=date_obj, + ).values(*form_fields) + with use_master: + for form in forms: + instance_count = Instance.objects.filter( + Q(deleted_at__isnull=True) | Q(deleted_at__gt=date_obj), + xform_id=form.get("id"), + date_created__lte=date_obj, + ).count() + row = [ + form.get("project__organization__username"), + form.get("project__name"), + form.get("title"), + instance_count, ] - writer.writerow(row) + if include_extra: + user = User.objects.get( + username=form.get("project__organization__username") + ) + is_org = is_organization(user.profile) + if is_org: + created_by = OrganizationProfile.objects.get( + user=user + ).creator.username + else: + created_by = "N/A" + row += [ + is_org, + created_by, + form.get("project__organization__last_login"), + ] + writer.writerow(row) class Command(BaseCommand): @@ -83,7 +94,7 @@ def add_arguments(self, parser): "-m", dest="month", help=( - "Month to calculate system statistics for." "Defaults to current month." + "Month to calculate system statistics for. Defaults to current month." ), default=None, ) @@ -91,9 +102,7 @@ def add_arguments(self, parser): "--year", "-y", dest="year", - help=( - "Year to calculate system statistics for." " Defaults to current year" - ), + help=("Year to calculate system statistics for. Defaults to current year"), default=None, ) parser.add_argument( @@ -102,11 +111,17 @@ def add_arguments(self, parser): action="store_true", dest="extra_info", default=False, - help="Include extra information; When an Organization was created and user last login", + help=( + "Include extra information; When an Organization was created and " + "user last login" + ), ) def handle(self, *args, **options): - month = int(options.get("month", datetime.now().month)) - year = int(options.get("year", datetime.now().year)) - include_extra = bool(options.get("extra_info", False)) - _write_stats_to_file(month, year) + month = int(options.get("month") or datetime.now().month) + year = int(options.get("year") or datetime.now().year) + include_extra = bool(options.get("extra_info")) + filename = f"platform_statistics_{month}_{year}.csv" + _write_stats_to_file(month, year, include_extra, filename) + if os.path.exists(filename): + self.stdout.write(f"File '{filename}' successfully created.") diff --git a/onadata/apps/logger/management/commands/import_tools.py b/onadata/apps/logger/management/commands/import_tools.py index fbb8eb7d35..49e46d37a6 100644 --- a/onadata/apps/logger/management/commands/import_tools.py +++ b/onadata/apps/logger/management/commands/import_tools.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ import_tools - import ODK formms and instances. """ @@ -26,7 +26,7 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): """Import ODK forms and instances.""" - if args.__len__() < 2: + if len(args) < 2: raise CommandError(_("path(xform instances) username")) path = args[0] username = args[1] diff --git a/onadata/apps/logger/management/commands/publish_xls.py b/onadata/apps/logger/management/commands/publish_xls.py index bd5f53851d..62da71e2d8 100644 --- a/onadata/apps/logger/management/commands/publish_xls.py +++ b/onadata/apps/logger/management/commands/publish_xls.py @@ -23,7 +23,7 @@ class Command(BaseCommand): args = "xls_file username project" help = gettext_lazy( - "Publish an XLS file with the option of replacing an" "existing one" + "Publish an XLSForm file with the option of replacing an existing one" ) def add_arguments(self, parser): diff --git a/onadata/apps/logger/management/commands/pull_from_aggregate.py b/onadata/apps/logger/management/commands/pull_from_aggregate.py index 07b7f75384..b94d9c92f9 100644 --- a/onadata/apps/logger/management/commands/pull_from_aggregate.py +++ b/onadata/apps/logger/management/commands/pull_from_aggregate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ pull_from_aggregate command diff --git a/onadata/apps/logger/management/commands/remove_columns_from_briefcase_data.py b/onadata/apps/logger/management/commands/remove_columns_from_briefcase_data.py index a9a8fc3702..f9141f0e71 100644 --- a/onadata/apps/logger/management/commands/remove_columns_from_briefcase_data.py +++ b/onadata/apps/logger/management/commands/remove_columns_from_briefcase_data.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Delete specific columns from submission XMLs pulled by ODK Briefcase. +""" import os from typing import List @@ -8,8 +12,8 @@ def _traverse_child_nodes_and_delete_column(xml_obj, column: str) -> None: - childNodes = xml_obj.childNodes - for elem in childNodes: + child_nodes = xml_obj.childNodes + for elem in child_nodes: if elem.nodeName in column: xml_obj.removeChild(elem) if hasattr(elem, "childNodes"): @@ -17,6 +21,7 @@ def _traverse_child_nodes_and_delete_column(xml_obj, column: str) -> None: def remove_columns_from_xml(xml: str, columns: List[str]) -> str: + """Returns the ``xml`` with columns/tags removed.""" xml_obj = clean_and_parse_xml(xml).documentElement for column in columns: _traverse_child_nodes_and_delete_column(xml_obj, column) @@ -24,7 +29,11 @@ def remove_columns_from_xml(xml: str, columns: List[str]) -> str: class Command(BaseCommand): - help = _("Delete specific columns from submission " "XMLs pulled by ODK Briefcase.") + """ + Delete specific columns from submission XMLs pulled by ODK Briefcase. + """ + + help = _("Delete specific columns from submission XMLs pulled by ODK Briefcase.") def add_arguments(self, parser): parser.add_argument( @@ -76,7 +85,9 @@ def handle(self, *args, **options): ) data = None - with open(f"{in_dir}/{submission_folder}/submission.xml", "r") as in_file: + with open( + f"{in_dir}/{submission_folder}/submission.xml", "r", encoding="utf-8" + ) as in_file: data = in_file.read().replace("\n", "") data = remove_columns_from_xml(data, columns) in_file.close() @@ -87,13 +98,17 @@ def handle(self, *args, **options): os.makedirs(f"{out_dir}/{submission_folder}") with open( - f"{out_dir}/{submission_folder}/submission.xml", "w" + f"{out_dir}/{submission_folder}/submission.xml", + "w", + encoding="utf-8", ) as out_file: out_file.write(data) out_file.close() else: with open( - f"{in_dir}/{submission_folder}/submission.xml", "r+" + f"{in_dir}/{submission_folder}/submission.xml", + "r+", + encoding="utf-8", ) as out_file: out_file.truncate(0) out_file.write(data) diff --git a/onadata/apps/logger/management/commands/replace_form_id_root_node.py b/onadata/apps/logger/management/commands/replace_form_id_root_node.py index d4f5f47562..2b9498c392 100644 --- a/onadata/apps/logger/management/commands/replace_form_id_root_node.py +++ b/onadata/apps/logger/management/commands/replace_form_id_root_node.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Management command used to replace the root node of an Instance when the root node is the XForm ID @@ -15,9 +16,11 @@ from onadata.apps.logger.models.instance import InstanceHistory +# pylint: disable=invalid-name def replace_form_id_with_correct_root_node( inst_id: int, root: str = None, commit: bool = False ) -> str: + """Returns the submission XML with updated root node tag name.""" inst: Instance = Instance.objects.get(id=inst_id, deleted_at__isnull=True) initial_xml = inst.xml form_id = re.escape(inst.xform.id_string) @@ -41,11 +44,13 @@ def replace_form_id_with_correct_root_node( inst.xml = edited_xml inst.save() return f"Modified Instance ID {inst.id} - History object {history.id}" - else: - return edited_xml + + return edited_xml class Command(BaseCommand): + """Replaces form ID String with 'data' for an instances root node""" + help = _("Replaces form ID String with 'data' for an instances root node") def add_arguments(self, parser): diff --git a/onadata/apps/logger/management/commands/set_xform_surveys_with_geopoints.py b/onadata/apps/logger/management/commands/set_xform_surveys_with_geopoints.py index b40780779a..daf2a9c749 100644 --- a/onadata/apps/logger/management/commands/set_xform_surveys_with_geopoints.py +++ b/onadata/apps/logger/management/commands/set_xform_surveys_with_geopoints.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 fileencoding=utf-8 - +""" +Import a folder of XForms for ODK. +""" from django.core.management.base import BaseCommand from django.utils.translation import gettext_lazy @@ -9,6 +11,8 @@ class Command(BaseCommand): + """Import a folder of XForms for ODK.""" + help = gettext_lazy("Import a folder of XForms for ODK.") def handle(self, *args, **kwargs): @@ -20,8 +24,9 @@ def handle(self, *args, **kwargs): try: xform.instances_with_geopoints = has_geo xform.save() + # pylint: disable=broad-except except Exception as e: self.stderr.write(e) else: count += 1 - self.stdout.write("%d of %d forms processed." % (count, total)) + self.stdout.write(f"{count} of {total} forms processed.") diff --git a/onadata/apps/logger/management/commands/set_xform_surveys_with_osm.py b/onadata/apps/logger/management/commands/set_xform_surveys_with_osm.py index df351dddae..d4236808b6 100644 --- a/onadata/apps/logger/management/commands/set_xform_surveys_with_osm.py +++ b/onadata/apps/logger/management/commands/set_xform_surveys_with_osm.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 fileencoding=utf-8 - +""" +Set xform.instances_with_osm +""" from django.core.management.base import BaseCommand from django.utils.translation import gettext_lazy @@ -10,6 +12,8 @@ class Command(BaseCommand): + """Set xform.instances_with_osm""" + help = gettext_lazy("Set xform.instances_with_osm") def handle(self, *args, **kwargs): @@ -28,9 +32,10 @@ def handle(self, *args, **kwargs): try: xform.instances_with_osm = True xform.save() + # pylint: disable=broad-except except Exception as e: self.stderr.write(e) else: count += 1 - self.stdout.write("%d of %d forms processed." % (count, total)) + self.stdout.write(f"{count} of {total} forms processed.") diff --git a/onadata/apps/logger/management/commands/sync_deleted_instances_fix.py b/onadata/apps/logger/management/commands/sync_deleted_instances_fix.py index f80f9682be..5a52f4800e 100644 --- a/onadata/apps/logger/management/commands/sync_deleted_instances_fix.py +++ b/onadata/apps/logger/management/commands/sync_deleted_instances_fix.py @@ -1,5 +1,8 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 fileencoding=utf-8 +""" +Fixes deleted instances by syncing deleted items from mongo. +""" import json from django.conf import settings @@ -12,9 +15,11 @@ class Command(BaseCommand): - help = gettext_lazy( - "Fixes deleted instances by syncing " "deleted items from mongo." - ) + """ + Fixes deleted instances by syncing deleted items from mongo. + """ + + help = gettext_lazy("Fixes deleted instances by syncing deleted items from mongo.") def handle(self, *args, **kwargs): # Reset all sql deletes to None diff --git a/onadata/apps/logger/management/commands/transferproject.py b/onadata/apps/logger/management/commands/transferproject.py index 1f8548237a..5dda3ba081 100644 --- a/onadata/apps/logger/management/commands/transferproject.py +++ b/onadata/apps/logger/management/commands/transferproject.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Functionality to transfer a project from one owner to another.""" from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand @@ -52,13 +53,14 @@ def add_arguments(self, parser): " transferred. If not, do not include the argument", ) - def get_user(self, username): # pylint: disable=C0111 + def get_user(self, username): + """Return user object with the given username.""" user_model = get_user_model() user = None try: user = user_model.objects.get(username=username) except user_model.DoesNotExist: - self.errors.append("User {0} does not exist".format(username)) + self.errors.append(f"User {username} does not exist") return user def update_xform_with_new_user(self, project, user): diff --git a/onadata/apps/logger/management/commands/update_moved_forms.py b/onadata/apps/logger/management/commands/update_moved_forms.py index 666c76c251..c2289a8a4b 100644 --- a/onadata/apps/logger/management/commands/update_moved_forms.py +++ b/onadata/apps/logger/management/commands/update_moved_forms.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Ensures all the forms are owned by the project owner +""" from django.core.management import BaseCommand from django.utils.translation import gettext_lazy @@ -6,7 +10,11 @@ class Command(BaseCommand): - help = gettext_lazy("Ensures all the forms are owned by the project" " owner") + """ + Ensures all the forms are owned by the project owner + """ + + help = gettext_lazy("Ensures all the forms are owned by the project owner") def handle(self, *args, **kwargs): self.stdout.write("Updating forms owner", ending="\n") @@ -16,16 +24,12 @@ def handle(self, *args, **kwargs): try: if xform.user != project.organization: self.stdout.write( - "Processing: {} - {}".format( - xform.id_string, xform.user.username - ) + f"Processing: {xform.id_string} - {xform.user.username}" ) xform.user = project.organization xform.save() + # pylint: disable=broad-except except Exception: self.stdout.write( - "Error processing: {} - {}".format( - xform.id_string, xform.user.username - ) + f"Error processing: {xform.id_string} - {xform.user.username}" ) - pass diff --git a/onadata/apps/logger/management/commands/update_xform_uuids.py b/onadata/apps/logger/management/commands/update_xform_uuids.py index ecc0ed9c8c..929f55f1c7 100644 --- a/onadata/apps/logger/management/commands/update_xform_uuids.py +++ b/onadata/apps/logger/management/commands/update_xform_uuids.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 fileencoding=utf-8 -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ update_xform_uuids command - Set uuid from a CSV file """ diff --git a/onadata/apps/logger/models/__init__.py b/onadata/apps/logger/models/__init__.py index 7d85c6d6be..8ddb3ffa20 100644 --- a/onadata/apps/logger/models/__init__.py +++ b/onadata/apps/logger/models/__init__.py @@ -10,9 +10,9 @@ from onadata.apps.logger.models.open_data import OpenData # noqa from onadata.apps.logger.models.osmdata import OsmData # noqa from onadata.apps.logger.models.project import Project # noqa +from onadata.apps.logger.models.submission_review import SubmissionReview # noqa from onadata.apps.logger.models.survey_type import SurveyType # noqa from onadata.apps.logger.models.widget import Widget # noqa from onadata.apps.logger.models.xform import XForm # noqa -from onadata.apps.logger.models.submission_review import SubmissionReview # noqa -from onadata.apps.logger.xform_instance_parser import InstanceParseError # noqa from onadata.apps.logger.models.xform_version import XFormVersion # noqa +from onadata.apps.logger.xform_instance_parser import InstanceParseError # noqa diff --git a/onadata/apps/logger/models/instance.py b/onadata/apps/logger/models/instance.py index dc29cb6566..a8ab841780 100644 --- a/onadata/apps/logger/models/instance.py +++ b/onadata/apps/logger/models/instance.py @@ -29,7 +29,7 @@ clean_and_parse_xml, get_uuid_from_xml, ) -from onadata.celery import app +from onadata.celeryapp import app from onadata.libs.data.query import get_numeric_fields from onadata.libs.utils.cache_tools import ( DATAVIEW_COUNT, @@ -447,7 +447,8 @@ def get_full_dict(self, load_existing=True): if review.get_note_text(): doc[REVIEW_COMMENT] = review.get_note_text() - # pylint: disable=attribute-defined-outside-init,access-member-before-definition + # pylint: disable=attribute-defined-outside-init + # pylint: disable=access-member-before-definition if not self.date_created: self.date_created = submission_time() @@ -487,7 +488,8 @@ def _set_survey_type(self): ) def _set_uuid(self): - # pylint: disable=no-member,attribute-defined-outside-init,access-member-before-definition + # pylint: disable=no-member,attribute-defined-outside-init + # pylint: disable=access-member-before-definition if self.xml and not self.uuid: # pylint: disable=no-member uuid = get_uuid_from_xml(self.xml) diff --git a/onadata/apps/logger/models/open_data.py b/onadata/apps/logger/models/open_data.py index 8c57f717c1..d24a99e9d6 100644 --- a/onadata/apps/logger/models/open_data.py +++ b/onadata/apps/logger/models/open_data.py @@ -28,7 +28,7 @@ class OpenData(models.Model): date_modified = models.DateTimeField(auto_now=True) def __str__(self): - return getattr(self, "name", "") + return str(getattr(self, "name", "")) class Meta: app_label = "logger" diff --git a/onadata/apps/logger/models/project.py b/onadata/apps/logger/models/project.py index 3dc7703c64..7ec858086b 100644 --- a/onadata/apps/logger/models/project.py +++ b/onadata/apps/logger/models/project.py @@ -2,6 +2,7 @@ """ Project model class """ +from django.apps import apps from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError @@ -28,9 +29,9 @@ class PrefetchManager(models.Manager): def get_queryset(self): """Return a queryset with the XForm, Team, tags, and other related relations prefetched.""" - # pylint: disable=import-outside-toplevel - from onadata.apps.api.models.team import Team - from onadata.apps.logger.models.xform import XForm + # pylint: disable=invalid-name + Team = apps.get_model("api", "Team") # noqa N806 + XForm = apps.get_model("logger", "XForm") # noqa N806 # pylint: disable=no-member return ( @@ -137,7 +138,9 @@ def __str__(self): return f"{self.organization}|{self.name}" def clean(self): - """Raises a validation error if a project with same name and organization exists.""" + """ + Raises a validation error if a project with same name and organization exists. + """ query_set = Project.objects.exclude(pk=self.pk).filter( name__iexact=self.name, organization=self.organization ) diff --git a/onadata/apps/logger/models/submission_review.py b/onadata/apps/logger/models/submission_review.py index 7a8205130e..2c40b1ee85 100644 --- a/onadata/apps/logger/models/submission_review.py +++ b/onadata/apps/logger/models/submission_review.py @@ -11,6 +11,7 @@ from django.utils.translation import gettext_lazy as _ +# pylint: disable=unused-argument def update_instance_json_on_save(sender, instance, **kwargs): """ Signal handler to update Instance Json with the submission review on save diff --git a/onadata/apps/logger/models/survey_type.py b/onadata/apps/logger/models/survey_type.py index 5a25fe0547..2c663e4aa4 100644 --- a/onadata/apps/logger/models/survey_type.py +++ b/onadata/apps/logger/models/survey_type.py @@ -16,4 +16,4 @@ class Meta: app_label = "logger" def __str__(self): - return "SurveyType: %s" % self.slug + return f"SurveyType: {self.slug}" diff --git a/onadata/apps/logger/models/widget.py b/onadata/apps/logger/models/widget.py index 44e878afac..eb5da5d4e1 100644 --- a/onadata/apps/logger/models/widget.py +++ b/onadata/apps/logger/models/widget.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Widget class module. +""" from builtins import str as text from django.db.models import JSONField @@ -24,6 +28,10 @@ class Widget(OrderedModel): + """ + Widget class - used for storing chart visual information. + """ + CHARTS = "charts" # Other widgets types to be added later @@ -57,10 +65,12 @@ def save(self, *args, **kwargs): if not self.key: self.key = get_uuid() - super(Widget, self).save(*args, **kwargs) + super().save(*args, **kwargs) + # pylint: disable=too-many-locals,too-many-branches @classmethod def query_data(cls, widget): + """Queries and returns chart information with the data for the chart.""" # get the columns needed column = widget.column group_by = widget.group_by if widget.group_by else None @@ -84,24 +94,24 @@ def query_data(cls, widget): field_label = get_field_label(field) columns = [ - SimpleField(field="json->>'%s'" % text(column), alias="{}".format(column)), - CountField(field="json->>'%s'" % text(column), alias="count"), + SimpleField(field=f"json->>'{text(column)}'", alias=f"{column}"), + CountField(field=f"json->>'{text(column)}'", alias="count"), ] if group_by: if field_type in NUMERIC_LIST: column_field = SimpleField( - field="json->>'%s'" % text(column), cast="float", alias=column + field=f"json->>'{text(column)}'", cast="float", alias=column ) else: column_field = SimpleField( - field="json->>'%s'" % text(column), alias=column + field=f"json->>'{text(column)}'", alias=column ) # build inner query inner_query_columns = [ column_field, - SimpleField(field="json->>'%s'" % text(group_by), alias=group_by), + SimpleField(field=f"json->>'{text(group_by)}'", alias=group_by), SimpleField(field="xform_id"), SimpleField(field="deleted_at"), ] @@ -110,14 +120,14 @@ def query_data(cls, widget): # build group-by query if field_type in NUMERIC_LIST: columns = [ - SimpleField(field=group_by, alias="%s" % group_by), + SimpleField(field=group_by, alias=f"{group_by}"), SumField(field=column, alias="sum"), AvgField(field=column, alias="mean"), ] elif field_type == SELECT_ONE: columns = [ - SimpleField(field=column, alias="%s" % column), - SimpleField(field=group_by, alias="%s" % group_by), + SimpleField(field=column, alias=f"{column}"), + SimpleField(field=group_by, alias=f"{group_by}"), CountField(field="*", alias="count"), ] @@ -138,7 +148,7 @@ def query_data(cls, widget): .from_table(Instance, columns) .where(xform_id=xform.pk, deleted_at=None) ) - query.group_by("json->>'%s'" % text(column)) + query.group_by(f"json->>'{text(column)}'") # run query records = query.select() diff --git a/onadata/apps/logger/models/xform.py b/onadata/apps/logger/models/xform.py index a605382ef5..3193cf9311 100644 --- a/onadata/apps/logger/models/xform.py +++ b/onadata/apps/logger/models/xform.py @@ -436,7 +436,6 @@ def flatten(elem, items=None): return flatten(element) - # pylint: disable=no-self-use def get_choice_label(self, field, choice_value, lang="English"): """Returns a choice's label for the given ``field`` and ``choice_value``.""" choices = [choice for choice in field.children if choice.name == choice_value] @@ -938,14 +937,14 @@ def _set_title(self): self.set_hash() if contains_xml_invalid_char(title_xml): raise XLSFormError( - _("Title shouldn't have any invalid xml " "characters ('>' '&' '<')") + _("Title shouldn't have any invalid xml characters ('>' '&' '<')") ) # Capture urls within form title if re.search( - r"^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$", + r"^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$", # noqa self.title, - ): # noqa + ): raise XLSFormError(_("Invalid title value; value shouldn't match a URL")) self.title = title_xml @@ -1049,8 +1048,8 @@ def save(self, *args, **kwargs): # noqa: MC0001 ): raise XLSFormError( _( - f"The XForm id_string provided exceeds {self.MAX_ID_LENGTH} characters." - f' Please change the "id_string" or "form_id" values' + f"The XForm id_string provided exceeds {self.MAX_ID_LENGTH}" + f' characters. Please change the "id_string" or "form_id" values' f"in settings sheet or reduce the file name if you do" f" not have a settings sheets." ) @@ -1059,7 +1058,7 @@ def save(self, *args, **kwargs): # noqa: MC0001 is_version_available = self.version is not None if is_version_available and contains_xml_invalid_char(self.version): raise XLSFormError( - _("Version shouldn't have any invalid " "characters ('>' '&' '<')") + _("Version shouldn't have any invalid characters ('>' '&' '<')") ) self.description = conditional_escape(self.description) diff --git a/onadata/apps/logger/models/xform_version.py b/onadata/apps/logger/models/xform_version.py index 277a9bd0c7..d85e230354 100644 --- a/onadata/apps/logger/models/xform_version.py +++ b/onadata/apps/logger/models/xform_version.py @@ -1,7 +1,12 @@ +# -*- coding: utf-8 -*- """ Module containing the XForm Version model """ from django.db import models +from django.contrib.auth import get_user_model + + +User = get_user_model() class XFormVersion(models.Model): @@ -21,7 +26,7 @@ class XFormVersion(models.Model): version = models.CharField(max_length=100) date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True) - created_by = models.ForeignKey("auth.User", on_delete=models.SET_NULL, null=True) + created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) xml = models.TextField() json = models.TextField() diff --git a/onadata/apps/logger/tests/test_encrypted_submissions.py b/onadata/apps/logger/tests/test_encrypted_submissions.py index 80d9f7809a..262eb7ed58 100644 --- a/onadata/apps/logger/tests/test_encrypted_submissions.py +++ b/onadata/apps/logger/tests/test_encrypted_submissions.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Test encrypted form submissions. """ diff --git a/onadata/apps/logger/views.py b/onadata/apps/logger/views.py index 64f0672cf6..5a3e6aa844 100644 --- a/onadata/apps/logger/views.py +++ b/onadata/apps/logger/views.py @@ -460,7 +460,7 @@ def download_xlsform(request, username, id_string): messages.add_message( request, messages.WARNING, - _("No XLS file for your form " "%(id)s") % {"id": id_string}, + _("No XLS file for your form %(id)s") % {"id": id_string}, ) return HttpResponseRedirect(f"/{username}") @@ -723,7 +723,7 @@ def view_download_submission(request, username): last_index = form_id.find("[") id_string = form_id[0:last_index] form_id_parts = form_id.split("/") - if form_id_parts.__len__() < 2: + if len(form_id_parts) < 2: return HttpResponseBadRequest() uuid = _extract_uuid(form_id_parts[1]) diff --git a/onadata/apps/logger/xform_instance_parser.py b/onadata/apps/logger/xform_instance_parser.py index bf2e160049..b782f4e257 100644 --- a/onadata/apps/logger/xform_instance_parser.py +++ b/onadata/apps/logger/xform_instance_parser.py @@ -392,7 +392,7 @@ def _set_attributes(self): if key in self._attributes: logger = logging.getLogger("console_logger") logger.debug( - "Skipping duplicate attribute: %s" " with value %s", key, value + "Skipping duplicate attribute: %s with value %s", key, value ) logger.debug(str(all_attributes)) else: diff --git a/onadata/apps/main/backends.py b/onadata/apps/main/backends.py index 3a11b237af..bdd607cf83 100644 --- a/onadata/apps/main/backends.py +++ b/onadata/apps/main/backends.py @@ -1,15 +1,29 @@ -from django.contrib.auth.models import User +# -*- coding: utf-8 -*- +""" +A custom ModelBackend class module. +""" +from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend as DjangoModelBackend from django.db.models import Q +User = get_user_model() + class ModelBackend(DjangoModelBackend): - def authenticate(self, request=None, username=None, password=None): + """ + A custom ModelBackend class + """ + + def authenticate(self, request, username=None, password=None, **kwargs): """ Username is case insensitive. Supports using email in place of username """ + if username is None or password is None: + return None + user = User.objects.filter( - Q(username__iexact=username) | Q(email__iexact=username)).first() + Q(username__iexact=username) | Q(email__iexact=username) + ).first() if user and user.check_password(password): return user diff --git a/onadata/apps/main/context_processors.py b/onadata/apps/main/context_processors.py index 9250a629bd..96e115c8c2 100644 --- a/onadata/apps/main/context_processors.py +++ b/onadata/apps/main/context_processors.py @@ -1,24 +1,30 @@ +# -*- coding: utf-8 -*- +""" +google_analytics and site_name context processor functions. +""" from django.conf import settings from django.contrib.sites.models import Site def google_analytics(request): - ga_pid = getattr(settings, 'GOOGLE_ANALYTICS_PROPERTY_ID', False) - ga_domain = getattr(settings, 'GOOGLE_ANALYTICS_DOMAIN', False) - ga_site_verification = getattr(settings, 'GOOGLE_SITE_VERIFICATION', False) + """Returns Google Analytics property id, domain and site verification settings.""" + ga_pid = getattr(settings, "GOOGLE_ANALYTICS_PROPERTY_ID", False) + ga_domain = getattr(settings, "GOOGLE_ANALYTICS_DOMAIN", False) + ga_site_verification = getattr(settings, "GOOGLE_SITE_VERIFICATION", False) return { - 'GOOGLE_ANALYTICS_PROPERTY_ID': ga_pid, - 'GOOGLE_ANALYTICS_DOMAIN': ga_domain, - 'GOOGLE_SITE_VERIFICATION': ga_site_verification + "GOOGLE_ANALYTICS_PROPERTY_ID": ga_pid, + "GOOGLE_ANALYTICS_DOMAIN": ga_domain, + "GOOGLE_SITE_VERIFICATION": ga_site_verification, } def site_name(request): - site_id = getattr(settings, 'SITE_ID', None) + """Returns the SITE_NAME/""" + site_id = getattr(settings, "SITE_ID", None) try: site = Site.objects.get(pk=site_id) except Site.DoesNotExist: - site_name = 'example.org' + name = "example.org" else: - site_name = site.name - return {'SITE_NAME': site_name} + name = site.name + return {"SITE_NAME": name} diff --git a/onadata/apps/main/forms.py b/onadata/apps/main/forms.py index ae0a82286e..3b7b7f9b58 100644 --- a/onadata/apps/main/forms.py +++ b/onadata/apps/main/forms.py @@ -1,13 +1,11 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ forms module. """ import os import random import re -from six.moves.urllib.parse import urlparse -import requests from django import forms from django.conf import settings from django.contrib.auth import get_user_model @@ -17,7 +15,10 @@ from django.forms import ModelForm from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy + +import requests from registration.forms import RegistrationFormUniqueEmail +from six.moves.urllib.parse import urlparse # pylint: disable=ungrouped-imports from onadata.apps.logger.models import Project @@ -475,7 +476,7 @@ def clean_sms_id_string(self): if not re.match(r"^[a-z0-9\_\-]+$", sms_id_string): raise forms.ValidationError( - "id_string can only contain alphanum" " characters" + "id_string can only contain alphanum characters" ) return sms_id_string diff --git a/onadata/apps/main/management/commands/create_enketo_express_urls.py b/onadata/apps/main/management/commands/create_enketo_express_urls.py index c9658fbd0d..f0d677e4ce 100644 --- a/onadata/apps/main/management/commands/create_enketo_express_urls.py +++ b/onadata/apps/main/management/commands/create_enketo_express_urls.py @@ -1,96 +1,105 @@ +# -*- coding: utf-8 -*- +""" +Creates enketo url including preview +""" from django.core.management.base import BaseCommand, CommandError from django.http import HttpRequest from django.utils.translation import gettext_lazy from onadata.apps.logger.models import XForm from onadata.libs.utils.model_tools import queryset_iterator -from onadata.libs.utils.viewer_tools import ( - get_enketo_urls, get_form_url) +from onadata.libs.utils.viewer_tools import get_enketo_urls, get_form_url class Command(BaseCommand): + """Create enketo url including preview""" + help = gettext_lazy("Create enketo url including preview") def add_arguments(self, parser): parser.add_argument( - "-n", "--server_name", dest="server_name", default="api.ona.io") - parser.add_argument( - "-p", "--server_port", dest="server_port", default="80") - parser.add_argument( - "-r", "--protocol", dest="protocol", default="https") + "-n", "--server_name", dest="server_name", default="api.ona.io" + ) + parser.add_argument("-p", "--server_port", dest="server_port", default="80") + parser.add_argument("-r", "--protocol", dest="protocol", default="https") parser.add_argument("-u", "--username", dest="username", default=None) + parser.add_argument("-x", "--id_string", dest="id_string", default=None) parser.add_argument( - "-x", "--id_string", dest="id_string", default=None) - parser.add_argument( - "-c", "--generate_consistent_urls", - dest="generate_consistent_urls", default=True) + "-c", + "--generate_consistent_urls", + dest="generate_consistent_urls", + default=True, + ) + # pylint: disable=too-many-locals,too-many-statements def handle(self, *args, **options): request = HttpRequest() - server_name = options.get('server_name') - server_port = options.get('server_port') - protocol = options.get('protocol') - username = options.get('username') - id_string = options.get('id_string') - generate_consistent_urls = options.get('generate_consistent_urls') + server_name = options.get("server_name") + server_port = options.get("server_port") + protocol = options.get("protocol") + username = options.get("username") + id_string = options.get("id_string") + generate_consistent_urls = options.get("generate_consistent_urls") if not server_name or not server_port or not protocol: raise CommandError( - 'please provide a server_name, a server_port and a protocol') + "please provide a server_name, a server_port and a protocol" + ) - if protocol not in ['http', 'https']: - raise CommandError('protocol provided is not valid') + if protocol not in ["http", "https"]: + raise CommandError("protocol provided is not valid") # required for generation of enketo url - request.META['HTTP_HOST'] = '%s:%s' % (server_name, server_port)\ - if server_port != '80' else server_name + request.META["HTTP_HOST"] = ( + f"{server_name}:{server_port}" if server_port != "80" else server_name + ) # required for generation of enketo preview url - request.META['SERVER_NAME'] = server_name - request.META['SERVER_PORT'] = server_port + request.META["SERVER_NAME"] = server_name + request.META["SERVER_PORT"] = server_port if username and id_string: try: - xform = XForm.objects.get( - user__username=username, id_string=id_string) + xform = XForm.objects.get(user__username=username, id_string=id_string) form_url = get_form_url( - request, - username, - protocol=protocol, - xform_pk=xform.pk, - generate_consistent_urls=generate_consistent_urls) + request, + username, + protocol=protocol, + xform_pk=xform.pk, + generate_consistent_urls=generate_consistent_urls, + ) id_string = xform.id_string enketo_urls = get_enketo_urls(form_url, id_string) - _url = (enketo_urls.get('offline_url') or - enketo_urls.get('url')) - _preview_url = enketo_urls.get('preview_url') + _url = enketo_urls.get("offline_url") or enketo_urls.get("url") + _preview_url = enketo_urls.get("preview_url") - self.stdout.write('enketo url: %s | preview url: %s' % - (_url, _preview_url)) + self.stdout.write(f"enketo url: {_url} | preview url: {_preview_url}") self.stdout.write("enketo urls generation completed!!") except XForm.DoesNotExist: self.stdout.write( - "No xform matching the provided username and id_string") + "No xform matching the provided username and id_string" + ) elif username and id_string is None: xforms = XForm.objects.filter(user__username=username) num_of_xforms = xforms.count() if xforms: for xform in queryset_iterator(xforms): form_url = get_form_url( - request, - username, - protocol=protocol, - xform_pk=xform.pk, - generate_consistent_urls=generate_consistent_urls) + request, + username, + protocol=protocol, + xform_pk=xform.pk, + generate_consistent_urls=generate_consistent_urls, + ) id_string = xform.id_string enketo_urls = get_enketo_urls(form_url, id_string) - _url = (enketo_urls.get('offline_url') or - enketo_urls.get('url')) - _preview_url = enketo_urls.get('preview_url') + _url = enketo_urls.get("offline_url") or enketo_urls.get("url") + _preview_url = enketo_urls.get("preview_url") num_of_xforms -= 1 self.stdout.write( - 'enketo url: %s | preview url: %s | remaining: %s' % - (_url, _preview_url, num_of_xforms)) + f"enketo url: {_url} | preview url: {_preview_url}" + f" | remaining: {num_of_xforms}" + ) self.stdout.write("enketo urls generation completed!!") else: self.stdout.write("Username doesn't own any form") @@ -101,17 +110,18 @@ def handle(self, *args, **options): username = xform.user.username id_string = xform.id_string form_url = get_form_url( - request, - username, - protocol=protocol, - xform_pk=xform.pk, - generate_consistent_urls=generate_consistent_urls) + request, + username, + protocol=protocol, + xform_pk=xform.pk, + generate_consistent_urls=generate_consistent_urls, + ) enketo_urls = get_enketo_urls(form_url, id_string) - _url = (enketo_urls.get('offline_url') or - enketo_urls.get('url')) - _preview_url = enketo_urls.get('preview_url') + _url = enketo_urls.get("offline_url") or enketo_urls.get("url") + _preview_url = enketo_urls.get("preview_url") num_of_xforms -= 1 self.stdout.write( - 'enketo url: %s | preview url: %s | remaining: %s' % - (_url, _preview_url, num_of_xforms)) + f"enketo url: {_url} | preview url: {_preview_url}" + f" | remaining: {num_of_xforms}" + ) self.stdout.write("enketo urls generation completed!!") diff --git a/onadata/apps/main/management/commands/create_metadata_for_kpi_deployed_forms.py b/onadata/apps/main/management/commands/create_metadata_for_kpi_deployed_forms.py index c5e8105a29..fd5c60f7f4 100644 --- a/onadata/apps/main/management/commands/create_metadata_for_kpi_deployed_forms.py +++ b/onadata/apps/main/management/commands/create_metadata_for_kpi_deployed_forms.py @@ -1,5 +1,8 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 fileencoding=utf-8 +""" +Create metadata for kpi forms that are not editable +""" from django.core.management.base import BaseCommand from django.utils.translation import gettext_lazy @@ -9,18 +12,17 @@ class Command(BaseCommand): + """Create metadata for kpi forms that are not editable""" + help = gettext_lazy("Create metadata for kpi forms that are not editable") def handle(self, *args, **kwargs): cursor = connection.cursor() - cursor.execute( - 'SELECT uid FROM kpi_asset WHERE asset_type=%s', ['survey']) - rs = cursor.cursor.fetchall() - uids = [a[0] for a in rs] + cursor.execute("SELECT uid FROM kpi_asset WHERE asset_type=%s", ["survey"]) + results = cursor.cursor.fetchall() + uids = [a[0] for a in results] xforms = XForm.objects.filter(id_string__in=uids) - for x in xforms: - MetaData.published_by_formbuilder(x, 'True') + for xform in xforms: + MetaData.published_by_formbuilder(xform, "True") - self.stdout.write( - "Done creating published_by_formbuilder metadata!!!" - ) + self.stdout.write("Done creating published_by_formbuilder metadata!!!") diff --git a/onadata/apps/main/management/commands/export_user_emails.py b/onadata/apps/main/management/commands/export_user_emails.py index 42289e9067..546357b2fa 100644 --- a/onadata/apps/main/management/commands/export_user_emails.py +++ b/onadata/apps/main/management/commands/export_user_emails.py @@ -1,5 +1,8 @@ #!/usr/bin/env python # vim: ai ts=4 sts=4 et sw=4 fileencoding=utf-8 +""" +export_user_emails command - prints a CSV of usernames and emails. +""" from django.core.management.base import BaseCommand from django.utils.translation import gettext_lazy @@ -8,16 +11,17 @@ class Command(BaseCommand): + """Export users and emails""" + help = gettext_lazy("Export users and emails") def handle(self, *args, **kwargs): self.stdout.write( '"username","email","first_name","last_name","name","organization"' ) - for p in queryset_iterator(UserProfile.objects.all()): + for profile in queryset_iterator(UserProfile.objects.all()): self.stdout.write( - u'"{}","{}","{}","{}","{}","{}"'.format( - p.user.username, p.user.email, p.user.first_name, - p.user.last_name, p.name, p.organization - ) + f'"{profile.user.username}","{profile.user.email}",' + f'"{profile.user.first_name}","{profile.user.last_name}",' + f'"{profile.name}","{profile.organization}"' ) diff --git a/onadata/apps/main/management/commands/get_accounts_with_duplicate_id_strings.py b/onadata/apps/main/management/commands/get_accounts_with_duplicate_id_strings.py index b13b8cdd2e..7a932f1e63 100644 --- a/onadata/apps/main/management/commands/get_accounts_with_duplicate_id_strings.py +++ b/onadata/apps/main/management/commands/get_accounts_with_duplicate_id_strings.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ get_accounts_with_duplicate_id_strings - Retrieves accounts with duplicate id_strings """ diff --git a/onadata/apps/main/management/commands/mailer.py b/onadata/apps/main/management/commands/mailer.py index 014c07d1e3..af3237abd8 100644 --- a/onadata/apps/main/management/commands/mailer.py +++ b/onadata/apps/main/management/commands/mailer.py @@ -1,23 +1,32 @@ +# -*- coding: utf-8 -*- +""" +mailer command - sends emails to all users. +""" +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import User from django.template.loader import get_template -from django.utils.translation import gettext as _, gettext_lazy +from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy from templated_email import send_templated_mail +User = get_user_model() + class Command(BaseCommand): + """Send an email to all onadata users""" + help = gettext_lazy("Send an email to all onadata users") def add_arguments(self, parser): parser.add_argument("-m", "--message", dest="message", default=False) def handle(self, *args, **options): - message = options.get('message') - verbosity = options.get('verbosity') - get_template('templated_email/notice.email') + message = options.get("message") + verbosity = options.get("verbosity") + get_template("templated_email/notice.email") if not message: - raise CommandError(_('message must be included in options')) + raise CommandError(_("message must be included in options")) # get all users users = User.objects.all() for user in users: @@ -25,16 +34,18 @@ def handle(self, *args, **options): if not name or len(name) == 0: name = user.email if verbosity: - self.stdout.write(_( - 'Emailing name: %(name)s, email: %(email)s') - % {'name': name, 'email': user.email}) + self.stdout.write( + _("Emailing name: %(name)s, email: %(email)s") + % {"name": name, "email": user.email} + ) # send each email separately so users cannot see eachother send_templated_mail( - template_name='notice', - from_email='noreply@ona.io', + template_name="notice", + from_email="noreply@ona.io", recipient_list=[user.email], context={ - 'username': user.username, - 'full_name': name, - 'message': message - }, ) + "username": user.username, + "full_name": name, + "message": message, + }, + ) diff --git a/onadata/apps/main/registration_views.py b/onadata/apps/main/registration_views.py index a18f3987b8..df81ffd30b 100644 --- a/onadata/apps/main/registration_views.py +++ b/onadata/apps/main/registration_views.py @@ -1,9 +1,15 @@ +# -*- coding: utf-8 -*- +""" +FHRegistrationView class module. +""" from registration.backends.default.views import RegistrationView class FHRegistrationView(RegistrationView): + """A custom RegistrationView.""" + def register(self, form): - new_user = super(FHRegistrationView, self).register(form) + new_user = super().register(form) form.save_user_profile(new_user) return new_user diff --git a/onadata/apps/main/signals.py b/onadata/apps/main/signals.py index 9e9b88883b..824d03b7d6 100644 --- a/onadata/apps/main/signals.py +++ b/onadata/apps/main/signals.py @@ -1,60 +1,64 @@ +# -*- coding: utf-8 -*- +""" +signal module. +""" from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.template.loader import render_to_string from onadata.libs.utils.email import send_generic_email +User = get_user_model() + + +# pylint: disable=unused-argument def set_api_permissions(sender, instance=None, created=False, **kwargs): + """Sets API permissions for a user.""" + # pylint: disable=import-outside-toplevel from onadata.libs.utils.user_auth import set_api_permissions_for_user + if created: set_api_permissions_for_user(instance) -def send_inactive_user_email( - sender, instance=None, created=False, **kwargs): +def send_inactive_user_email(sender, instance=None, created=False, **kwargs): + """Sends email to inactive user upon account creation.""" if (created and not instance.is_active) and getattr( - settings, "ENABLE_ACCOUNT_ACTIVATION_EMAILS", False): - deployment_name = getattr(settings, 'DEPLOYMENT_NAME', 'Ona') - context = { - 'username': instance.username, - 'deployment_name': deployment_name - } - email = render_to_string( - 'registration/inactive_account_email.txt', context) + settings, "ENABLE_ACCOUNT_ACTIVATION_EMAILS", False + ): + deployment_name = getattr(settings, "DEPLOYMENT_NAME", "Ona") + context = {"username": instance.username, "deployment_name": deployment_name} + email = render_to_string("registration/inactive_account_email.txt", context) if instance.email: send_generic_email( instance.email, email, - f'{deployment_name} account created - Pending activation') + f"{deployment_name} account created - Pending activation", + ) -def send_activation_email( - sender, instance=None, **kwargs -): +def send_activation_email(sender, instance=None, **kwargs): + """Sends activation email to user.""" instance_id = instance.id - if instance_id and getattr( - settings, "ENABLE_ACCOUNT_ACTIVATION_EMAILS", False): + if instance_id and getattr(settings, "ENABLE_ACCOUNT_ACTIVATION_EMAILS", False): try: - user = User.objects.using('default').get( - id=instance_id) + user = User.objects.using("default").get(id=instance_id) except User.DoesNotExist: pass else: if not user.is_active and instance.is_active: - deployment_name = getattr(settings, 'DEPLOYMENT_NAME', 'Ona') + deployment_name = getattr(settings, "DEPLOYMENT_NAME", "Ona") context = { - 'username': instance.username, - 'deployment_name': deployment_name + "username": instance.username, + "deployment_name": deployment_name, } email = render_to_string( - 'registration/activated_account_email.txt', context + "registration/activated_account_email.txt", context ) if instance.email: send_generic_email( - instance.email, - email, - f'{deployment_name} account activated' + instance.email, email, f"{deployment_name} account activated" ) diff --git a/onadata/apps/main/tests/test_base.py b/onadata/apps/main/tests/test_base.py index deee090673..34f6015b87 100644 --- a/onadata/apps/main/tests/test_base.py +++ b/onadata/apps/main/tests/test_base.py @@ -9,6 +9,7 @@ import os import re import socket +import warnings from io import StringIO from tempfile import NamedTemporaryFile @@ -19,12 +20,11 @@ from django.test.client import Client from django.utils import timezone -from six.moves.urllib.error import URLError -from six.moves.urllib.request import urlopen - from django_digest.test import Client as DigestClient from django_digest.test import DigestAuth from rest_framework.test import APIRequestFactory +from six.moves.urllib.error import URLError +from six.moves.urllib.request import urlopen from onadata.apps.api.viewsets.xform_viewset import XFormViewSet from onadata.apps.logger.models import Attachment, Instance, XForm @@ -41,6 +41,8 @@ # pylint: disable=invalid-name User = get_user_model() +warnings.simplefilter("ignore") + # pylint: disable=too-many-instance-attributes class TestBase(PyxformMarkdown, TransactionTestCase): diff --git a/onadata/apps/main/tests/test_style.py b/onadata/apps/main/tests/test_style.py deleted file mode 100644 index 3d23a74ceb..0000000000 --- a/onadata/apps/main/tests/test_style.py +++ /dev/null @@ -1,12 +0,0 @@ -from subprocess import call - -from django.test import TestCase - - -class TestStyle(TestCase): - - def test_flake8(self): - result = call( - ['flake8', '--exclude=migrations,src,settings', 'onadata'] - ) - self.assertEqual(result, 0, "Code is not flake8.") diff --git a/onadata/apps/main/urls.py b/onadata/apps/main/urls.py index d26fa5c6c8..79c351c75e 100644 --- a/onadata/apps/main/urls.py +++ b/onadata/apps/main/urls.py @@ -171,7 +171,7 @@ ), re_path( # pylint: disable=line-too-long - r"^(?P[^/]+)/forms/(?P[^/]+)/formid-media/(?P\d+)", + r"^(?P[^/]+)/forms/(?P[^/]+)/formid-media/(?P\d+)", # noqa main_views.download_media_data, # noqa name="download-media-data", ), diff --git a/onadata/apps/main/views.py b/onadata/apps/main/views.py index 86735bf757..3c6812177b 100644 --- a/onadata/apps/main/views.py +++ b/onadata/apps/main/views.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- # pylint: disable=too-many-lines """ Main views. @@ -326,7 +326,7 @@ def set_form(): try: resp = render(request, "profile.html", data) except XLSFormError as e: - resp = HttpResponseBadRequest(e.__str__()) + resp = HttpResponseBadRequest(str(e)) return resp @@ -514,7 +514,7 @@ def show(request, username=None, id_string=None, uuid=None): data["mapbox_layer"] = MetaData.mapbox_layer_upload(xform) data["external_export"] = MetaData.external_export(xform) except XLSFormError as e: - return HttpResponseBadRequest(e.__str__()) + return HttpResponseBadRequest(str(e)) if is_xform_owner: set_xform_owner_data(data, xform, request, username, id_string) @@ -611,7 +611,7 @@ def api(request, username=None, id_string=None): # noqa C901 cursor = query_data(**args) except (ValueError, TypeError) as e: - return HttpResponseBadRequest(conditional_escape(e.__str__())) + return HttpResponseBadRequest(conditional_escape(str(e))) if "callback" in request.GET and request.GET.get("callback") != "": callback = request.GET.get("callback") @@ -1456,7 +1456,7 @@ def stringify_unknowns(obj): query_args["count"] = int(request.GET.get("count")) > 0 cursor = AuditLog.query_data(**query_args) except ValueError as e: - return HttpResponseBadRequest(e.__str__()) + return HttpResponseBadRequest(str(e)) records = list(record for record in cursor) if "callback" in request.GET and request.GET.get("callback") != "": diff --git a/onadata/apps/messaging/apps.py b/onadata/apps/messaging/apps.py index 1ee410eb62..3016851258 100644 --- a/onadata/apps/messaging/apps.py +++ b/onadata/apps/messaging/apps.py @@ -11,17 +11,18 @@ class MessagingConfig(AppConfig): """ Messaging AppsConfig class. """ - name = 'onadata.apps.messaging' - verbose_name = 'Messaging' + + name = "onadata.apps.messaging" + verbose_name = "Messaging" def ready(self): - from onadata.apps.messaging import signals # noqa pylint: disable=W0612 + # pylint: disable=import-outside-toplevel,unused-import + from onadata.apps.messaging import signals # noqa # this needs to be imported inline because otherwise we get # django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet. from actstream import registry - registry.register(apps.get_model(model_name='User', app_label='auth')) - registry.register( - apps.get_model(model_name='XForm', app_label='logger')) - registry.register( - apps.get_model(model_name='Project', app_label='logger')) + + registry.register(apps.get_model(model_name="User", app_label="auth")) + registry.register(apps.get_model(model_name="XForm", app_label="logger")) + registry.register(apps.get_model(model_name="Project", app_label="logger")) diff --git a/onadata/apps/messaging/backends/base.py b/onadata/apps/messaging/backends/base.py index 932b5ddab7..782c62f8fd 100644 --- a/onadata/apps/messaging/backends/base.py +++ b/onadata/apps/messaging/backends/base.py @@ -23,7 +23,7 @@ def call_backend(backend, instance_id, backend_options=None): backend_class(options=backend_options).send(instance) -class BaseBackend(object): # pylint: disable=too-few-public-methods +class BaseBackend: # pylint: disable=too-few-public-methods """ Base class for notification backends """ diff --git a/onadata/apps/messaging/backends/mqtt.py b/onadata/apps/messaging/backends/mqtt.py index e8711052bc..35c0ab12ce 100644 --- a/onadata/apps/messaging/backends/mqtt.py +++ b/onadata/apps/messaging/backends/mqtt.py @@ -7,14 +7,19 @@ import json import ssl -import paho.mqtt.publish as publish from django.conf import settings +from paho.mqtt import publish + from onadata.apps.logger.models import XForm from onadata.apps.messaging.backends.base import BaseBackend -from onadata.apps.messaging.constants import MESSAGE -from onadata.apps.messaging.constants import PROJECT, USER, XFORM, \ - VERB_TOPIC_DICT +from onadata.apps.messaging.constants import ( + MESSAGE, + PROJECT, + USER, + VERB_TOPIC_DICT, + XFORM, +) def get_target_metadata(target_obj): @@ -24,12 +29,12 @@ def get_target_metadata(target_obj): target_obj_type = target_obj._meta.model_name metadata = dict(id=target_obj.pk) if target_obj_type == PROJECT: - metadata['name'] = target_obj.name + metadata["name"] = target_obj.name elif target_obj_type == XFORM: - metadata['name'] = target_obj.title - metadata['form_id'] = target_obj.id_string + metadata["name"] = target_obj.title + metadata["form_id"] = target_obj.id_string elif target_obj_type == USER: - metadata['name'] = target_obj.get_full_name() + metadata["name"] = target_obj.get_full_name() return metadata @@ -37,8 +42,9 @@ def get_payload(instance, verbose_payload: bool = False): """ Constructs the message payload """ - full_message_payload = getattr( - settings, 'FULL_MESSAGE_PAYLOAD', False) or verbose_payload + full_message_payload = ( + getattr(settings, "FULL_MESSAGE_PAYLOAD", False) or verbose_payload + ) try: description = json.loads(instance.description) except json.JSONDecodeError: @@ -50,24 +56,24 @@ def get_payload(instance, verbose_payload: bool = False): "verb": instance.verb, "message": description, "user": instance.actor.username, - "timestamp": instance.timestamp.isoformat() + "timestamp": instance.timestamp.isoformat(), } else: payload = { - 'id': instance.id, - 'time': instance.timestamp.isoformat(), - 'payload': { - 'author': { - 'username': instance.actor.username, - 'real_name': instance.actor.get_full_name() + "id": instance.id, + "time": instance.timestamp.isoformat(), + "payload": { + "author": { + "username": instance.actor.username, + "real_name": instance.actor.get_full_name(), }, - 'context': { - 'type': instance.target._meta.model_name, - 'metadata': get_target_metadata(instance.target), - 'verb': instance.verb + "context": { + "type": instance.target._meta.model_name, + "metadata": get_target_metadata(instance.target), + "verb": instance.verb, }, - 'message': description - } + "message": description, + }, } return json.dumps(payload) @@ -79,30 +85,32 @@ class MQTTBackend(BaseBackend): """ def __init__(self, options=None): - super(MQTTBackend, self).__init__() + super().__init__() if not options: raise Exception("MQTT Backend expects configuration options.") - self.host = options.get('HOST') + self.host = options.get("HOST") if not self.host: raise Exception("An MQTT host is required.") - self.port = options.get('PORT') + self.port = options.get("PORT") self.cert_info = None - secure = options.get('SECURE', False) + secure = options.get("SECURE", False) if secure: - if options.get('CA_CERT_FILE') is None: - raise Exception("The Certificate Authority certificate file " - "is required.") + if options.get("CA_CERT_FILE") is None: + raise Exception( + "The Certificate Authority certificate file is required." + ) self.cert_info = dict( - ca_certs=options.get('CA_CERT_FILE'), - certfile=options.get('CERT_FILE'), - keyfile=options.get('KEY_FILE'), + ca_certs=options.get("CA_CERT_FILE"), + certfile=options.get("CERT_FILE"), + keyfile=options.get("KEY_FILE"), tls_version=ssl.PROTOCOL_TLSv1_2, - cert_reqs=ssl.CERT_NONE) + cert_reqs=ssl.CERT_NONE, + ) - self.qos = options.get('QOS', 0) - self.retain = options.get('RETAIN', False) - self.topic_base = options.get('TOPIC_BASE', 'onadata') + self.qos = options.get("QOS", 0) + self.retain = options.get("RETAIN", False) + self.topic_base = options.get("TOPIC_BASE", "onadata") def get_topic(self, instance): """ @@ -114,26 +122,28 @@ def get_topic(self, instance): /onadata/users/[pk or uuid]/[verb]/messages/publish """ kwargs = { - 'target_id': instance.target_object_id, - 'target_name': instance.target._meta.model_name, - 'topic_base': self.topic_base, - 'verb': instance.verb + "target_id": instance.target_object_id, + "target_name": instance.target._meta.model_name, + "topic_base": self.topic_base, + "verb": instance.verb, } - if kwargs.get('target_name') == XFORM: + if kwargs.get("target_name") == XFORM: xform = XForm.objects.get(id=instance.target_object_id) - kwargs[ - 'organization_username'] = xform.project.organization.username - kwargs['verb'] = VERB_TOPIC_DICT[instance.verb] - kwargs['project_id'] = xform.project.id - return ('/{topic_base}/organization/{organization_username}/' - 'project/{project_id}/{target_name}/{target_id}/{verb}/' - 'messages/publish').format(**kwargs) - - elif kwargs.get('verb') == MESSAGE: + kwargs["organization_username"] = xform.project.organization.username + kwargs["verb"] = VERB_TOPIC_DICT[instance.verb] + kwargs["project_id"] = xform.project.id return ( - '/{topic_base}/{target_name}/{target_id}/' - 'messages/publish'.format( - **kwargs)) + "/{topic_base}/organization/{organization_username}/" + "project/{project_id}/{target_name}/{target_id}/{verb}/" + "messages/publish" + ).format(**kwargs) + + if kwargs.get("verb") == MESSAGE: + return "/{topic_base}/{target_name}/{target_id}/" "messages/publish".format( + **kwargs + ) + + return "" def send(self, instance): """ @@ -143,6 +153,12 @@ def send(self, instance): payload = get_payload(instance) # send it - return publish.single(topic, payload=payload, hostname=self.host, - port=self.port, tls=self.cert_info, qos=self.qos, - retain=self.retain) + return publish.single( + topic, + payload=payload, + hostname=self.host, + port=self.port, + tls=self.cert_info, + qos=self.qos, + retain=self.retain, + ) diff --git a/onadata/apps/messaging/filters.py b/onadata/apps/messaging/filters.py index 8250e2d703..9a2e7fffc5 100644 --- a/onadata/apps/messaging/filters.py +++ b/onadata/apps/messaging/filters.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals from actstream.models import Action -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.utils.translation import gettext as _ from rest_framework import exceptions, filters from django_filters import rest_framework as rest_filters @@ -13,6 +13,9 @@ from onadata.apps.messaging.utils import TargetDoesNotExist, get_target +User = get_user_model() + + DATETIME_LOOKUPS = [ "exact", "gt", @@ -50,10 +53,7 @@ class ActionFilterSet(rest_filters.FilterSet): class Meta: model = Action - fields = { - "verb": ["exact"], - "timestamp": DATETIME_LOOKUPS - } + fields = {"verb": ["exact"], "timestamp": DATETIME_LOOKUPS} class TargetTypeFilterBackend(filters.BaseFilterBackend): @@ -61,26 +61,25 @@ class TargetTypeFilterBackend(filters.BaseFilterBackend): A filter backend that filters by target type. """ - # pylint: disable=no-self-use def filter_queryset(self, request, queryset, view): """ Return a filtered queryset. """ - if view.action == 'list': - target_type = request.query_params.get('target_type') + if view.action == "list": + target_type = request.query_params.get("target_type") if target_type: try: target = get_target(target_type) - except TargetDoesNotExist: + except TargetDoesNotExist as exc: raise exceptions.ParseError( - "Unknown target_type {}".format(target_type)) + f"Unknown target_type {target_type}" + ) from exc return queryset.filter(target_content_type=target) - raise exceptions.ParseError( - _("Parameter 'target_type' is missing.")) + raise exceptions.ParseError(_("Parameter 'target_type' is missing.")) return queryset @@ -90,14 +89,13 @@ class TargetIDFilterBackend(filters.BaseFilterBackend): A filter backend that filters by target id. """ - # pylint: disable=no-self-use def filter_queryset(self, request, queryset, view): """ Return a filtered queryset. """ - if view.action == 'list': - target_id = request.query_params.get('target_id') + if view.action == "list": + target_id = request.query_params.get("target_id") if target_id: return queryset.filter(target_object_id=target_id) @@ -113,14 +111,13 @@ class UserFilterBackend(filters.BaseFilterBackend): A filter backend that filters by username. """ - # pylint: disable=no-self-use def filter_queryset(self, request, queryset, view): """ Return a filtered queryset. """ - if view.action == 'list': - username = request.query_params.get('user') + if view.action == "list": + username = request.query_params.get("user") try: user = User.objects.get(username=username) return queryset.filter(actor_object_id=user.id) diff --git a/onadata/apps/messaging/permissions.py b/onadata/apps/messaging/permissions.py index 7e1f776039..42c1c6727e 100644 --- a/onadata/apps/messaging/permissions.py +++ b/onadata/apps/messaging/permissions.py @@ -4,22 +4,26 @@ """ from __future__ import unicode_literals -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from rest_framework import exceptions, permissions +User = get_user_model() + + class TargetObjectPermissions(permissions.BasePermission): """ Check target object permissions """ + perms_map = { - 'GET': ['%(app_label)s.view_%(model_name)s'], - 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], - 'HEAD': ['%(app_label)s.view_%(model_name)s'], - 'POST': ['%(app_label)s.change_%(model_name)s'], - 'PUT': ['%(app_label)s.change_%(model_name)s'], - 'PATCH': ['%(app_label)s.change_%(model_name)s'], - 'DELETE': ['%(app_label)s.delete_%(model_name)s'], + "GET": ["%(app_label)s.view_%(model_name)s"], + "OPTIONS": ["%(app_label)s.view_%(model_name)s"], + "HEAD": ["%(app_label)s.view_%(model_name)s"], + "POST": ["%(app_label)s.change_%(model_name)s"], + "PUT": ["%(app_label)s.change_%(model_name)s"], + "PATCH": ["%(app_label)s.change_%(model_name)s"], + "DELETE": ["%(app_label)s.delete_%(model_name)s"], } def get_required_object_permissions(self, method, model_cls): @@ -28,8 +32,8 @@ def get_required_object_permissions(self, method, model_cls): """ kwargs = { - 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.model_name + "app_label": model_cls._meta.app_label, + "model_name": model_cls._meta.model_name, } if method not in self.perms_map: diff --git a/onadata/apps/messaging/serializers.py b/onadata/apps/messaging/serializers.py index ca71d4942e..4f24232b80 100644 --- a/onadata/apps/messaging/serializers.py +++ b/onadata/apps/messaging/serializers.py @@ -12,7 +12,7 @@ from actstream.models import Action from actstream.signals import action from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.http import HttpRequest from django.utils.translation import gettext as _ from rest_framework import exceptions, serializers @@ -21,12 +21,14 @@ from onadata.apps.messaging.utils import TargetDoesNotExist, get_target +User = get_user_model() + + class ContentTypeChoiceField(serializers.ChoiceField): """ Custom ChoiceField that gets the model name from a ContentType object """ - # pylint: disable=no-self-use def to_representation(self, value): """ Get the model from ContentType object @@ -38,30 +40,41 @@ class MessageSerializer(serializers.ModelSerializer): """ Serializer class for Message objects """ - TARGET_CHOICES = (('xform', 'XForm'), ('project', 'Project'), - ('user', 'User')) # yapf: disable - - message = serializers.CharField(source='description', allow_blank=False) - target_id = serializers.IntegerField(source='target_object_id') - target_type = ContentTypeChoiceField( - TARGET_CHOICES, source='target_content_type') - user = serializers.CharField(source='actor', required=False) + + TARGET_CHOICES = ( + ("xform", "XForm"), + ("project", "Project"), + ("user", "User"), + ) # yapf: disable + + message = serializers.CharField(source="description", allow_blank=False) + target_id = serializers.IntegerField(source="target_object_id") + target_type = ContentTypeChoiceField(TARGET_CHOICES, source="target_content_type") + user = serializers.CharField(source="actor", required=False) verb = serializers.ChoiceField(MESSAGE_VERBS, default=MESSAGE) class Meta: """ MessageSerializer metadata """ + model = Action - fields = ['id', 'verb', 'message', 'user', 'target_id', 'target_type', - 'timestamp'] + fields = [ + "id", + "verb", + "message", + "user", + "target_id", + "target_type", + "timestamp", + ] def __init__(self, *args, **kwargs): - super(MessageSerializer, self).__init__(*args, **kwargs) - request = self.context.get('request') - full_message_payload = getattr(settings, 'FULL_MESSAGE_PAYLOAD', False) - if request and request.method == 'GET' and not full_message_payload: - extra_fields = ['target_type', 'target_id'] + super().__init__(*args, **kwargs) + request = self.context.get("request") + full_message_payload = getattr(settings, "FULL_MESSAGE_PAYLOAD", False) + if request and request.method == "GET" and not full_message_payload: + extra_fields = ["target_type", "target_id"] for field in extra_fields: self.fields.pop(field) @@ -69,39 +82,47 @@ def create(self, validated_data): """ Creates the Message in the Action model """ - request = self.context['request'] + request = self.context["request"] target_type = validated_data.get("target_content_type") target_id = validated_data.get("target_object_id") verb = validated_data.get("verb", MESSAGE) try: content_type = get_target(target_type) - except TargetDoesNotExist: - raise serializers.ValidationError({ - 'target_type': _('Unknown target type') - }) # yapf: disable + except TargetDoesNotExist as exc: + raise serializers.ValidationError( + {"target_type": _("Unknown target type")} + ) from exc # yapf: disable else: try: - target_object = \ - content_type.get_object_for_this_type(pk=target_id) - except content_type.model_class().DoesNotExist: - raise serializers.ValidationError({ - 'target_id': _('target_id not found') - }) # yapf: disable + target_object = content_type.get_object_for_this_type(pk=target_id) + except content_type.model_class().DoesNotExist as exc: + raise serializers.ValidationError( + {"target_id": _("target_id not found")} + ) from exc # yapf: disable else: # check if request.user has permission to the target_object - permission = '{}.change_{}'.format( - target_object._meta.app_label, - target_object._meta.model_name) - if not request.user.has_perm(permission, target_object) \ - and verb == MESSAGE: - message = (_("You do not have permission to add messages " - "to target_id %s.") % target_object) + permission = ( + f"{target_object._meta.app_label}." + f"change_{target_object._meta.model_name}" + ) + if ( + not request.user.has_perm(permission, target_object) + and verb == MESSAGE + ): + message = ( + _( + "You do not have permission to add messages " + "to target_id %s." + ) + % target_object + ) raise exceptions.PermissionDenied(detail=message) results = action.send( request.user, verb=verb, target=target_object, - description=validated_data.get("description")) + description=validated_data.get("description"), + ) # results will be a list of tuples with the first item in the # tuple being the signal handler function and the second @@ -109,22 +130,28 @@ def create(self, validated_data): # element in the list whose function is `action_handler` try: + # pylint: disable=comparison-with-callable instance = [ - instance for (receiver, instance) in results + instance + for (receiver, instance) in results if receiver == action_handler ].pop() - except IndexError: + except IndexError as exc: # if you get here it means we have no instances raise serializers.ValidationError( - "Message not created. Please retry.") + "Message not created. Please retry." + ) from exc else: return instance def send_message( - instance_id: Union[list, int], - target_id: int, - target_type: str, user: User, message_verb: str): + instance_id: Union[list, int], + target_id: int, + target_type: str, + user: User, + message_verb: str, +): """ Send a message. :param id: A single ID or list of IDs that have been affected by an action @@ -133,7 +160,7 @@ def send_message( :param request: http request object :return: """ - message_id_limit = getattr(settings, 'NOTIFICATION_ID_LIMIT', 100) + message_id_limit = getattr(settings, "NOTIFICATION_ID_LIMIT", 100) if user: if isinstance(instance_id, int): instance_id = [instance_id] @@ -143,25 +170,22 @@ def send_message( data = { "target_id": target_id, "target_type": target_type, - "verb": message_verb + "verb": message_verb, } # If ID is a list and the message limit on the amount of IDs # in one message is passed. Split the ids into # chunks - if isinstance(instance_id, list) and\ - len(instance_id) > message_id_limit: + if isinstance(instance_id, list) and len(instance_id) > message_id_limit: ids = instance_id while len(ids) > 0: - data["message"] = json.dumps({'id': ids[:message_id_limit]}) - message = MessageSerializer( - data=data, context={"request": request}) + data["message"] = json.dumps({"id": ids[:message_id_limit]}) + message = MessageSerializer(data=data, context={"request": request}) del ids[:message_id_limit] if message.is_valid(): message.save() else: - data["message"] = json.dumps({'id': instance_id}) - message = MessageSerializer( - data=data, context={"request": request}) + data["message"] = json.dumps({"id": instance_id}) + message = MessageSerializer(data=data, context={"request": request}) if message.is_valid(): message.save() diff --git a/onadata/apps/messaging/signals.py b/onadata/apps/messaging/signals.py index abd1365cb9..d0b75c1fd3 100644 --- a/onadata/apps/messaging/signals.py +++ b/onadata/apps/messaging/signals.py @@ -13,23 +13,24 @@ from onadata.apps.messaging.tasks import call_backend_async -@receiver(post_save, sender=Action, dispatch_uid='messaging_backends_handler') -def messaging_backends_handler(sender, **kwargs): # pylint: disable=W0613 +@receiver(post_save, sender=Action, dispatch_uid="messaging_backends_handler") +def messaging_backends_handler(sender, **kwargs): # pylint: disable=unused-argument """ Handler to send messages to notification backends e.g MQTT. """ - backends = getattr(settings, 'NOTIFICATION_BACKENDS', {}) - as_task = getattr(settings, 'MESSAGING_ASYNC_NOTIFICATION', False) - created = kwargs.get('created') - instance = kwargs.get('instance') + backends = getattr(settings, "NOTIFICATION_BACKENDS", {}) + as_task = getattr(settings, "MESSAGING_ASYNC_NOTIFICATION", False) + created = kwargs.get("created") + instance = kwargs.get("instance") if instance and created: for name in backends: - backend = backends[name]['BACKEND'] - backend_options = backends[name].get('OPTIONS') + backend = backends[name]["BACKEND"] + backend_options = backends[name].get("OPTIONS") if as_task: # Sometimes the Action isn't created yet, hence # the need to delay 2 seconds call_backend_async.apply_async( - (backend, instance.id, backend_options), countdown=2) + (backend, instance.id, backend_options), countdown=2 + ) else: call_backend(backend, instance.id, backend_options) diff --git a/onadata/apps/messaging/tasks.py b/onadata/apps/messaging/tasks.py index 98a3b9c3e7..3567570cbe 100644 --- a/onadata/apps/messaging/tasks.py +++ b/onadata/apps/messaging/tasks.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals from onadata.apps.messaging.backends.base import call_backend -from onadata.celery import app +from onadata.celeryapp import app @app.task(ignore_result=True) diff --git a/onadata/apps/messaging/urls.py b/onadata/apps/messaging/urls.py index 2fe9163fd8..f3612df94f 100644 --- a/onadata/apps/messaging/urls.py +++ b/onadata/apps/messaging/urls.py @@ -7,9 +7,9 @@ from onadata.apps.messaging.viewsets import MessagingViewSet -router = routers.DefaultRouter(trailing_slash=False) # pylint: disable=C0103 -router.register(r'messaging', MessagingViewSet) +router = routers.DefaultRouter(trailing_slash=False) # pylint: disable=invalid-name +router.register(r"messaging", MessagingViewSet) -urlpatterns = [ # pylint: disable=C0103 - re_path(r'^api/v1/', include(router.urls)), +urlpatterns = [ # pylint: disable=invalid-name + re_path(r"^api/v1/", include(router.urls)), ] diff --git a/onadata/apps/messaging/utils.py b/onadata/apps/messaging/utils.py index 573c4d108a..763626dbff 100644 --- a/onadata/apps/messaging/utils.py +++ b/onadata/apps/messaging/utils.py @@ -14,6 +14,7 @@ class TargetDoesNotExist(Exception): """ Target does not Exist exception class. """ + message = UNKNOWN_TARGET @@ -26,5 +27,5 @@ def get_target(target_type): app_label = APP_LABEL_MAPPING[target_type] return ContentType.objects.get(app_label=app_label, model=target_type) - except (KeyError, ContentType.DoesNotExist): - raise TargetDoesNotExist() + except (KeyError, ContentType.DoesNotExist) as exc: + raise TargetDoesNotExist() from exc diff --git a/onadata/apps/messaging/viewsets.py b/onadata/apps/messaging/viewsets.py index 470a3c93f0..fe2482e9ca 100644 --- a/onadata/apps/messaging/viewsets.py +++ b/onadata/apps/messaging/viewsets.py @@ -13,17 +13,24 @@ from onadata.apps.messaging.constants import MESSAGE_VERBS from onadata.apps.messaging.filters import ( - ActionFilterSet, TargetIDFilterBackend, TargetTypeFilterBackend, - UserFilterBackend) + ActionFilterSet, + TargetIDFilterBackend, + TargetTypeFilterBackend, + UserFilterBackend, +) from onadata.apps.messaging.permissions import TargetObjectPermissions from onadata.apps.messaging.serializers import MessageSerializer from onadata.libs.pagination import StandardPageNumberPagination # pylint: disable=too-many-ancestors -class MessagingViewSet(mixins.CreateModelMixin, mixins.ListModelMixin, - mixins.RetrieveModelMixin, mixins.DestroyModelMixin, - viewsets.GenericViewSet): +class MessagingViewSet( + mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): """ ViewSet for the Messaging app - implements /messaging API endpoint """ @@ -31,8 +38,12 @@ class MessagingViewSet(mixins.CreateModelMixin, mixins.ListModelMixin, serializer_class = MessageSerializer queryset = Action.objects.filter(verb__in=MESSAGE_VERBS) permission_classes = [IsAuthenticated, TargetObjectPermissions] - filter_backends = (TargetTypeFilterBackend, TargetIDFilterBackend, - UserFilterBackend, DjangoFilterBackend) + filter_backends = ( + TargetTypeFilterBackend, + TargetIDFilterBackend, + UserFilterBackend, + DjangoFilterBackend, + ) filterset_class = ActionFilterSet pagination_class = StandardPageNumberPagination @@ -40,24 +51,24 @@ def list(self, request, *args, **kwargs): headers = None queryset = self.filter_queryset(self.get_queryset()) no_of_records = queryset.count() - retrieval_threshold = getattr( - settings, "MESSAGE_RETRIEVAL_THRESHOLD", 10000) - pagination_keys = [self.paginator.page_query_param, - self.paginator.page_size_query_param] + retrieval_threshold = getattr(settings, "MESSAGE_RETRIEVAL_THRESHOLD", 10000) + pagination_keys = [ + self.paginator.page_query_param, + self.paginator.page_size_query_param, + ] query_param_keys = self.request.query_params - should_paginate = any( - [k in query_param_keys for k in pagination_keys]) or \ - no_of_records > retrieval_threshold + should_paginate = ( + any(k in query_param_keys for k in pagination_keys) + or no_of_records > retrieval_threshold + ) - if should_paginate and \ - "page_size" not in self.request.query_params.keys(): + if should_paginate and "page_size" not in self.request.query_params.keys(): self.paginator.page_size = retrieval_threshold if should_paginate: page = self.paginate_queryset(queryset) serializer = self.get_serializer(page, many=True) - headers = self.paginator.generate_link_header( - self.request, queryset) + headers = self.paginator.generate_link_header(self.request, queryset) else: serializer = self.get_serializer(queryset, many=True) diff --git a/onadata/apps/restservice/signals.py b/onadata/apps/restservice/signals.py index fea296a59d..e2db6b72b4 100644 --- a/onadata/apps/restservice/signals.py +++ b/onadata/apps/restservice/signals.py @@ -12,7 +12,7 @@ ) # pylint: disable=invalid-name -trigger_webhook = django.dispatch.Signal(providing_args=["instance"]) +trigger_webhook = django.dispatch.Signal() def call_webhooks(sender, **kwargs): # pylint: disable=unused-argument diff --git a/onadata/apps/restservice/tasks.py b/onadata/apps/restservice/tasks.py index e9acd263d2..1aeaaa4ac6 100644 --- a/onadata/apps/restservice/tasks.py +++ b/onadata/apps/restservice/tasks.py @@ -4,7 +4,7 @@ """ from onadata.apps.logger.models.instance import Instance from onadata.apps.restservice.utils import call_service -from onadata.celery import app +from onadata.celeryapp import app @app.task() diff --git a/onadata/apps/sms_support/parser.py b/onadata/apps/sms_support/parser.py index 05ed039ee1..66677e98af 100644 --- a/onadata/apps/sms_support/parser.py +++ b/onadata/apps/sms_support/parser.py @@ -107,7 +107,7 @@ def media_value(value, medias): return filename except (AttributeError, TypeError, binascii.Error) as e: raise SMSCastingError( - _("Media file format " "incorrect. %(except)r") % {"except": e}, + _("Media file format incorrect. %(except)r") % {"except": e}, xlsf_name, ) from e @@ -122,7 +122,7 @@ def media_value(value, medias): if choice.get("sms_option") == value: return choice.get("name") raise SMSCastingError( - _("No matching choice " "for '%(input)s'") % {"input": value}, xlsf_name + _("No matching choice for '%(input)s'") % {"input": value}, xlsf_name ) if xlsf_type == "select all that apply": values = [s.strip() for s in value.split()] @@ -272,7 +272,7 @@ def process_incoming_smses(username, incomings, id_string=None): # noqa C901 json_submissions = [] resp_str = { "success": _( - "[SUCCESS] Your submission has been accepted. " "It's ID is {{ id }}." + "[SUCCESS] Your submission has been accepted. It's ID is {{ id }}." ) } @@ -289,7 +289,7 @@ def process_incoming(incoming, id_string): responses.append( { "code": SMS_API_ERROR, - "text": _("Missing 'identity' " "or 'text' field."), + "text": _("Missing 'identity' or 'text' field."), } ) return @@ -298,7 +298,7 @@ def process_incoming(incoming, id_string): responses.append( { "code": SMS_API_ERROR, - "text": _("'identity' and 'text' fields can " "not be empty."), + "text": _("'identity' and 'text' fields can not be empty."), } ) return @@ -316,7 +316,7 @@ def process_incoming(incoming, id_string): { "code": SMS_SUBMISSION_REFUSED, "text": _( - "The form '%(id_string)s' does not " "accept SMS submissions." + "The form '%(id_string)s' does not accept SMS submissions." ) % {"id_string": xform.id_string}, } @@ -334,14 +334,12 @@ def process_incoming(incoming, id_string): resp_str.update({"success": json_survey.get("sms_response")}) # check that the form contains at least one filled group - meta_groups = sum([1 for k in list(json_submission) if k.startswith("meta")]) + meta_groups = sum(1 for k in list(json_submission) if k.startswith("meta")) if len(list(json_submission)) <= meta_groups: responses.append( { "code": SMS_PARSING_ERROR, - "text": _( - "There must be at least one group of " "questions filled." - ), + "text": _("There must be at least one group of questions filled."), } ) return @@ -362,7 +360,7 @@ def process_incoming(incoming, id_string): responses.append( { "code": SMS_SUBMISSION_REFUSED, - "text": _(f"Required field `{field}` is " "missing."), + "text": _(f"Required field `{field}` is missing."), } ) return diff --git a/onadata/apps/sms_support/providers/smssync.py b/onadata/apps/sms_support/providers/smssync.py index b146c90b04..c8258a00fa 100644 --- a/onadata/apps/sms_support/providers/smssync.py +++ b/onadata/apps/sms_support/providers/smssync.py @@ -36,14 +36,14 @@ def autodoc(url_root, username, id_string): "Ushaidi's SMS Sync" } + "

  1. " - + _("Download the SMS Sync App on your phone serving " "as a gateway.") + + _("Download the SMS Sync App on your phone serving as a gateway.") + "
  2. " + _("Configure the app to point to one of the following URLs") + '
    %(urla)s' + "
    %(urlb)s

    " - + _("Optionnaly set a keyword to prevent non-formhub " "messages to be sent.") + + _("Optionnaly set a keyword to prevent non-formhub messages to be sent.") + "
  3. " - + _("In the preferences, tick the box to allow " "replies from the server.") + + _("In the preferences, tick the box to allow replies from the server.") + "

" + _( "That's it. Now Send an SMS Formhub submission to the number " diff --git a/onadata/apps/sms_support/tools.py b/onadata/apps/sms_support/tools.py index 033dc6cc89..07e39b08c7 100644 --- a/onadata/apps/sms_support/tools.py +++ b/onadata/apps/sms_support/tools.py @@ -248,7 +248,7 @@ def prep_return(msg, comp=None): sensitive_fields += ("datetime",) # must not contain out-of-group questions - if sum([1 for e in groups if e.get("type") != "group"]): + if sum(1 for e in groups if e.get("type") != "group"): return prep_return(_("All your questions must be in groups.")) # all groups must have an sms_field bad_groups = [ @@ -333,7 +333,7 @@ def prep_return(msg, comp=None): # has date field with no sms_date_format if not json_survey.get("sms_date_format", ""): for group in groups: - if sum([1 for e in group.get("children", [{}]) if e.get("type") == "date"]): + if sum(1 for e in group.get("children", [{}]) if e.get("type") == "date"): warnings.append( "

  • You have 'date' fields without " "explicitly setting a date format. " @@ -344,7 +344,7 @@ def prep_return(msg, comp=None): if not json_survey.get("sms_date_format", ""): for group in groups: if sum( - [1 for e in group.get("children", [{}]) if e.get("type") == "datetime"] + 1 for e in group.get("children", [{}]) if e.get("type") == "datetime" ): warnings.append( "
  • You have 'datetime' fields without " diff --git a/onadata/apps/viewer/admin.py b/onadata/apps/viewer/admin.py index 8ca3929619..d6ef7188b8 100644 --- a/onadata/apps/viewer/admin.py +++ b/onadata/apps/viewer/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from reversion.admin import VersionAdmin from django.contrib import admin @@ -6,13 +7,13 @@ class DataDictionaryAdmin(VersionAdmin, admin.ModelAdmin): - exclude = ('user',) + exclude = ("user",) def get_queryset(self, request): - qs = super(DataDictionaryAdmin, self).get_queryset(request) + queryset = super().get_queryset(request) if request.user.is_superuser: - return qs - return qs.filter(user=request.user) + return queryset + return queryset.filter(user=request.user) admin.site.register(DataDictionary, DataDictionaryAdmin) diff --git a/onadata/apps/viewer/management/commands/mark_start_times.py b/onadata/apps/viewer/management/commands/mark_start_times.py index 096fab9b39..9a92fcfe97 100644 --- a/onadata/apps/viewer/management/commands/mark_start_times.py +++ b/onadata/apps/viewer/management/commands/mark_start_times.py @@ -1,20 +1,23 @@ +# -*- coding: utf-8 -*- +""" +mark_start_times command - This is a one-time command to mark start times of old surveys +""" from django.core.management.base import BaseCommand -from django.utils.translation import gettext_lazy, gettext as _ +from django.utils.translation import gettext_lazy from onadata.apps.viewer.models.data_dictionary import DataDictionary class Command(BaseCommand): + """ + This is a one-time command to mark start times of old surveys. + """ + help = gettext_lazy( - "This is a one-time command to " "mark start times of old surveys." + "This is a one-time command to mark start times of old surveys." ) def handle(self, *args, **kwargs): - for dd in DataDictionary.objects.all(): - try: - dd.mark_start_time_boolean() - dd.save() - except Exception: - self.stderr.write( - _("Could not mark start time for DD: %(data)s") % {"data": repr(dd)} - ) + for xform in DataDictionary.objects.all(): + xform.mark_start_time_boolean() + xform.save() diff --git a/onadata/apps/viewer/management/commands/set_uuid_in_xml.py b/onadata/apps/viewer/management/commands/set_uuid_in_xml.py index 0faf46ed7b..919b0d1bec 100644 --- a/onadata/apps/viewer/management/commands/set_uuid_in_xml.py +++ b/onadata/apps/viewer/management/commands/set_uuid_in_xml.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +set_uuid_in_xml command - Insert UUID into XML of all existing XForms. +""" from django.core.management.base import BaseCommand from django.utils.translation import gettext as _, gettext_lazy @@ -6,15 +10,20 @@ class Command(BaseCommand): + """ + set_uuid_in_xml command - Insert UUID into XML of all existing XForms. + """ + help = gettext_lazy("Insert UUID into XML of all existing XForms") def handle(self, *args, **kwargs): - self.stdout.write(_('%(nb)d XForms to update') - % {'nb': DataDictionary.objects.count()}) - for i, dd in enumerate( - queryset_iterator(DataDictionary.objects.all())): - if dd.xls: - dd.set_uuid_in_xml() - super(DataDictionary, dd).save() + self.stdout.write( + _("%(nb)d XForms to update") % {"nb": DataDictionary.objects.count()} + ) + for i, xform in enumerate(queryset_iterator(DataDictionary.objects.all())): + if xform.xls: + xform.set_uuid_in_xml() + # pylint: disable=bad-super-call + super(DataDictionary, xform).save() if (i + 1) % 10 == 0: - self.stdout.write(_('Updated %(nb)d XForms...') % {'nb': i}) + self.stdout.write(_(f"Updated {i} XForms...")) diff --git a/onadata/apps/viewer/models/data_dictionary.py b/onadata/apps/viewer/models/data_dictionary.py index 59d15d2154..4de9a2eeaa 100644 --- a/onadata/apps/viewer/models/data_dictionary.py +++ b/onadata/apps/viewer/models/data_dictionary.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ DataDictionary model. """ @@ -65,6 +65,7 @@ def process_xlsform(xls, default_name): # adopted from pyxform.utils.sheet_to_csv +# pylint: disable=too-many-branches,too-many-locals def sheet_to_csv(xls_content, sheet_name): """Writes a csv file of a specified sheet from a an excel file @@ -78,9 +79,7 @@ def sheet_to_csv(xls_content, sheet_name): sheet = workbook.get_sheet_by_name(sheet_name) if not sheet or sheet.max_column < 2: - raise Exception( - _("Sheet <'%(sheet_name)s'> has no data." % {"sheet_name": sheet_name}) - ) + raise Exception(_(f"Sheet <'{sheet_name}'> has no data.")) csv_file = BytesIO() @@ -143,7 +142,7 @@ def __init__(self, *args, **kwargs): self.instances_for_export = lambda d: d.instances.all() self.has_external_choices = False self._id_string_changed = False - super(DataDictionary, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def __str__(self): return getattr(self, "id_string", "") @@ -200,7 +199,7 @@ def save(self, *args, **kwargs): if "skip_xls_read" in kwargs: del kwargs["skip_xls_read"] - super(DataDictionary, self).save(*args, **kwargs) + super().save(*args, **kwargs) def file_name(self): return os.path.split(self.xls.name)[-1] @@ -214,13 +213,14 @@ def set_object_permissions(sender, instance=None, created=False, **kwargs): """ if instance.project: # clear cache - safe_delete("{}{}".format(PROJ_FORMS_CACHE, instance.project.pk)) - safe_delete("{}{}".format(PROJ_BASE_FORMS_CACHE, instance.project.pk)) + safe_delete(f"{PROJ_FORMS_CACHE}{instance.project.pk}") + safe_delete(f"{PROJ_BASE_FORMS_CACHE}{instance.project.pk}") # seems the super is not called, have to get xform from here xform = XForm.objects.get(pk=instance.pk) if created: + # pylint: disable=import-outside-toplevel from onadata.libs.permissions import OwnerRole OwnerRole.add(instance.user, xform) @@ -228,6 +228,7 @@ def set_object_permissions(sender, instance=None, created=False, **kwargs): if instance.created_by and instance.user != instance.created_by: OwnerRole.add(instance.created_by, xform) + # pylint: disable=import-outside-toplevel from onadata.libs.utils.project_utils import ( set_project_perms_to_xform_async, ) # noqa @@ -235,6 +236,7 @@ def set_object_permissions(sender, instance=None, created=False, **kwargs): try: set_project_perms_to_xform_async.delay(xform.pk, instance.project.pk) except OperationalError: + # pylint: disable=import-outside-toplevel from onadata.libs.utils.project_utils import ( set_project_perms_to_xform, ) # noqa @@ -248,6 +250,7 @@ def set_object_permissions(sender, instance=None, created=False, **kwargs): size = f.tell() f.seek(0) + # pylint: disable=import-outside-toplevel from onadata.apps.main.models.meta_data import MetaData data_file = InMemoryUploadedFile( diff --git a/onadata/apps/viewer/models/export.py b/onadata/apps/viewer/models/export.py index fc3ea9bf3f..b506ebc651 100644 --- a/onadata/apps/viewer/models/export.py +++ b/onadata/apps/viewer/models/export.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Export model. """ @@ -38,7 +38,7 @@ def get_export_options_query_kwargs(options): if field in options: field_value = options.get(field) - key = "options__{}".format(field) + key = f"options__{field}" options_kwargs[key] = field_value return options_kwargs @@ -162,7 +162,7 @@ class Meta: unique_together = (("xform", "filename"),) def __str__(self): - return "%s - %s (%s)" % (self.export_type, self.xform, self.filename) + return f"{self.export_type} - {self.xform} ({self.filename})" def save(self, *args, **kwargs): # pylint: disable=arguments-differ if not self.pk and self.xform: @@ -177,11 +177,11 @@ def save(self, *args, **kwargs): # pylint: disable=arguments-differ # update time_of_last_submission with # xform.time_of_last_submission_update - # pylint: disable=E1101 + # pylint: disable=no-member self.time_of_last_submission = self.xform.time_of_last_submission_update() if self.filename: self.internal_status = Export.SUCCESSFUL - super(Export, self).save(*args, **kwargs) + super().save(*args, **kwargs) @classmethod def _delete_oldest_export(cls, xform, export_type): @@ -213,7 +213,7 @@ def status(self): # need to have this since existing models will have their # internal_status set to PENDING - the default return Export.SUCCESSFUL - elif self.internal_status == Export.FAILED: + if self.internal_status == Export.FAILED: return Export.FAILED return Export.PENDING @@ -230,7 +230,7 @@ def set_filename(self, filename): def _update_filedir(self): if not self.filename: raise AssertionError() - # pylint: disable=E1101 + # pylint: disable=no-member self.filedir = os.path.join( self.xform.user.username, "exports", self.xform.id_string, self.export_type ) @@ -258,6 +258,7 @@ def full_filepath(self): except NotImplementedError: # read file from s3 _name, ext = os.path.splitext(self.filepath) + # pylint: disable=consider-using-with tmp = NamedTemporaryFile(suffix=ext, delete=False) f = default_storage.open(self.filepath) tmp.write(f.read()) @@ -280,7 +281,7 @@ def exports_outdated(cls, xform, export_type, options=None): xform=xform, export_type=export_type, internal_status__in=[Export.SUCCESSFUL, Export.PENDING], - **export_options + **export_options, ).latest("created_on") except cls.DoesNotExist: return True diff --git a/onadata/apps/viewer/signals.py b/onadata/apps/viewer/signals.py index 02a211fe68..5020262964 100644 --- a/onadata/apps/viewer/signals.py +++ b/onadata/apps/viewer/signals.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Viewer signals module. """ @@ -11,14 +11,15 @@ from onadata.apps.viewer.models import ParsedInstance from onadata.libs.utils.osm import save_osm_data_async -ASYNC_POST_SUBMISSION_PROCESSING_ENABLED = \ - getattr(settings, 'ASYNC_POST_SUBMISSION_PROCESSING_ENABLED', False) +ASYNC_POST_SUBMISSION_PROCESSING_ENABLED = getattr( + settings, "ASYNC_POST_SUBMISSION_PROCESSING_ENABLED", False +) -# pylint: disable=C0103 -process_submission = django.dispatch.Signal(providing_args=['instance']) +# pylint: disable=invalid-name +process_submission = django.dispatch.Signal() -def post_save_osm_data(instance_id): # pylint: disable=W0613 +def post_save_osm_data(instance_id): # pylint: disable=unused-argument """ Process OSM data post submission. """ @@ -34,31 +35,31 @@ def _post_process_submissions(instance): post_save_osm_data(instance.pk) -def post_save_submission(sender, **kwargs): # pylint: disable=W0613 +def post_save_submission(sender, **kwargs): # pylint: disable=unused-argument """ Calls webhooks and OSM data processing for ParsedInstance model. """ - parsed_instance = kwargs.get('instance') - created = kwargs.get('created') + parsed_instance = kwargs.get("instance") + created = kwargs.get("created") if created: _post_process_submissions(parsed_instance.instance) post_save.connect( - post_save_submission, - sender=ParsedInstance, - dispatch_uid='post_save_submission') + post_save_submission, sender=ParsedInstance, dispatch_uid="post_save_submission" +) -def process_saved_submission(sender, **kwargs): # pylint: disable=W0613 +def process_saved_submission(sender, **kwargs): # pylint: disable=unused-argument """ Calls webhooks and OSM data processing for Instance model. """ - instance = kwargs.get('instance') + instance = kwargs.get("instance") if instance: _post_process_submissions(instance) -process_submission.connect(process_saved_submission, sender=Instance, - dispatch_uid='process_saved_submission') +process_submission.connect( + process_saved_submission, sender=Instance, dispatch_uid="process_saved_submission" +) diff --git a/onadata/apps/viewer/tasks.py b/onadata/apps/viewer/tasks.py index 19da86e727..28ec746657 100644 --- a/onadata/apps/viewer/tasks.py +++ b/onadata/apps/viewer/tasks.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Export tasks. """ @@ -18,7 +18,7 @@ ExportConnectionError, ExportTypeError, ) -from onadata.celery import app +from onadata.celeryapp import app from onadata.libs.exceptions import NoRecordsFoundError from onadata.libs.utils.common_tools import get_boolean_value, report_exception from onadata.libs.utils.export_tools import ( diff --git a/onadata/apps/viewer/xls_writer.py b/onadata/apps/viewer/xls_writer.py index 9854d78d02..f7e1079bc3 100644 --- a/onadata/apps/viewer/xls_writer.py +++ b/onadata/apps/viewer/xls_writer.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +XlsWriter module - generate a spreadsheet workbook in XLSX format. +""" from builtins import str as text from collections import defaultdict from io import StringIO @@ -7,13 +11,16 @@ from onadata.apps.logger.models.xform import question_types_to_exclude -class XlsWriter(object): +# pylint: disable=too-many-instance-attributes, +class XlsWriter: + """XlsWriter class - generate a spreadsheet workbook in XLSX format.""" def __init__(self): self.set_file() self.reset_workbook() self.sheet_name_limit = 30 self._generated_sheet_name_dict = {} + self._data_dictionary = None def set_file(self, file_object=None): """ @@ -25,6 +32,7 @@ def set_file(self, file_object=None): self._file = StringIO() def reset_workbook(self): + """Reset a Workbook to sensible default.""" self._workbook = Workbook() self._sheets = {} self._columns = defaultdict(list) @@ -32,11 +40,13 @@ def reset_workbook(self): self._generated_sheet_name_dict = {} def add_sheet(self, name): + """Add a given ``name`` sheet to this workbook.""" unique_sheet_name = self._unique_name_for_xls(name) sheet = self._workbook.add_sheet(unique_sheet_name) self._sheets[unique_sheet_name] = sheet def add_column(self, sheet_name, column_name): + """Add a ``column_name`` to the given ``sheet_name`` to this workbook.""" index = len(self._columns[sheet_name]) sheet = self._sheets.get(sheet_name) if sheet: @@ -44,6 +54,7 @@ def add_column(self, sheet_name, column_name): self._columns[sheet_name].append(column_name) def add_row(self, sheet_name, row): + """Add a ``row`` to the given ``sheet_name`` to this workbook.""" i = self._current_index[sheet_name] columns = self._columns[sheet_name] for key in list(row): @@ -51,25 +62,27 @@ def add_row(self, sheet_name, row): self.add_column(sheet_name, key) for j, column_name in enumerate(self._columns[sheet_name]): # leaving this untranslated as I'm not sure it's in django context - self._sheets[sheet_name].write(i, j, row.get(column_name, u"n/a")) + self._sheets[sheet_name].write(i, j, row.get(column_name, "n/a")) self._current_index[sheet_name] += 1 def add_obs(self, obs): + """Add data in ``obs`` dictionary into specified sheets to this workbook.""" self._fix_indices(obs) for sheet_name, rows in obs.items(): for row in rows: actual_sheet_name = self._generated_sheet_name_dict.get( - sheet_name, sheet_name) + sheet_name, sheet_name + ) self.add_row(actual_sheet_name, row) def _fix_indices(self, obs): for sheet_name, rows in obs.items(): for row in rows: - row[u'_index'] += self._current_index[sheet_name] - if row[u'_parent_index'] == -1: + row["_index"] += self._current_index[sheet_name] + if row["_parent_index"] == -1: continue - i = self._current_index[row[u'_parent_table_name']] - row[u'_parent_index'] += i + i = self._current_index[row["_parent_table_name"]] + row["_parent_index"] += i def write_tables_to_workbook(self, tables): """ @@ -88,10 +101,12 @@ def write_tables_to_workbook(self, tables): return self._workbook def save_workbook_to_file(self): + """Saves the XLSX workbook to a file.""" self._workbook.save(self._file) return self._file def set_data_dictionary(self, data_dictionary): + """Set the data_dictionary XForm model object for this class.""" self._data_dictionary = data_dictionary self.reset_workbook() self._add_sheets() @@ -100,18 +115,20 @@ def set_data_dictionary(self, data_dictionary): self.add_obs(obs) def _add_sheets(self): - for e in self._data_dictionary.get_survey_elements(): - if isinstance(e, Section): - sheet_name = e.name - self.add_sheet(sheet_name) - for f in e.children: - if isinstance(f, Question) and\ - not question_types_to_exclude(f.type): - self.add_column(sheet_name, f.name) + if self._data_dictionary: + for e in self._data_dictionary.get_survey_elements(): + if isinstance(e, Section): + sheet_name = e.name + self.add_sheet(sheet_name) + for f in e.children: + if isinstance(f, Question) and not question_types_to_exclude( + f.type + ): + self.add_column(sheet_name, f.name) def _unique_name_for_xls(self, sheet_name): # excel worksheet name limit seems to be 31 characters (30 to be safe) - unique_sheet_name = sheet_name[0:self.sheet_name_limit] + unique_sheet_name = sheet_name[0 : self.sheet_name_limit] unique_sheet_name = self._generate_unique_sheet_name(unique_sheet_name) self._generated_sheet_name_dict[sheet_name] = unique_sheet_name return unique_sheet_name @@ -120,15 +137,15 @@ def _generate_unique_sheet_name(self, sheet_name): # check if sheet name exists if sheet_name not in self._sheets: return sheet_name - else: - i = 1 - unique_name = sheet_name - while(unique_name in self._sheets): - number_len = len(text(i)) - allowed_name_len = self.sheet_name_limit - number_len - # make name required len - if(len(unique_name) > allowed_name_len): - unique_name = unique_name[0:allowed_name_len] - unique_name = "{0}{1}".format(unique_name, i) - i = i + 1 - return unique_name + + i = 1 + unique_name = sheet_name + while unique_name in self._sheets: + number_len = len(text(i)) + allowed_name_len = self.sheet_name_limit - number_len + # make name required len + if len(unique_name) > allowed_name_len: + unique_name = unique_name[0:allowed_name_len] + unique_name = f"{unique_name}{i}" + i = i + 1 + return unique_name diff --git a/onadata/celery.py b/onadata/celery.py deleted file mode 100644 index 2c598e2c0e..0000000000 --- a/onadata/celery.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Celery module for onadata. -""" -from __future__ import absolute_import, unicode_literals - -import os - -from django.conf import settings - -import celery -import sentry_sdk -from sentry_sdk.integrations.celery import CeleryIntegration - -# set the default Django settings module for the 'celery' program. -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "onadata.settings.common") - - -class Celery(celery.Celery): - """ - Celery class that allows Sentry configuration. - """ - - def on_configure(self): # pylint: disable=method-hidden - """ - Register Sentry for celery tasks. - """ - if getattr(settings, "RAVEN_CONFIG", None): - sentry_sdk.init( - dsn=settings.RAVEN_CONFIG["dsn"], integrations=[CeleryIntegration()] - ) - - -app = Celery(__name__) # pylint: disable=invalid-name - -# Using a string here means the worker will not have to -# pickle the object when using Windows. -app.config_from_object("django.conf:settings", namespace="CELERY") -app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) -app.conf.broker_transport_options = {"visibility_timeout": 10} - - -@app.task -def debug_task(): - """A test task""" - print("Hello!") - return True diff --git a/onadata/celeryapp.py b/onadata/celeryapp.py new file mode 100644 index 0000000000..8cd3e75bde --- /dev/null +++ b/onadata/celeryapp.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +Celery module for onadata. +""" +import os + +from celery import Celery + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "onadata.settings.common") + +app = Celery(__name__) # pylint: disable=invalid-name + +# Using a string here means the worker will not have to +# pickle the object when using Windows. +app.config_from_object("django.conf:settings", namespace="CELERY") +app.autodiscover_tasks() +app.conf.broker_transport_options = {"visibility_timeout": 10} + + +@app.task +def debug_task(): + """A test task""" + print("Hello!") + return True diff --git a/onadata/devwsgi.py b/onadata/devwsgi.py index 0b30f727e4..d84bdb0830 100644 --- a/onadata/devwsgi.py +++ b/onadata/devwsgi.py @@ -1,5 +1,5 @@ """ -WSGI config for mspray project. +WSGI config It exposes the WSGI callable as a module-level variable named ``application``. @@ -8,11 +8,12 @@ """ import os -import uwsgi -from uwsgidecorators import timer -from django.utils import autoreload from django.core.wsgi import get_wsgi_application +from django.utils import autoreload + +import uwsgi # pylint: disable=import-error +from uwsgidecorators import timer os.environ.setdefault("DJANGO_SETTINGS_MODULE", "onadata.settings.common") @@ -20,6 +21,7 @@ @timer(3) -def change_code_gracefull_reload(sig): - if autoreload.code_changed(): +def change_code_gracefull_reload(sig): # pylint: disable=unused-argument + """Reload uWSGI whenever the code changes""" + if autoreload.file_changed: uwsgi.reload() diff --git a/onadata/libs/authentication.py b/onadata/libs/authentication.py index 739a6cffae..81bd4cae1f 100644 --- a/onadata/libs/authentication.py +++ b/onadata/libs/authentication.py @@ -21,6 +21,7 @@ from oauth2_provider.models import AccessToken from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.settings import oauth2_settings +from oidc.utils import authenticate_sso from rest_framework import exceptions from rest_framework.authentication import ( BaseAuthentication, @@ -29,16 +30,10 @@ ) from rest_framework.authtoken.models import Token from rest_framework.exceptions import AuthenticationFailed -from oidc.utils import authenticate_sso from onadata.apps.api.models.temp_token import TempToken from onadata.apps.api.tasks import send_account_lockout_email -from onadata.libs.utils.cache_tools import ( - LOCKOUT_IP, - LOGIN_ATTEMPTS, - cache, - safe_key, -) +from onadata.libs.utils.cache_tools import LOCKOUT_IP, LOGIN_ATTEMPTS, cache, safe_key from onadata.libs.utils.common_tags import API_TOKEN from onadata.libs.utils.email import get_account_lockout_email_data @@ -152,7 +147,7 @@ def authenticate(self, request): raise exceptions.AuthenticationFailed(error_msg) if len(auth) > 2: error_msg = _( - "Invalid token header. " "Token string should not contain spaces." + "Invalid token header. Token string should not contain spaces." ) raise exceptions.AuthenticationFailed(error_msg) @@ -240,7 +235,7 @@ class SSOHeaderAuthentication(BaseAuthentication): cookie or HTTP_SSO header. """ - def authenticate(self, request): # pylint: disable=no-self-use + def authenticate(self, request): return authenticate_sso(request) @@ -367,9 +362,22 @@ class MasterReplicaOAuth2Validator(OAuth2Validator): """ Custom OAuth2Validator class that takes into account replication lag between Master & Replica databases - https://github.com/jazzband/django-oauth-toolkit/blob/3bde632d5722f1f85ffcd8277504955321f00fff/oauth2_provider/oauth2_validators.py#L49 + https://github.com/jazzband/django-oauth-toolkit/blob/ + 3bde632d5722f1f85ffcd8277504955321f00fff/oauth2_provider/oauth2_validators.py#L49 """ + def introspect_token(self, token, token_type_hint, request, *args, **kwargs): + """See oauthlib.oauth2.rfc6749.request_validator""" + raise NotImplementedError("Subclasses must implement this method.") + + def validate_silent_authorization(self, request): + """See oauthlib.oauth2.rfc6749.request_validator""" + raise NotImplementedError("Subclasses must implement this method.") + + def validate_silent_login(self, request): + """See oauthlib.oauth2.rfc6749.request_validator""" + raise NotImplementedError("Subclasses must implement this method.") + def validate_bearer_token(self, token, scopes, request): if not token: return False diff --git a/onadata/libs/baseviewset.py b/onadata/libs/baseviewset.py index 7748951883..9329b85651 100644 --- a/onadata/libs/baseviewset.py +++ b/onadata/libs/baseviewset.py @@ -1,2 +1,10 @@ -class DefaultBaseViewset(object): - pass +# -*- coding: utf-8 -*- +""" +The DefaultBaseViewset class +""" + + +class DefaultBaseViewset: + """ + The DefaultBaseViewset class + """ diff --git a/onadata/libs/data/__init__.py b/onadata/libs/data/__init__.py index a8efc0e3a5..45540a6fcf 100644 --- a/onadata/libs/data/__init__.py +++ b/onadata/libs/data/__init__.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Data utility functions. """ diff --git a/onadata/libs/data/query.py b/onadata/libs/data/query.py index c58a19a4e6..614cf27add 100644 --- a/onadata/libs/data/query.py +++ b/onadata/libs/data/query.py @@ -1,11 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Query data utility functions. +""" import logging + from django.conf import settings from django.db import connection -from onadata.libs.utils.common_tags import ( - SUBMISSION_TIME, SUBMITTED_BY) from onadata.apps.logger.models.data_view import DataView - +from onadata.libs.utils.common_tags import SUBMISSION_TIME, SUBMITTED_BY logger = logging.getLogger(__name__) @@ -14,10 +17,7 @@ def _dictfetchall(cursor): "Returns all rows from a cursor as a dict" desc = cursor.description - return [ - dict(zip([col[0] for col in desc], row)) - for row in cursor.fetchall() - ] + return [dict(zip([col[0] for col in desc], row)) for row in cursor.fetchall()] def _execute_query(query, to_dict=True): @@ -29,8 +29,7 @@ def _execute_query(query, to_dict=True): def _get_fields_of_type(xform, types): k = [] - survey_elements = flatten( - [xform.get_survey_elements_of_type(t) for t in types]) + survey_elements = flatten([xform.get_survey_elements_of_type(t) for t in types]) for element in survey_elements: name = element.get_abbreviated_xpath() @@ -40,14 +39,15 @@ def _get_fields_of_type(xform, types): def _additional_data_view_filters(data_view): + # pylint: disable=protected-access where, where_params = DataView._get_where_clause(data_view) data_view_where = "" if where: - data_view_where = u" AND " + u" AND ".join(where) + data_view_where = " AND " + " AND ".join(where) - for it in where_params: - data_view_where = data_view_where.replace('%s', "'{}'".format(it), 1) + for param in where_params: + data_view_where = data_view_where.replace("%s", f"'{param}'", 1) return data_view_where @@ -55,29 +55,36 @@ def _additional_data_view_filters(data_view): def _json_query(field): if not field: logger.info("Field is empty") - return "json->>'%s'" % field - return "json->>'%s'" % field.replace("'", "''") + return f"json->>'{field}'" + + _field = field.replace("'", "''") + return f"json->>'{_field}'" -def _postgres_count_group_field_n_group_by(field, name, xform, group_by, - data_view): + +def _postgres_count_group_field_n_group_by(field, name, xform, group_by, data_view): string_args = _query_args(field, name, xform, group_by) if is_date_field(xform, field): - string_args['json'] = "to_char(to_date(%(json)s, 'YYYY-MM-DD'), 'YYYY"\ - "-MM-DD')" % string_args + string_args["json"] = ( + "to_char(to_date(%(json)s, 'YYYY-MM-DD'), 'YYYY" "-MM-DD')" % string_args + ) additional_filters = "" if data_view: additional_filters = _additional_data_view_filters(data_view) restricted_string = _restricted_query(xform) - query = "SELECT %(json)s AS \"%(name)s\", "\ - "%(group_by)s AS \"%(group_name)s\", "\ - "count(*) as count "\ - "FROM %(table)s WHERE " + restricted_string + \ - "AND deleted_at IS NULL " + additional_filters + \ - " GROUP BY %(json)s, %(group_by)s" + \ - " ORDER BY %(json)s, %(group_by)s" + query = ( + 'SELECT %(json)s AS "%(name)s", ' + '%(group_by)s AS "%(group_name)s", ' + "count(*) as count " + "FROM %(table)s WHERE " + + restricted_string + + "AND deleted_at IS NULL " + + additional_filters + + " GROUP BY %(json)s, %(group_by)s" + + " ORDER BY %(json)s, %(group_by)s" + ) query = query % string_args return query @@ -86,8 +93,9 @@ def _postgres_count_group_field_n_group_by(field, name, xform, group_by, def _postgres_count_group(field, name, xform, data_view=None): string_args = _query_args(field, name, xform) if is_date_field(xform, field): - string_args['json'] = "to_char(to_date(%(json)s, 'YYYY-MM-DD'), 'YYYY"\ - "-MM-DD')" % string_args + string_args["json"] = ( + "to_char(to_date(%(json)s, 'YYYY-MM-DD'), 'YYYY" "-MM-DD')" % string_args + ) additional_filters = "" if data_view: @@ -99,10 +107,15 @@ def _postgres_count_group(field, name, xform, data_view=None): string_args["join"] = "i LEFT JOIN auth_user au ON au.id = i.user_id" restricted_string = _restricted_query(xform) - sql_query = "SELECT %(json)s AS \"%(name)s\", COUNT(*) AS count FROM " \ - "%(table)s %(join)s WHERE " + restricted_string + \ - " AND deleted_at IS NULL " + additional_filters + " GROUP BY %(json)s"\ + sql_query = ( + 'SELECT %(json)s AS "%(name)s", COUNT(*) AS count FROM ' + "%(table)s %(join)s WHERE " + + restricted_string + + " AND deleted_at IS NULL " + + additional_filters + + " GROUP BY %(json)s" " ORDER BY %(json)s" + ) sql_query = sql_query % string_args return sql_query @@ -111,8 +124,9 @@ def _postgres_count_group(field, name, xform, data_view=None): def _postgres_aggregate_group_by(field, name, xform, group_by, data_view=None): string_args = _query_args(field, name, xform, group_by) if is_date_field(xform, field): - string_args['json'] = "to_char(to_date(%(json)s, 'YYYY-MM-DD'), 'YYYY"\ - "-MM-DD')" % string_args + string_args["json"] = ( + "to_char(to_date(%(json)s, 'YYYY-MM-DD'), 'YYYY" "-MM-DD')" % string_args + ) additional_filters = "" if data_view: @@ -123,27 +137,37 @@ def _postgres_aggregate_group_by(field, name, xform, group_by, data_view=None): if isinstance(group_by, list): group_by_group_by = [] for i, __ in enumerate(group_by): - group_by_select += "%(group_by" + str(i) + \ - ")s AS \"%(group_name" + str(i) + ")s\", " + group_by_select += ( + "%(group_by" + str(i) + ')s AS "%(group_name' + str(i) + ')s", ' + ) group_by_group_by.append("%(group_by" + str(i) + ")s") group_by_group_by = ",".join(group_by_group_by) else: - group_by_select = "%(group_by)s AS \"%(group_name)s\"," + group_by_select = '%(group_by)s AS "%(group_name)s",' group_by_group_by = "%(group_by)s" restricted_string = _restricted_query(xform) aggregation_string = "COUNT(%(json)s) AS count " if field in get_numeric_fields(xform) or not isinstance(group_by, list): - aggregation_string += ", SUM((%(json)s)::numeric) AS sum, " \ - "AVG((%(json)s)::numeric) AS mean " + aggregation_string += ( + ", SUM((%(json)s)::numeric) AS sum, " "AVG((%(json)s)::numeric) AS mean " + ) else: group_by_select = "%(json)s AS %(name)s, " + group_by_select group_by_group_by = "%(json)s, " + group_by_group_by - query = "SELECT " + group_by_select + aggregation_string + \ - "FROM %(table)s WHERE " + restricted_string + \ - " AND deleted_at IS NULL " + additional_filters + \ - " GROUP BY " + group_by_group_by + \ - " ORDER BY " + group_by_group_by + query = ( + "SELECT " + + group_by_select + + aggregation_string + + "FROM %(table)s WHERE " + + restricted_string + + " AND deleted_at IS NULL " + + additional_filters + + " GROUP BY " + + group_by_group_by + + " ORDER BY " + + group_by_group_by + ) return query % string_args @@ -151,9 +175,11 @@ def _postgres_aggregate_group_by(field, name, xform, group_by, data_view=None): def _postgres_select_key(field, name, xform): string_args = _query_args(field, name, xform) restricted_string = _restricted_query(xform) - query = "SELECT %(json)s AS \"%(name)s\" FROM %(table)s WHERE " + \ - restricted_string + " AND deleted_at IS NULL "\ - + query = ( + 'SELECT %(json)s AS "%(name)s" FROM %(table)s WHERE ' + + restricted_string + + " AND deleted_at IS NULL " + ) return query % string_args @@ -166,34 +192,36 @@ def _restricted_query(xform): def _query_args(field, name, xform, group_by=None): qargs = { - 'table': 'logger_instance', - 'json': _json_query(field), - 'name': name, - 'restrict_field': 'xform_id', - 'restrict_value': xform.pk, - 'join': '', + "table": "logger_instance", + "json": _json_query(field), + "name": name, + "restrict_field": "xform_id", + "restrict_value": xform.pk, + "join": "", } if xform.is_merged_dataset: xforms = tuple( - __ for __ in xform.mergedxform.xforms.filter( - deleted_at__isnull=True).values_list('id', flat=True) + __ + for __ in xform.mergedxform.xforms.filter( + deleted_at__isnull=True + ).values_list("id", flat=True) ) or (xform.pk, xform.pk) - qargs['restrict_value'] = xforms + qargs["restrict_value"] = xforms if isinstance(group_by, list): for i, v in enumerate(group_by): - qargs['group_name%d' % i] = v - qargs['group_by%d' % i] = _json_query(v) + qargs[f"group_name{i}"] = v + qargs[f"group_by{i}"] = _json_query(v) else: - qargs['group_name'] = group_by - qargs['group_by'] = _json_query(group_by) + qargs["group_name"] = group_by + qargs["group_by"] = _json_query(group_by) return qargs def _select_key(field, name, xform): - if using_postgres: + if using_postgres(): result = _postgres_select_key(field, name, xform) else: raise Exception("Unsupported Database") @@ -202,23 +230,25 @@ def _select_key(field, name, xform): def flatten(lst): + """Flattens a list of lists.""" return [item for sublist in lst for item in sublist] def get_date_fields(xform): """List of date field names for specified xform""" return [SUBMISSION_TIME] + _get_fields_of_type( - xform, ['date', 'datetime', 'start', 'end', 'today']) + xform, ["date", "datetime", "start", "end", "today"] + ) def get_field_records(field, xform): - result = _execute_query(_select_key(field, field, xform), - to_dict=False) + """Queries and returns all records of the given field.""" + result = _execute_query(_select_key(field, field, xform), to_dict=False) return [float(i[0]) for i in result if i[0] is not None] -def get_form_submissions_grouped_by_field(xform, field, name=None, - data_view=None): +# pylint: disable=invalid-name +def get_form_submissions_grouped_by_field(xform, field, name=None, data_view=None): """Number of submissions grouped by field""" if not name: name = field @@ -226,41 +256,43 @@ def get_form_submissions_grouped_by_field(xform, field, name=None, return _execute_query(_postgres_count_group(field, name, xform, data_view)) -def get_form_submissions_aggregated_by_select_one(xform, field, name=None, - group_by=None, - data_view=None): +# pylint: disable=invalid-name +def get_form_submissions_aggregated_by_select_one( + xform, field, name=None, group_by=None, data_view=None +): """Number of submissions grouped and aggregated by select_one field""" if not name: name = field - return _execute_query(_postgres_aggregate_group_by(field, - name, - xform, - group_by, - data_view)) + return _execute_query( + _postgres_aggregate_group_by(field, name, xform, group_by, data_view) + ) -def get_form_submissions_grouped_by_select_one(xform, field, group_by, - name=None, data_view=None): +# pylint: disable=invalid-name +def get_form_submissions_grouped_by_select_one( + xform, field, group_by, name=None, data_view=None +): """Number of submissions disaggregated by select_one field""" if not name: name = field - return _execute_query(_postgres_count_group_field_n_group_by(field, - name, - xform, - group_by, - data_view)) + return _execute_query( + _postgres_count_group_field_n_group_by(field, name, xform, group_by, data_view) + ) def get_numeric_fields(xform): """List of numeric field names for specified xform""" - return _get_fields_of_type(xform, ['decimal', 'integer']) + return _get_fields_of_type(xform, ["decimal", "integer"]) def is_date_field(xform, field): + """Returns True if an XForm field is a date field.""" return field in get_date_fields(xform) -@property def using_postgres(): - return settings.DATABASES[ - 'default']['ENGINE'] == 'django.db.backends.postgresql' + """Returns True if django.db.backends.postgresql is the DB engine in use""" + return settings.DATABASES["default"]["ENGINE"] in [ + "django.db.backends.postgresql", + "django.contrib.gis.db.backends.postgis", + ] diff --git a/onadata/libs/data/statistics.py b/onadata/libs/data/statistics.py index bb5216a9b3..661f0e4324 100644 --- a/onadata/libs/data/statistics.py +++ b/onadata/libs/data/statistics.py @@ -1,9 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Statistics utility functions. +""" import numpy as np + from onadata.apps.api.tools import DECIMAL_PRECISION from onadata.libs.data.query import get_field_records, get_numeric_fields -def _chk_asarray(a, axis): +def _chk_asarray(a, axis): # pylint: disable=invalid-name if axis is None: a = np.ravel(a) outaxis = 0 @@ -14,10 +19,12 @@ def _chk_asarray(a, axis): def get_mean(values): + """Returns numpy.mean() of values.""" return np.mean(values) def get_median(values, axis=None): + """Returns numpy.median() of values for the given axis""" return np.median(values, axis) @@ -26,14 +33,14 @@ def get_mode(values, axis=0): Adapted from https://github.com/scipy/scipy/blob/master/scipy/stats/stats.py#L568 """ - a, axis = _chk_asarray(values, axis) - scores = np.unique(np.ravel(a)) # get ALL unique values + a, axis = _chk_asarray(values, axis) # pylint: disable=invalid-name + scores = np.unique(np.ravel(a)) # get ALL unique values testshape = list(a.shape) testshape[axis] = 1 oldmostfreq = np.zeros(testshape) oldcounts = np.zeros(testshape) for score in scores: - template = (a == score) + template = a == score counts = np.expand_dims(np.sum(template, axis), axis) mostfrequent = np.where(counts > oldcounts, score, oldmostfreq) oldcounts = np.maximum(counts, oldcounts) @@ -42,10 +49,16 @@ def get_mode(values, axis=0): def get_median_for_field(field, xform): + """Returns numpy.median() of values in the given field.""" return np.median(get_field_records(field, xform)) +# pylint: disable=invalid-name def get_median_for_numeric_fields_in_form(xform, field=None): + """Get's numpy.median() of values in numeric fields. + + Returns a dict with the fields as key and the median as a value. + """ data = {} for field_name in [field] if field else get_numeric_fields(xform): median = get_median_for_field(field_name, xform) @@ -54,10 +67,16 @@ def get_median_for_numeric_fields_in_form(xform, field=None): def get_mean_for_field(field, xform): + """Returns numpy.mean() of values in the given field.""" return np.mean(get_field_records(field, xform)) +# pylint: disable=invalid-name def get_mean_for_numeric_fields_in_form(xform, field): + """Get's numpy.mean() of values in numeric fields. + + Returns a dict with the fields as key and the mean as a value. + """ data = {} for field_name in [field] if field else get_numeric_fields(xform): mean = get_mean_for_field(field_name, xform) @@ -66,12 +85,18 @@ def get_mean_for_numeric_fields_in_form(xform, field): def get_mode_for_field(field, xform): - a = np.array(get_field_records(field, xform)) - m, count = get_mode(a) + """Returns mode of values in the given field.""" + a = np.array(get_field_records(field, xform)) # pylint: disable=invalid-name + m, _count = get_mode(a) # pylint: disable=invalid-name return m +# pylint: disable=invalid-name def get_mode_for_numeric_fields_in_form(xform, field=None): + """Get's mode of values in numeric fields. + + Returns a dict with the fields as key and the mode as a value. + """ data = {} for field_name in [field] if field else get_numeric_fields(xform): mode = get_mode_for_field(field_name, xform) @@ -80,7 +105,8 @@ def get_mode_for_numeric_fields_in_form(xform, field=None): def get_min_max_range_for_field(field, xform): - a = np.array(get_field_records(field, xform)) + """Returns min, max, range of values in the given field.""" + a = np.array(get_field_records(field, xform)) # pylint: disable=invalid-name _max = np.max(a) _min = np.min(a) _range = _max - _min @@ -88,14 +114,23 @@ def get_min_max_range_for_field(field, xform): def get_min_max_range(xform, field=None): + """Get's min, max, range of values in numeric fields. + + Returns a dict with the fields as key and the min, max, range as a value. + """ data = {} for field_name in [field] if field else get_numeric_fields(xform): _min, _max, _range = get_min_max_range_for_field(field_name, xform) - data[field_name] = {'max': _max, 'min': _min, 'range': _range} + data[field_name] = {"max": _max, "min": _min, "range": _range} return data def get_all_stats(xform, field=None): + """Get's mean, median, mode, min, max, range of values in numeric fields. + + Returns a dict with the fields as key and the mean, median, mode, min, max, + range as a value. + """ data = {} for field_name in [field] if field else get_numeric_fields(xform): _min, _max, _range = get_min_max_range_for_field(field_name, xform) @@ -103,11 +138,11 @@ def get_all_stats(xform, field=None): mean = get_mean_for_field(field_name, xform) median = get_median_for_field(field_name, xform) data[field_name] = { - 'mean': np.round(mean, DECIMAL_PRECISION), - 'median': median, - 'mode': np.round(mode, DECIMAL_PRECISION), - 'max': _max, - 'min': _min, - 'range': _range + "mean": np.round(mean, DECIMAL_PRECISION), + "median": median, + "mode": np.round(mode, DECIMAL_PRECISION), + "max": _max, + "min": _min, + "range": _range, } return data diff --git a/onadata/libs/filters.py b/onadata/libs/filters.py index 78ad7d7c1f..3704c28f8e 100644 --- a/onadata/libs/filters.py +++ b/onadata/libs/filters.py @@ -4,14 +4,14 @@ """ from uuid import UUID -import six - from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from django.http import Http404 from django.shortcuts import get_object_or_404 + +import six from django_filters import rest_framework as django_filter_filters from rest_framework import filters from rest_framework_guardian.filters import ObjectPermissionsFilter @@ -19,10 +19,9 @@ from onadata.apps.api.models import OrganizationProfile, Team from onadata.apps.logger.models import Instance, Project, XForm from onadata.apps.viewer.models import Export -from onadata.libs.utils.numeric import int_or_parse_error -from onadata.libs.utils.common_tags import MEDIA_FILE_TYPES from onadata.libs.permissions import exclude_items_from_queryset_using_xform_meta_perms - +from onadata.libs.utils.common_tags import MEDIA_FILE_TYPES +from onadata.libs.utils.numeric import int_or_parse_error # pylint: disable=invalid-name User = get_user_model() @@ -55,7 +54,7 @@ def filter_queryset(self, request, queryset, view): if form_id: if lookup_field == "pk": int_or_parse_error( - form_id, "Invalid form ID. It must be a positive" " integer" + form_id, "Invalid form ID. It must be a positive integer" ) try: @@ -104,7 +103,6 @@ class XFormListObjectPermissionFilter(AnonDjangoObjectPermissionFilter): class XFormListXFormPKFilter: """Filter forms via 'xform_pk' param.""" - # pylint: disable=no-self-use def filter_queryset(self, request, queryset, view): """Returns an XForm queryset filtered by the 1xform_pk' param.""" xform_pk = view.kwargs.get("xform_pk") @@ -393,7 +391,7 @@ def _instance_filter(self, request, view, keyword): for object_id in [instance_id, project_id]: int_or_parse_error( object_id, - "Invalid value for instanceid. It must be" " a positive integer.", + "Invalid value for instanceid. It must be a positive integer.", ) instance = get_object_or_404(Instance, pk=instance_id) @@ -506,7 +504,7 @@ def filter_queryset(self, request, queryset, view): if instance_id: int_or_parse_error( instance_id, - "Invalid value for instance_id. It must be" " a positive integer.", + "Invalid value for instance_id. It must be a positive integer.", ) instance = get_object_or_404(Instance, pk=instance_id) queryset = queryset.filter(instance=instance) @@ -615,8 +613,8 @@ class UserProfileFilter(filters.BaseFilterBackend): """Filter by the ``users`` query parameter.""" def filter_queryset(self, request, queryset, view): - """Filter by the ``users`` query parameter - returns a queryset of only the users - in the users parameter when `view.action == "list"`""" + """Filter by the ``users`` query parameter - returns a queryset of only the + users in the users parameter when `view.action == "list"`""" if view.action == "list": users = request.GET.get("users") if users: @@ -641,7 +639,7 @@ def filter_queryset(self, request, queryset, view): if instance_id: int_or_parse_error( instance_id, - "Invalid value for instance_id. It must be" " a positive integer", + "Invalid value for instance_id. It must be a positive integer", ) instance = get_object_or_404(Instance, pk=instance_id) @@ -692,7 +690,7 @@ def filter_queryset(self, request, queryset, view): class PublicDatasetsFilter: """Public data set filter where the share attribute is True""" - # pylint: disable=unused-argument,no-self-use + # pylint: disable=unused-argument def filter_queryset(self, request, queryset, view): """Return a queryset of shared=True data if the user is anonymous.""" if request and request.user.is_anonymous: diff --git a/onadata/libs/mixins/anonymous_user_mixin.py b/onadata/libs/mixins/anonymous_user_mixin.py index 7b8110ea21..a920c00086 100644 --- a/onadata/libs/mixins/anonymous_user_mixin.py +++ b/onadata/libs/mixins/anonymous_user_mixin.py @@ -1,14 +1,30 @@ +# -*- coding: utf-8 -*- +""" +Implements AnonymousUserMixin class + +Sets the DB AnonymousUser object to a request user to allow for object permission +checks. +""" from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 +User = get_user_model() + + +class AnonymousUserMixin: + """ + Implements AnonymousUserMixin class -class AnonymousUserMixin(object): + Sets the DB AnonymousUser object to a request user to allow for object permission + checks. + """ def get_queryset(self): """Set AnonymousUser from the database to allow object permissions.""" if self.request and self.request.user.is_anonymous: self.request.user = get_object_or_404( - User, username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME) + User, username__iexact=settings.ANONYMOUS_DEFAULT_USERNAME + ) - return super(AnonymousUserMixin, self).get_queryset() + return super().get_queryset() diff --git a/onadata/libs/mixins/anonymous_user_public_forms_mixin.py b/onadata/libs/mixins/anonymous_user_public_forms_mixin.py index f11ced98e8..6735ebdbcd 100644 --- a/onadata/libs/mixins/anonymous_user_public_forms_mixin.py +++ b/onadata/libs/mixins/anonymous_user_public_forms_mixin.py @@ -1,7 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Implements the AnonymousUserPublicFormsMixin class + +Filters only public forms. +""" from onadata.apps.logger.models.xform import XForm -class AnonymousUserPublicFormsMixin(object): +class AnonymousUserPublicFormsMixin: + """ + Implements the AnonymousUserPublicFormsMixin class + + Filters only public forms. + """ def _get_public_forms_queryset(self): return XForm.objects.filter(shared=True) @@ -11,4 +22,4 @@ def get_queryset(self): if self.request and self.request.user.is_anonymous: return self._get_public_forms_queryset() - return super(AnonymousUserPublicFormsMixin, self).get_queryset() + return super().get_queryset() diff --git a/onadata/libs/mixins/authenticate_header_mixin.py b/onadata/libs/mixins/authenticate_header_mixin.py index 7b22596682..f332ca40f0 100644 --- a/onadata/libs/mixins/authenticate_header_mixin.py +++ b/onadata/libs/mixins/authenticate_header_mixin.py @@ -1,17 +1,31 @@ -from rest_framework.authentication import get_authorization_header -from rest_framework.authentication import TokenAuthentication +# -*- coding: utf-8 -*- +""" +Implements the AuthenticateHeaderMixin class + +Set's the appropriate authentication header using either the TempToken or Token. +""" +from rest_framework.authentication import TokenAuthentication, get_authorization_header + from onadata.libs.authentication import TempTokenAuthentication -class AuthenticateHeaderMixin(object): +class AuthenticateHeaderMixin: + """ + Implements the AuthenticateHeaderMixin class + + Set's the appropriate authentication header using either the TempToken or Token. + """ + def get_authenticate_header(self, request): + """ + Set's the appropriate authentication header using either the TempToken or Token. + """ auth = get_authorization_header(request).split() - if auth and auth[0].lower() == b'token': + if auth and auth[0].lower() == b"token": return TokenAuthentication().authenticate_header(request) - if auth and auth[0].lower() == b'temptoken': + if auth and auth[0].lower() == b"temptoken": return TempTokenAuthentication().authenticate_header(request) - return super(AuthenticateHeaderMixin, self)\ - .get_authenticate_header(request) + return super().get_authenticate_header(request) diff --git a/onadata/libs/mixins/bulk_create_mixin.py b/onadata/libs/mixins/bulk_create_mixin.py index 6ed43607ee..ff5b524319 100644 --- a/onadata/libs/mixins/bulk_create_mixin.py +++ b/onadata/libs/mixins/bulk_create_mixin.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals -class BulkCreateMixin(object): +class BulkCreateMixin: """ Bulk Create Mixin Allows the bulk creation of resources @@ -16,7 +16,7 @@ def get_serializer(self, *args, **kwargs): Gets the appropriate serializer depending on if you are creating a single resource or many resources """ - if isinstance(kwargs.get('data', {}), list): - kwargs['many'] = True + if isinstance(kwargs.get("data", {}), list): + kwargs["many"] = True - return super(BulkCreateMixin, self).get_serializer(*args, **kwargs) + return super().get_serializer(*args, **kwargs) diff --git a/onadata/libs/mixins/cache_control_mixin.py b/onadata/libs/mixins/cache_control_mixin.py index 55ab1c501b..236badf957 100644 --- a/onadata/libs/mixins/cache_control_mixin.py +++ b/onadata/libs/mixins/cache_control_mixin.py @@ -1,21 +1,38 @@ +# -*- coding: utf-8 -*- +""" +Implements the CacheControlMixin class + +Adds Cache headers to a viewsets response. +""" from django.conf import settings from django.utils.cache import patch_cache_control - CACHE_MIXIN_SECONDS = 60 -class CacheControlMixin(object): +class CacheControlMixin: + """ + Implements the CacheControlMixin class + + Adds Cache headers to a viewsets response. + """ + def set_cache_control(self, response, max_age=CACHE_MIXIN_SECONDS): - if hasattr(settings, 'CACHE_MIXIN_SECONDS'): + """Adds Cache headers to a response""" + if hasattr(settings, "CACHE_MIXIN_SECONDS"): max_age = settings.CACHE_MIXIN_SECONDS patch_cache_control(response, max_age=max_age) def finalize_response(self, request, response, *args, **kwargs): - if request.method == 'GET' and not response.streaming and \ - response.status_code in [200, 201, 202]: + """Overrides the finalize_response method + + Adds Cache headers to a response.""" + if ( + request.method == "GET" + and not response.streaming + and response.status_code in [200, 201, 202] + ): self.set_cache_control(response) - return super(CacheControlMixin, self).finalize_response( - request, response, *args, **kwargs) + return super().finalize_response(request, response, *args, **kwargs) diff --git a/onadata/libs/mixins/etags_mixin.py b/onadata/libs/mixins/etags_mixin.py index 4a7e47ba5f..144d81384b 100644 --- a/onadata/libs/mixins/etags_mixin.py +++ b/onadata/libs/mixins/etags_mixin.py @@ -1,12 +1,25 @@ -from builtins import str as text +# -*- coding: utf-8 -*- +""" +Implements the EtagsMixin class + +Adds Etag headers to the viewset response. +""" from hashlib import md5 -MODELS_WITH_DATE_MODIFIED = ('XForm', 'Instance', 'Project', 'Attachment', - 'MetaData', 'Note', 'OrganizationProfile', - 'UserProfile', 'Team') +MODELS_WITH_DATE_MODIFIED = ( + "XForm", + "Instance", + "Project", + "Attachment", + "MetaData", + "Note", + "OrganizationProfile", + "UserProfile", + "Team", +) -class ETagsMixin(object): +class ETagsMixin: """ Applies the Etag on GET responses with status code 200, 201, 202 @@ -15,26 +28,31 @@ class ETagsMixin(object): """ def set_etag_header(self, etag_value, etag_hash=None): + """Updates the response headers with Etag header""" if etag_value: - etag_hash = md5(text(etag_value).encode('utf-8')).hexdigest() + etag_hash = md5(str(etag_value).encode("utf-8")).hexdigest() if etag_hash: - self.headers.update({'ETag': etag_hash}) + self.headers.update({"ETag": etag_hash}) def finalize_response(self, request, response, *args, **kwargs): - if request.method == 'GET' and not response.streaming and \ - response.status_code in [200, 201, 202]: + """Overrides the finalize_response method + + Adds the Etag header to response.""" + if ( + request.method == "GET" + and not response.streaming + and response.status_code in [200, 201, 202] + ): etag_value = None - if hasattr(self, 'etag_data') and self.etag_data: + if hasattr(self, "etag_data") and self.etag_data: etag_value = str(self.etag_data) - elif hasattr(self, 'object'): + elif hasattr(self, "object"): if self.object.__class__.__name__ in MODELS_WITH_DATE_MODIFIED: etag_value = self.object.date_modified - if hasattr(self, 'etag_hash') and self.etag_hash: + if hasattr(self, "etag_hash") and self.etag_hash: self.set_etag_header(None, self.etag_hash) else: self.set_etag_header(etag_value) - return super(ETagsMixin, self).finalize_response( - request, response, *args, **kwargs - ) + return super().finalize_response(request, response, *args, **kwargs) diff --git a/onadata/libs/mixins/last_modified_mixin.py b/onadata/libs/mixins/last_modified_mixin.py index d028ebba76..ab53d12191 100644 --- a/onadata/libs/mixins/last_modified_mixin.py +++ b/onadata/libs/mixins/last_modified_mixin.py @@ -1,30 +1,41 @@ +# -*- coding: utf-8 -*- +""" +Implements the LastModifiedMixin class + +Adds the Last-Modified header to a viewset response. +""" import types -from onadata.libs.utils.timing import last_modified_header, get_date + +from onadata.libs.utils.timing import get_date, last_modified_header -class LastModifiedMixin(object): +class LastModifiedMixin: + """ + Implements the LastModifiedMixin class - last_modified_field = 'modified' + Adds the Last-Modified header to a viewset response. + """ + + last_modified_field = "modified" last_modified_date = None def finalize_response(self, request, response, *args, **kwargs): - if request.method == 'GET' and response.status_code < 300: + """Overrides the finalize_response method + + Adds the Last-Modified header to a viewset response.""" + if request.method == "GET" and response.status_code < 300: if self.last_modified_date is not None: - self.headers.update( - last_modified_header(self.last_modified_date)) + self.headers.update(last_modified_header(self.last_modified_date)) else: obj = None - if hasattr(self, 'object_list'): - generator_type = isinstance(self.object_list, - types.GeneratorType) - if isinstance(self.object_list, list) \ - and len(self.object_list): + if hasattr(self, "object_list"): + generator_type = isinstance(self.object_list, types.GeneratorType) + if isinstance(self.object_list, list) and len(self.object_list): obj = self.object_list[len(self.object_list) - 1] - elif not isinstance(self.object_list, list) and \ - not generator_type: + elif not isinstance(self.object_list, list) and not generator_type: obj = self.object_list.last() - if hasattr(self, 'object'): + if hasattr(self, "object"): obj = self.object if not obj: @@ -32,5 +43,4 @@ def finalize_response(self, request, response, *args, **kwargs): self.headers.update(last_modified_header(get_date(obj))) - return super(LastModifiedMixin, self).finalize_response( - request, response, *args, **kwargs) + return super().finalize_response(request, response, *args, **kwargs) diff --git a/onadata/libs/mixins/multi_lookup_mixin.py b/onadata/libs/mixins/multi_lookup_mixin.py index ca7f6a127e..2f623b20d6 100644 --- a/onadata/libs/mixins/multi_lookup_mixin.py +++ b/onadata/libs/mixins/multi_lookup_mixin.py @@ -1,15 +1,29 @@ +# -*- coding: utf-8 -*- +""" +Implements MultiLookupMixin class + +Looks up an object using multiple lookup fields. +""" from django.shortcuts import get_object_or_404 + from rest_framework import serializers from rest_framework.exceptions import ParseError -class MultiLookupMixin(object): +class MultiLookupMixin: + """ + Implements MultiLookupMixin class + + Looks up an object using multiple lookup fields. + """ + def get_object(self, queryset=None): + """Looks up an object using multiple lookup fields.""" if queryset is None: queryset = self.filter_queryset(self.get_queryset()) filter_kwargs = {} serializer = self.get_serializer() - lookup_fields = getattr(self, 'lookup_fields', []) + lookup_fields = getattr(self, "lookup_fields", []) for field in lookup_fields: lookup_field = field @@ -20,15 +34,13 @@ def get_object(self, queryset=None): if isinstance(k, serializers.HyperlinkedRelatedField): if k.source: lookup_field = k.source - lookup_field = '%s__%s' % (lookup_field, k.lookup_field) + lookup_field = f"{lookup_field}__{k.lookup_field}" if self.kwargs.get(field, None) is None: - raise ParseError( - 'Expected URL keyword argument `%s`.' % field - ) + raise ParseError(f"Expected URL keyword argument `{field}`.") filter_kwargs[lookup_field] = self.kwargs[field] - obj = get_object_or_404(queryset, **filter_kwargs) + obj = get_object_or_404(queryset, **filter_kwargs) # May raise a permission denied self.check_object_permissions(self.request, obj) diff --git a/onadata/libs/mixins/object_lookup_mixin.py b/onadata/libs/mixins/object_lookup_mixin.py index d0ae27d926..fac3584fac 100644 --- a/onadata/libs/mixins/object_lookup_mixin.py +++ b/onadata/libs/mixins/object_lookup_mixin.py @@ -1,18 +1,30 @@ +# -*- coding: utf-8 -*- +""" +Implements ObjectLookupMixin class + +Incase the lookup is on an object that has been hyperlinked +then update the queryset filter appropriately +""" from rest_framework import serializers from rest_framework.exceptions import ParseError from rest_framework.generics import get_object_or_404 -class ObjectLookupMixin(object): +class ObjectLookupMixin: + """ + Implements ObjectLookupMixin class + + Incase the lookup is on an object that has been hyperlinked + then update the queryset filter appropriately + """ + def get_object(self, queryset=None): """ Incase the lookup is on an object that has been hyperlinked then update the queryset filter appropriately """ if self.kwargs.get(self.lookup_field, None) is None: - raise ParseError( - 'Expected URL keyword argument `%s`.' % self.lookup_field - ) + raise ParseError(f"Expected URL keyword argument `{self.lookup_field}`.") if queryset is None: queryset = self.filter_queryset(self.get_queryset()) @@ -23,7 +35,7 @@ def get_object(self, queryset=None): if self.lookup_field in serializer.get_fields(): k = serializer.get_fields()[self.lookup_field] if isinstance(k, serializers.HyperlinkedRelatedField): - lookup_field = '%s__%s' % (self.lookup_field, k.lookup_field) + lookup_field = f"{self.lookup_field}__{k.lookup_field}" filter_kwargs[lookup_field] = self.kwargs[self.lookup_field] diff --git a/onadata/libs/mixins/openrosa_headers_mixin.py b/onadata/libs/mixins/openrosa_headers_mixin.py index 9cc5b10d62..fd61389a9f 100644 --- a/onadata/libs/mixins/openrosa_headers_mixin.py +++ b/onadata/libs/mixins/openrosa_headers_mixin.py @@ -1,14 +1,15 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ OpenRosaHeadersMixin module """ from datetime import datetime -import pytz from django.conf import settings +import pytz + # 10,000,000 bytes -DEFAULT_CONTENT_LENGTH = getattr(settings, 'DEFAULT_CONTENT_LENGTH', 10000000) +DEFAULT_CONTENT_LENGTH = getattr(settings, "DEFAULT_CONTENT_LENGTH", 10000000) def get_openrosa_headers(request, location=True): @@ -18,18 +19,18 @@ def get_openrosa_headers(request, location=True): """ now = datetime.now(pytz.timezone(settings.TIME_ZONE)) data = { - 'Date': now.strftime('%a, %d %b %Y %H:%M:%S %Z'), - 'X-OpenRosa-Version': '1.0', - 'X-OpenRosa-Accept-Content-Length': DEFAULT_CONTENT_LENGTH + "Date": now.strftime("%a, %d %b %Y %H:%M:%S %Z"), + "X-OpenRosa-Version": "1.0", + "X-OpenRosa-Accept-Content-Length": DEFAULT_CONTENT_LENGTH, } if location: - data['Location'] = request.build_absolute_uri(request.path) + data["Location"] = request.build_absolute_uri(request.path) return data -class OpenRosaHeadersMixin(object): # pylint: disable=R0903 +class OpenRosaHeadersMixin: """ OpenRosaHeadersMixin class - sets OpenRosa headers in a response for a View or Viewset. @@ -41,5 +42,4 @@ def finalize_response(self, request, response, *args, **kwargs): """ self.headers.update(get_openrosa_headers(request)) - return super(OpenRosaHeadersMixin, self).finalize_response( - request, response, *args, **kwargs) + return super().finalize_response(request, response, *args, **kwargs) diff --git a/onadata/libs/mixins/profiler_mixin.py b/onadata/libs/mixins/profiler_mixin.py index e4b6e08e46..991f7598f0 100644 --- a/onadata/libs/mixins/profiler_mixin.py +++ b/onadata/libs/mixins/profiler_mixin.py @@ -1,64 +1,80 @@ +# -*- coding: utf-8 -*- +""" +Implements a ProfilerMixin - profiles a Django Rest Framework viewset. +""" import logging import time + from django.conf import settings -from django.core.signals import request_started, request_finished +from django.core.signals import request_finished, request_started from django.http import StreamingHttpResponse from rest_framework.fields import empty +project_viewset_profiler = logging.getLogger("profiler_logger") -project_viewset_profiler = logging.getLogger('profiler_logger') +DISPATCH_TIME = 0 +RENDER_TIME = 0 +STARTED = 0 +SERIALIZER_TIME = 0 -class ProfilerMixin(object): +class ProfilerMixin: + """ + Implements a ProfilerMixin - profiles a Django Rest Framework viewset. + """ def get_serializer(self, instance=None, data=empty, **kwargs): + """Override the get_serializer() method.""" serializer_class = self.get_serializer_class() - kwargs['context'] = self.get_serializer_context() + kwargs["context"] = self.get_serializer_context() if settings.PROFILE_API_ACTION_FUNCTION: - global serializer_time + global SERIALIZER_TIME # pylint: disable=global-statement serializer_start = time.time() serializer = serializer_class(instance, data=data, **kwargs) - serializer_time = time.time() - serializer_start + SERIALIZER_TIME = time.time() - serializer_start return serializer return serializer_class(instance, data=data, **kwargs) def dispatch(self, request, *args, **kwargs): - global render_time - global dispatch_time + """Override the viewset dispatch method.""" + global RENDER_TIME # pylint: disable=global-statement + global DISPATCH_TIME # pylint: disable=global-statement dispatch_start = time.time() - ret = super(ProfilerMixin, self).dispatch(request, *args, **kwargs) + ret = super().dispatch(request, *args, **kwargs) if not isinstance(ret, StreamingHttpResponse): render_start = time.time() ret.render() - render_time = time.time() - render_start + RENDER_TIME = time.time() - render_start else: - render_time = 0 - dispatch_time = time.time() - dispatch_start + RENDER_TIME = 0 + DISPATCH_TIME = time.time() - dispatch_start return ret -def started(sender, **kwargs): - global started - started = time.time() +def started(sender, **kwargs): # pylint: disable=unused-argument + """Signal that starts the timer""" + global STARTED # pylint: disable=global-statement + STARTED = time.time() -def finished(sender, **kwargs): +def finished(sender, **kwargs): # pylint: disable=unused-argument + """Signal that captures the end of the timer""" try: - total = time.time() - started + total = time.time() - STARTED api_view_time = dispatch_time - (render_time + serializer_time) request_response_time = total - dispatch_time + # pylint: disable=consider-using-f-string output = "\n" output += "Serialization | %.4fs\n" % serializer_time - output += "Django request/response | %.4fs\n" %\ - request_response_time + output += "Django request/response | %.4fs\n" % request_response_time output += "API view | %.4fs\n" % api_view_time output += "Response rendering | %.4fs\n" % render_time diff --git a/onadata/libs/mixins/xform_id_string_lookup.py b/onadata/libs/mixins/xform_id_string_lookup.py index c41dc2fc00..a42689257a 100644 --- a/onadata/libs/mixins/xform_id_string_lookup.py +++ b/onadata/libs/mixins/xform_id_string_lookup.py @@ -1,11 +1,27 @@ +# -*- coding: utf-8 -*- +""" +XForm id_strng lookup mixin class + +Looks up an XForm using the id_string. +""" from django.core.exceptions import ImproperlyConfigured from django.shortcuts import get_object_or_404 -class XFormIdStringLookupMixin(object): - lookup_id_string = 'id_string' +class XFormIdStringLookupMixin: + """ + XForm id_strng lookup mixin class + + Looks up an XForm using the id_string. + """ + + lookup_id_string = "id_string" def get_object(self, queryset=None): + """Looks up an XForm object using the ``id_string`` + + Returns the XForm object or raises a 404 HTTP response exception + """ if queryset is None: queryset = self.filter_queryset(self.get_queryset()) @@ -19,10 +35,10 @@ def get_object(self, queryset=None): lookup_field = self.lookup_id_string else: raise ImproperlyConfigured( - 'Expected view %s to be called with a URL keyword argument ' - 'named "%s". Fix your URL conf, or set the `.lookup_field` ' - 'attribute on the view correctly.' % - (self.__class__.__name__, self.lookup_field) + f"Expected view {self.__class__.__name__} to be called with a " + f'URL keyword argument named "{self.lookup_field}". ' + "Fix your URL conf, or set the `.lookup_field` " + "attribute on the view correctly." ) filter_kwargs = {lookup_field: lookup} diff --git a/onadata/libs/models/signals.py b/onadata/libs/models/signals.py index 5c395078a2..e8711a13cc 100644 --- a/onadata/libs/models/signals.py +++ b/onadata/libs/models/signals.py @@ -4,9 +4,9 @@ from onadata.apps.logger.models import XForm -# pylint: disable=unexpected-keyword-arg -xform_tags_add = django.dispatch.Signal(providing_args=["xform", "tags"]) -xform_tags_delete = django.dispatch.Signal(providing_args=["xform", "tag"]) +# pylint: disable=invalid-name +xform_tags_add = django.dispatch.Signal() +xform_tags_delete = django.dispatch.Signal() # pylint: disable=unused-argument diff --git a/onadata/libs/models/sorting.py b/onadata/libs/models/sorting.py index 7e0935f7f7..708db7b209 100644 --- a/onadata/libs/models/sorting.py +++ b/onadata/libs/models/sorting.py @@ -1,12 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Sorting utility functions. +""" import json -import six from typing import Dict +import six + def sort_from_mongo_sort_str(sort_str): + """Create a sort query list based on MongoDB sort string input.""" sort_values = [] if isinstance(sort_str, six.string_types): - if sort_str.startswith('{'): + if sort_str.startswith("{"): sort_dict = json.loads(sort_str) for k, v in sort_dict.items(): try: @@ -14,7 +20,7 @@ def sort_from_mongo_sort_str(sort_str): except ValueError: pass if v < 0: - k = u'-{}'.format(k) + k = f"-{k}" sort_values.append(k) else: sort_values.append(sort_str) @@ -22,34 +28,41 @@ def sort_from_mongo_sort_str(sort_str): return sort_values -def json_order_by( - sort_list, none_json_fields: Dict = {}, model_name: str = ""): +def json_order_by(sort_list, none_json_fields: Dict = None, model_name: str = ""): + """Returns SQL ORDER BY string portion based on JSON input.""" _list = [] + if none_json_fields is None: + none_json_fields = {} for field in sort_list: - field_key = field.lstrip('-') - _str = u" json->>%s"\ - if field_key not in none_json_fields.keys() else\ - f'"{model_name}"."{none_json_fields.get(field_key)}"' + field_key = field.lstrip("-") + _str = ( + " json->>%s" + if field_key not in none_json_fields + else f'"{model_name}"."{none_json_fields.get(field_key)}"' + ) - if field.startswith('-'): - _str += u" DESC" + if field.startswith("-"): + _str += " DESC" else: - _str += u" ASC" + _str += " ASC" _list.append(_str) if len(_list) > 0: - return u"ORDER BY {}".format(u",".join(_list)) + return f'ORDER BY {",".join(_list)}' - return u"" + return "" -def json_order_by_params(sort_list, none_json_fields: Dict = {}): +def json_order_by_params(sort_list, none_json_fields: Dict = None): + """Creates the ORDER BY parameters list from JSON input.""" params = [] + if none_json_fields is None: + none_json_fields = {} for field in sort_list: - field = field.lstrip('-') - if field not in none_json_fields.keys(): - params.append(field.lstrip('-')) + field = field.lstrip("-") + if field not in none_json_fields: + params.append(field.lstrip("-")) return params diff --git a/onadata/libs/pagination.py b/onadata/libs/pagination.py index 06888f4e62..10b6317409 100644 --- a/onadata/libs/pagination.py +++ b/onadata/libs/pagination.py @@ -1,38 +1,58 @@ -from django.core.paginator import Paginator +# -*- coding: utf-8 -*- +""" +Pagination classes. +""" from django.conf import settings +from django.core.paginator import Paginator from django.db.models import QuerySet +from django.utils.functional import cached_property + from rest_framework.pagination import ( - PageNumberPagination, InvalidPage, NotFound, replace_query_param) + InvalidPage, + NotFound, + PageNumberPagination, + replace_query_param, +) from rest_framework.request import Request class StandardPageNumberPagination(PageNumberPagination): + """The Standard PageNumberPagination class + + Set's the default ``page_size`` to 1000 with a maximum page_size of 10,000 records + per page. + """ + page_size = 1000 - page_size_query_param = 'page_size' - max_page_size = getattr( - settings, "STANDARD_PAGINATION_MAX_PAGE_SIZE", 10000) + page_size_query_param = "page_size" + max_page_size = getattr(settings, "STANDARD_PAGINATION_MAX_PAGE_SIZE", 10000) def get_first_page_link(self): + """Returns the URL to the first page.""" if self.page.number == 1: return None + url = self.request.build_absolute_uri() + return replace_query_param(url, self.page_query_param, 1) def get_last_page_link(self): + """Returns the URL to the last page.""" if self.page.number == self.paginator.num_pages: return None + url = self.request.build_absolute_uri() - return replace_query_param( - url, self.page_query_param, self.paginator.num_pages) - def generate_link_header( - self, request: Request, queryset: QuerySet - ): + return replace_query_param(url, self.page_query_param, self.paginator.num_pages) + + def generate_link_header(self, request: Request, queryset: QuerySet): + """Generates pagination headers for a HTTP response object""" links = [] page_size = self.get_page_size(request) if not page_size: return {} page_number = request.query_params.get(self.page_query_param, 1) + # pylint: disable=attribute-defined-outside-init self.paginator = self.django_paginator_class(queryset, page_size) self.request = request @@ -42,27 +62,41 @@ def generate_link_header( return {} for rel, link in ( - ('prev', self.get_previous_link()), - ('next', self.get_next_link()), - ('last', self.get_last_page_link()), - ('first', self.get_first_page_link())): + ("prev", self.get_previous_link()), + ("next", self.get_next_link()), + ("last", self.get_last_page_link()), + ("first", self.get_first_page_link()), + ): if link: links.append(f'<{link}>; rel="{rel}"') - return {'Link': ', '.join(links)} + return {"Link": ", ".join(links)} class CountOverridablePaginator(Paginator): + """Count override Paginator + + Allows overriding the count especially in the event it may be expensive request. + """ + + # pylint: disable=too-many-arguments def __init__( - self, object_list, per_page, - orphans: int = 0, allow_empty_first_page: bool = True, - count_override: int = None) -> None: + self, + object_list, + per_page, + orphans: int = 0, + allow_empty_first_page: bool = True, + count_override: int = None, + ) -> None: self.count_override = count_override super().__init__( - object_list, per_page, - orphans=orphans, allow_empty_first_page=allow_empty_first_page) + object_list, + per_page, + orphans=orphans, + allow_empty_first_page=allow_empty_first_page, + ) - @property + @cached_property def count(self): if self.count_override: return self.count_override @@ -70,17 +104,21 @@ def count(self): class CountOverridablePageNumberPagination(StandardPageNumberPagination): + """Count override PageNumberPagination + + Allows overriding the count especially in the event it may be expensive request. + """ + django_paginator_class = CountOverridablePaginator def paginate_queryset(self, queryset, request, view, count=None): + # pylint: disable=attribute-defined-outside-init page_size = self.get_page_size(request) if not page_size: return None paginator = self.django_paginator_class( - queryset, - page_size, - count_override=count + queryset, page_size, count_override=count ) page_number = request.query_params.get(self.page_query_param, 1) if page_number in self.last_page_strings: @@ -89,9 +127,10 @@ def paginate_queryset(self, queryset, request, view, count=None): try: self.page = paginator.page(page_number) except InvalidPage as exc: - msg = self.invalid_page_message.format(page_number=page_number, - message=str(exc)) - raise NotFound(msg) + msg = self.invalid_page_message.format( + page_number=page_number, message=str(exc) + ) + raise NotFound(msg) from exc if paginator.num_pages > 1 and self.template is not None: self.display_page_controls = True diff --git a/onadata/libs/permissions.py b/onadata/libs/permissions.py index 91b8b8ced3..c3715f3420 100644 --- a/onadata/libs/permissions.py +++ b/onadata/libs/permissions.py @@ -1,84 +1,93 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Permissions module. """ import json from collections import defaultdict -import six -from django.db.models.base import ModelBase +from django.apps import apps from django.db.models import Q -from guardian.shortcuts import (assign_perm, get_perms, get_users_with_perms, - remove_perm) - -from onadata.apps.api.models import OrganizationProfile -from onadata.apps.logger.models import MergedXForm, Project, XForm,\ - Attachment -from onadata.apps.logger.models.project import (ProjectGroupObjectPermission, - ProjectUserObjectPermission) -from onadata.apps.logger.models.xform import (XFormGroupObjectPermission, - XFormUserObjectPermission) -from onadata.apps.main.models.user_profile import UserProfile -from onadata.apps.viewer.models import DataDictionary +from django.db.models.base import ModelBase + +import six +from guardian.shortcuts import assign_perm, get_perms, get_users_with_perms, remove_perm + +from onadata.apps.logger.models.attachment import Attachment +from onadata.apps.logger.models.project import ( + Project, + ProjectGroupObjectPermission, + ProjectUserObjectPermission, +) +from onadata.apps.logger.models.xform import ( + XForm, + XFormGroupObjectPermission, + XFormUserObjectPermission, +) from onadata.libs.exceptions import NoRecordsPermission from onadata.libs.utils.common_tags import XFORM_META_PERMS from onadata.libs.utils.model_tools import queryset_iterator # Userprofile Permissions -CAN_ADD_USERPROFILE = 'add_userprofile' -CAN_CHANGE_USERPROFILE = 'change_userprofile' -CAN_DELETE_USERPROFILE = 'delete_userprofile' -CAN_ADD_PROJECT_TO_PROFILE = 'can_add_project' -CAN_ADD_XFORM_TO_PROFILE = 'can_add_xform' -CAN_VIEW_PROFILE = 'view_profile' +CAN_ADD_USERPROFILE = "add_userprofile" +CAN_CHANGE_USERPROFILE = "change_userprofile" +CAN_DELETE_USERPROFILE = "delete_userprofile" +CAN_ADD_PROJECT_TO_PROFILE = "can_add_project" +CAN_ADD_XFORM_TO_PROFILE = "can_add_xform" +CAN_VIEW_PROFILE = "view_profile" # Organization Permissions -CAN_VIEW_ORGANIZATION_PROFILE = 'view_organizationprofile' -CAN_ADD_ORGANIZATION_PROFILE = 'add_organizationprofile' -CAN_ADD_ORGANIZATION_PROJECT = 'can_add_project' -CAN_ADD_ORGANIZATION_XFORM = 'can_add_xform' -CAN_CHANGE_ORGANIZATION_PROFILE = 'change_organizationprofile' -CAN_DELETE_ORGANIZATION_PROFILE = 'delete_organizationprofile' -IS_ORGANIZATION_OWNER = 'is_org_owner' - -# Xform Permissions -CAN_CHANGE_XFORM = 'change_xform' -CAN_ADD_XFORM = 'add_xform' -CAN_DELETE_XFORM = 'delete_xform' -CAN_VIEW_XFORM = 'view_xform' -CAN_VIEW_XFORM_DATA = 'view_xform_data' -CAN_VIEW_XFORM_ALL = 'view_xform_all' -CAN_ADD_SUBMISSIONS = 'report_xform' -CAN_DELETE_SUBMISSION = 'delete_submission' -CAN_TRANSFER_OWNERSHIP = 'transfer_xform' -CAN_MOVE_TO_FOLDER = 'move_xform' -CAN_EXPORT_XFORM = 'can_export_xform_data' +CAN_VIEW_ORGANIZATION_PROFILE = "view_organizationprofile" +CAN_ADD_ORGANIZATION_PROFILE = "add_organizationprofile" +CAN_ADD_ORGANIZATION_PROJECT = "can_add_project" +CAN_ADD_ORGANIZATION_XFORM = "can_add_xform" +CAN_CHANGE_ORGANIZATION_PROFILE = "change_organizationprofile" +CAN_DELETE_ORGANIZATION_PROFILE = "delete_organizationprofile" +IS_ORGANIZATION_OWNER = "is_org_owner" + +# XForm Permissions +CAN_CHANGE_XFORM = "change_xform" +CAN_ADD_XFORM = "add_xform" +CAN_DELETE_XFORM = "delete_xform" +CAN_VIEW_XFORM = "view_xform" +CAN_VIEW_XFORM_DATA = "view_xform_data" +CAN_VIEW_XFORM_ALL = "view_xform_all" +CAN_ADD_SUBMISSIONS = "report_xform" +CAN_DELETE_SUBMISSION = "delete_submission" +CAN_TRANSFER_OWNERSHIP = "transfer_xform" +CAN_MOVE_TO_FOLDER = "move_xform" +CAN_EXPORT_XFORM = "can_export_xform_data" # MergedXform Permissions -CAN_VIEW_MERGED_XFORM = 'view_mergedxform' +CAN_VIEW_MERGED_XFORM = "view_mergedxform" # Project Permissions -CAN_ADD_PROJECT = 'add_project' -CAN_VIEW_PROJECT = 'view_project' -CAN_VIEW_PROJECT_DATA = 'view_project_data' -CAN_VIEW_PROJECT_ALL = 'view_project_all' -CAN_CHANGE_PROJECT = 'change_project' -CAN_TRANSFER_PROJECT_OWNERSHIP = 'transfer_project' -CAN_DELETE_PROJECT = 'delete_project' -CAN_ADD_PROJECT_XFORM = 'add_project_xform' -CAN_ADD_SUBMISSIONS_PROJECT = 'report_project_xform' -CAN_EXPORT_PROJECT = 'can_export_project_data' +CAN_ADD_PROJECT = "add_project" +CAN_VIEW_PROJECT = "view_project" +CAN_VIEW_PROJECT_DATA = "view_project_data" +CAN_VIEW_PROJECT_ALL = "view_project_all" +CAN_CHANGE_PROJECT = "change_project" +CAN_TRANSFER_PROJECT_OWNERSHIP = "transfer_project" +CAN_DELETE_PROJECT = "delete_project" +CAN_ADD_PROJECT_XFORM = "add_project_xform" +CAN_ADD_SUBMISSIONS_PROJECT = "report_project_xform" +CAN_EXPORT_PROJECT = "can_export_project_data" # Data dictionary permissions -CAN_ADD_DATADICTIONARY = 'add_datadictionary' -CAN_CHANGE_DATADICTIONARY = 'change_datadictionary' -CAN_DELETE_DATADICTIONARY = 'delete_datadictionary' +CAN_ADD_DATADICTIONARY = "add_datadictionary" +CAN_CHANGE_DATADICTIONARY = "change_datadictionary" +CAN_DELETE_DATADICTIONARY = "delete_datadictionary" + +DataDictionary = apps.get_model("viewer", "DataDictionary") +MergedXForm = apps.get_model("logger", "MergedXForm") +OrganizationProfile = apps.get_model("api", "OrganizationProfile") +UserProfile = apps.get_model("main", "UserProfile") -class Role(object): +class Role: """ Base Role class. """ + class_to_permissions = defaultdict(list) name = None @@ -135,12 +144,16 @@ class ReadOnlyRoleNoDownload(Role): """ Read-only no download Role class. """ - name = 'readonly-no-download' - permissions = ((CAN_VIEW_ORGANIZATION_PROFILE, - OrganizationProfile), (CAN_VIEW_XFORM, XForm), - (CAN_VIEW_PROJECT, Project), (CAN_VIEW_XFORM_ALL, XForm), - (CAN_VIEW_PROJECT_ALL, Project), (CAN_VIEW_MERGED_XFORM, - MergedXForm), ) + + name = "readonly-no-download" + permissions = ( + (CAN_VIEW_ORGANIZATION_PROFILE, OrganizationProfile), + (CAN_VIEW_XFORM, XForm), + (CAN_VIEW_PROJECT, Project), + (CAN_VIEW_XFORM_ALL, XForm), + (CAN_VIEW_PROJECT_ALL, Project), + (CAN_VIEW_MERGED_XFORM, MergedXForm), + ) class_to_permissions = { MergedXForm: [CAN_VIEW_MERGED_XFORM], @@ -153,7 +166,8 @@ class ReadOnlyRole(Role): """ Read-only Role class. """ - name = 'readonly' + + name = "readonly" class_to_permissions = { MergedXForm: [CAN_VIEW_MERGED_XFORM], @@ -167,13 +181,13 @@ class DataEntryOnlyRole(Role): """ Data-Entry only Role class. """ - name = 'dataentry-only' + + name = "dataentry-only" class_to_permissions = { MergedXForm: [CAN_VIEW_MERGED_XFORM], OrganizationProfile: [CAN_VIEW_ORGANIZATION_PROFILE], - Project: - [CAN_ADD_SUBMISSIONS_PROJECT, CAN_EXPORT_PROJECT, CAN_VIEW_PROJECT], + Project: [CAN_ADD_SUBMISSIONS_PROJECT, CAN_EXPORT_PROJECT, CAN_VIEW_PROJECT], XForm: [CAN_ADD_SUBMISSIONS], } @@ -183,17 +197,22 @@ class DataEntryMinorRole(Role): Data-Entry minor Role class - user can submit and has readonly access to data they submitted. """ - name = 'dataentry-minor' + + name = "dataentry-minor" class_to_permissions = { MergedXForm: [CAN_VIEW_MERGED_XFORM], OrganizationProfile: [CAN_VIEW_ORGANIZATION_PROFILE], Project: [ - CAN_ADD_SUBMISSIONS_PROJECT, CAN_EXPORT_PROJECT, CAN_VIEW_PROJECT, - CAN_VIEW_PROJECT_DATA + CAN_ADD_SUBMISSIONS_PROJECT, + CAN_EXPORT_PROJECT, + CAN_VIEW_PROJECT, + CAN_VIEW_PROJECT_DATA, ], XForm: [ - CAN_ADD_SUBMISSIONS, CAN_EXPORT_XFORM, CAN_VIEW_XFORM, - CAN_VIEW_XFORM_DATA + CAN_ADD_SUBMISSIONS, + CAN_EXPORT_XFORM, + CAN_VIEW_XFORM, + CAN_VIEW_XFORM_DATA, ], } @@ -203,17 +222,24 @@ class DataEntryRole(Role): Data-Entry Role class - user can submit data and has readonly permissions to all the data including data submitted by others. """ - name = 'dataentry' + + name = "dataentry" class_to_permissions = { MergedXForm: [CAN_VIEW_MERGED_XFORM], OrganizationProfile: [CAN_VIEW_ORGANIZATION_PROFILE], Project: [ - CAN_ADD_SUBMISSIONS_PROJECT, CAN_EXPORT_PROJECT, CAN_VIEW_PROJECT, - CAN_VIEW_PROJECT_ALL, CAN_VIEW_PROJECT_DATA + CAN_ADD_SUBMISSIONS_PROJECT, + CAN_EXPORT_PROJECT, + CAN_VIEW_PROJECT, + CAN_VIEW_PROJECT_ALL, + CAN_VIEW_PROJECT_DATA, ], XForm: [ - CAN_ADD_SUBMISSIONS, CAN_EXPORT_XFORM, CAN_VIEW_XFORM, - CAN_VIEW_XFORM_ALL, CAN_VIEW_XFORM_DATA + CAN_ADD_SUBMISSIONS, + CAN_EXPORT_XFORM, + CAN_VIEW_XFORM, + CAN_VIEW_XFORM_ALL, + CAN_VIEW_XFORM_DATA, ], } @@ -223,17 +249,25 @@ class EditorMinorRole(Role): Editor-Minor Role class - user can submit data, read and edit only the data they submitted. """ - name = 'editor-minor' + + name = "editor-minor" class_to_permissions = { MergedXForm: [CAN_VIEW_MERGED_XFORM], OrganizationProfile: [CAN_VIEW_ORGANIZATION_PROFILE], Project: [ - CAN_ADD_SUBMISSIONS_PROJECT, CAN_CHANGE_PROJECT, - CAN_EXPORT_PROJECT, CAN_VIEW_PROJECT, CAN_VIEW_PROJECT_DATA + CAN_ADD_SUBMISSIONS_PROJECT, + CAN_CHANGE_PROJECT, + CAN_EXPORT_PROJECT, + CAN_VIEW_PROJECT, + CAN_VIEW_PROJECT_DATA, ], XForm: [ - CAN_ADD_SUBMISSIONS, CAN_CHANGE_XFORM, CAN_DELETE_SUBMISSION, - CAN_EXPORT_XFORM, CAN_VIEW_XFORM, CAN_VIEW_XFORM_DATA + CAN_ADD_SUBMISSIONS, + CAN_CHANGE_XFORM, + CAN_DELETE_SUBMISSION, + CAN_EXPORT_XFORM, + CAN_VIEW_XFORM, + CAN_VIEW_XFORM_DATA, ], } @@ -242,19 +276,27 @@ class EditorRole(Role): """ Editor Role class - user can submit, read and edit any submitted data. """ - name = 'editor' + + name = "editor" class_to_permissions = { MergedXForm: [CAN_VIEW_MERGED_XFORM], OrganizationProfile: [CAN_VIEW_ORGANIZATION_PROFILE], Project: [ - CAN_ADD_SUBMISSIONS_PROJECT, CAN_CHANGE_PROJECT, - CAN_EXPORT_PROJECT, CAN_VIEW_PROJECT, CAN_VIEW_PROJECT_ALL, - CAN_VIEW_PROJECT_DATA + CAN_ADD_SUBMISSIONS_PROJECT, + CAN_CHANGE_PROJECT, + CAN_EXPORT_PROJECT, + CAN_VIEW_PROJECT, + CAN_VIEW_PROJECT_ALL, + CAN_VIEW_PROJECT_DATA, ], XForm: [ - CAN_ADD_SUBMISSIONS, CAN_CHANGE_XFORM, CAN_DELETE_SUBMISSION, - CAN_EXPORT_XFORM, CAN_VIEW_XFORM, CAN_VIEW_XFORM_ALL, - CAN_VIEW_XFORM_DATA + CAN_ADD_SUBMISSIONS, + CAN_CHANGE_XFORM, + CAN_DELETE_SUBMISSION, + CAN_EXPORT_XFORM, + CAN_VIEW_XFORM, + CAN_VIEW_XFORM_ALL, + CAN_VIEW_XFORM_DATA, ], } @@ -264,24 +306,40 @@ class ManagerRole(Role): Manager Role class - user can add,delete,edit forms and data as well as control access to data, forms and projects. """ - name = 'manager' + + name = "manager" class_to_permissions = { MergedXForm: [CAN_VIEW_MERGED_XFORM], - OrganizationProfile: - [CAN_ADD_ORGANIZATION_PROJECT, CAN_ADD_ORGANIZATION_XFORM, - CAN_VIEW_ORGANIZATION_PROFILE], + OrganizationProfile: [ + CAN_ADD_ORGANIZATION_PROJECT, + CAN_ADD_ORGANIZATION_XFORM, + CAN_VIEW_ORGANIZATION_PROFILE, + ], Project: [ - CAN_ADD_PROJECT, CAN_ADD_PROJECT_XFORM, - CAN_ADD_SUBMISSIONS_PROJECT, CAN_CHANGE_PROJECT, - CAN_EXPORT_PROJECT, CAN_VIEW_PROJECT, CAN_VIEW_PROJECT_ALL, - CAN_VIEW_PROJECT_DATA + CAN_ADD_PROJECT, + CAN_ADD_PROJECT_XFORM, + CAN_ADD_SUBMISSIONS_PROJECT, + CAN_CHANGE_PROJECT, + CAN_EXPORT_PROJECT, + CAN_VIEW_PROJECT, + CAN_VIEW_PROJECT_ALL, + CAN_VIEW_PROJECT_DATA, + ], + UserProfile: [ + CAN_ADD_PROJECT_TO_PROFILE, + CAN_ADD_XFORM_TO_PROFILE, + CAN_VIEW_PROFILE, ], - UserProfile: [CAN_ADD_PROJECT_TO_PROFILE, CAN_ADD_XFORM_TO_PROFILE, - CAN_VIEW_PROFILE], XForm: [ - CAN_ADD_SUBMISSIONS, CAN_ADD_XFORM, CAN_CHANGE_XFORM, - CAN_DELETE_SUBMISSION, CAN_DELETE_XFORM, CAN_EXPORT_XFORM, - CAN_VIEW_XFORM, CAN_VIEW_XFORM_ALL, CAN_VIEW_XFORM_DATA + CAN_ADD_SUBMISSIONS, + CAN_ADD_XFORM, + CAN_CHANGE_XFORM, + CAN_DELETE_SUBMISSION, + CAN_DELETE_XFORM, + CAN_EXPORT_XFORM, + CAN_VIEW_XFORM, + CAN_VIEW_XFORM_ALL, + CAN_VIEW_XFORM_DATA, ], } @@ -290,52 +348,80 @@ class MemberRole(Role): """ This is a role for a member of an organization. """ - name = 'member' + + name = "member" class OwnerRole(Role): """ This is a role for an owner of a dataset, organization, or project. """ - name = 'owner' + + name = "owner" class_to_permissions = { DataDictionary: [ - CAN_ADD_DATADICTIONARY, CAN_CHANGE_DATADICTIONARY, - CAN_DELETE_DATADICTIONARY + CAN_ADD_DATADICTIONARY, + CAN_CHANGE_DATADICTIONARY, + CAN_DELETE_DATADICTIONARY, ], MergedXForm: [CAN_VIEW_MERGED_XFORM], OrganizationProfile: [ - CAN_ADD_ORGANIZATION_PROJECT, CAN_ADD_ORGANIZATION_XFORM, - CAN_ADD_ORGANIZATION_PROFILE, CAN_ADD_ORGANIZATION_PROJECT, - CAN_ADD_ORGANIZATION_XFORM, CAN_CHANGE_ORGANIZATION_PROFILE, - CAN_DELETE_ORGANIZATION_PROFILE, CAN_VIEW_ORGANIZATION_PROFILE, - IS_ORGANIZATION_OWNER + CAN_ADD_ORGANIZATION_PROJECT, + CAN_ADD_ORGANIZATION_XFORM, + CAN_ADD_ORGANIZATION_PROFILE, + CAN_ADD_ORGANIZATION_PROJECT, + CAN_ADD_ORGANIZATION_XFORM, + CAN_CHANGE_ORGANIZATION_PROFILE, + CAN_DELETE_ORGANIZATION_PROFILE, + CAN_VIEW_ORGANIZATION_PROFILE, + IS_ORGANIZATION_OWNER, ], Project: [ - CAN_ADD_PROJECT, CAN_ADD_PROJECT_XFORM, - CAN_ADD_SUBMISSIONS_PROJECT, CAN_CHANGE_PROJECT, - CAN_DELETE_PROJECT, CAN_EXPORT_PROJECT, - CAN_TRANSFER_PROJECT_OWNERSHIP, CAN_VIEW_PROJECT, - CAN_VIEW_PROJECT_ALL, CAN_VIEW_PROJECT_DATA + CAN_ADD_PROJECT, + CAN_ADD_PROJECT_XFORM, + CAN_ADD_SUBMISSIONS_PROJECT, + CAN_CHANGE_PROJECT, + CAN_DELETE_PROJECT, + CAN_EXPORT_PROJECT, + CAN_TRANSFER_PROJECT_OWNERSHIP, + CAN_VIEW_PROJECT, + CAN_VIEW_PROJECT_ALL, + CAN_VIEW_PROJECT_DATA, ], UserProfile: [ - CAN_ADD_PROJECT_TO_PROFILE, CAN_ADD_XFORM_TO_PROFILE, - CAN_ADD_USERPROFILE, CAN_CHANGE_USERPROFILE, - CAN_DELETE_USERPROFILE, CAN_VIEW_PROFILE + CAN_ADD_PROJECT_TO_PROFILE, + CAN_ADD_XFORM_TO_PROFILE, + CAN_ADD_USERPROFILE, + CAN_CHANGE_USERPROFILE, + CAN_DELETE_USERPROFILE, + CAN_VIEW_PROFILE, ], XForm: [ - CAN_ADD_SUBMISSIONS, CAN_ADD_XFORM, CAN_CHANGE_XFORM, - CAN_DELETE_SUBMISSION, CAN_DELETE_XFORM, CAN_EXPORT_XFORM, - CAN_VIEW_XFORM, CAN_VIEW_XFORM_ALL, CAN_VIEW_XFORM_DATA, - CAN_MOVE_TO_FOLDER, CAN_TRANSFER_OWNERSHIP + CAN_ADD_SUBMISSIONS, + CAN_ADD_XFORM, + CAN_CHANGE_XFORM, + CAN_DELETE_SUBMISSION, + CAN_DELETE_XFORM, + CAN_EXPORT_XFORM, + CAN_VIEW_XFORM, + CAN_VIEW_XFORM_ALL, + CAN_VIEW_XFORM_DATA, + CAN_MOVE_TO_FOLDER, + CAN_TRANSFER_OWNERSHIP, ], } ROLES_ORDERED = [ - ReadOnlyRoleNoDownload, ReadOnlyRole, DataEntryOnlyRole, - DataEntryMinorRole, DataEntryRole, EditorMinorRole, EditorRole, - ManagerRole, OwnerRole + ReadOnlyRoleNoDownload, + ReadOnlyRole, + DataEntryOnlyRole, + DataEntryMinorRole, + DataEntryRole, + EditorMinorRole, + EditorRole, + ManagerRole, + OwnerRole, ] ROLES = {role.name: role for role in ROLES_ORDERED} @@ -347,8 +433,7 @@ def is_organization(obj): UserProfiles do. Check for that first since it avoids a database hit. """ try: - return (hasattr(obj, 'userprofile_ptr') - or obj.organizationprofile is not None) + return hasattr(obj, "userprofile_ptr") or obj.organizationprofile is not None except OrganizationProfile.DoesNotExist: return False @@ -369,7 +454,7 @@ def get_role_in_org(user, organization): """ perms = get_perms(user, organization) - if 'is_org_owner' in perms: + if "is_org_owner" in perms: return OwnerRole.name return get_role(perms, organization) or MemberRole.name @@ -382,8 +467,11 @@ def get_user_perms(obj): model = XFormUserObjectPermission if isinstance(obj, XForm) else None model = ProjectUserObjectPermission if isinstance(obj, Project) else model - return queryset_iterator( - model.objects.filter(content_object_id=obj.pk)) if model else None + return ( + queryset_iterator(model.objects.filter(content_object_id=obj.pk)) + if model + else None + ) def get_group_perms(obj): @@ -393,8 +481,11 @@ def get_group_perms(obj): model = XFormGroupObjectPermission if isinstance(obj, XForm) else None model = ProjectGroupObjectPermission if isinstance(obj, Project) else model - return queryset_iterator( - model.objects.filter(content_object_id=obj.pk)) if model else None + return ( + queryset_iterator(model.objects.filter(content_object_id=obj.pk)) + if model + else None + ) def _get_group_users_with_perms(obj, attach_perms=False, user_perms=None): @@ -404,7 +495,8 @@ def _get_group_users_with_perms(obj, attach_perms=False, user_perms=None): group_obj_perms = get_group_perms(obj) if group_obj_perms is None: return get_users_with_perms( - obj, attach_perms=attach_perms, with_group_users=True) + obj, attach_perms=attach_perms, with_group_users=True + ) group_users = {} if attach_perms: if user_perms: @@ -420,9 +512,8 @@ def _get_group_users_with_perms(obj, attach_perms=False, user_perms=None): group_users[user] = set([perm.permission.codename]) else: group_users = set() if not user_perms else set(user_perms) - for perm in group_obj_perms.distinct('group'): - group_users.union( - set([user for user in perm.group.user_set.all()])) + for perm in group_obj_perms.distinct("group"): + group_users.union(set(user for user in perm.group.user_set.all())) group_users = list(group_obj_perms) return group_users @@ -435,7 +526,8 @@ def _get_users_with_perms(obj, attach_perms=False, with_group_users=None): user_obj_perms = get_user_perms(obj) if user_obj_perms is None: return get_users_with_perms( - obj, attach_perms=attach_perms, with_group_users=with_group_users) + obj, attach_perms=attach_perms, with_group_users=with_group_users + ) user_perms = {} if attach_perms: for perm in user_obj_perms: @@ -445,7 +537,7 @@ def _get_users_with_perms(obj, attach_perms=False, with_group_users=None): user_perms[perm.user] = set([perm.permission.codename]) else: user_perms = [ - perm.user for perm in user_obj_perms.only('user').distinct('user') + perm.user for perm in user_obj_perms.only("user").distinct("user") ] if with_group_users: @@ -454,9 +546,10 @@ def _get_users_with_perms(obj, attach_perms=False, with_group_users=None): return user_perms -def get_object_users_with_permissions(obj, # pylint: disable=invalid-name - username=False, - with_group_users=False): +# pylint: disable=invalid-name +def get_object_users_with_permissions( + obj, username=False, with_group_users=False # pylint: disable=invalid-name +): """ Returns users, roles and permissions for an object. @@ -467,17 +560,21 @@ def get_object_users_with_permissions(obj, # pylint: disable=invalid-name if obj: users_with_perms = _get_users_with_perms( - obj, attach_perms=True, with_group_users=with_group_users).items() - - result = [{ - 'user': user.username if username else user, - 'first_name': user.first_name, - 'last_name': user.last_name, - 'role': get_role(permissions, obj), - 'is_org': is_organization(user.profile), - 'gravatar': user.profile.gravatar, - 'metadata': user.profile.metadata - } for user, permissions in users_with_perms] + obj, attach_perms=True, with_group_users=with_group_users + ).items() + + result = [ + { + "user": user.username if username else user, + "first_name": user.first_name, + "last_name": user.last_name, + "role": get_role(permissions, obj), + "is_org": is_organization(user.profile), + "gravatar": user.profile.gravatar, + "metadata": user.profile.metadata, + } + for user, permissions in users_with_perms + ] return result @@ -494,45 +591,49 @@ def get_team_project_default_permissions(team, project): def _check_meta_perms_enabled(xform): """ - Check for meta-perms settings in the xform metadata model. - :param xform: - :return: bool + Check for meta-perms settings in the xform metadata model. + :param xform: + :return: bool """ return xform.metadata_set.filter(data_type=XFORM_META_PERMS).count() > 0 -def exclude_items_from_queryset_using_xform_meta_perms( - xform, user, queryset): +# pylint: disable=invalid-name +def exclude_items_from_queryset_using_xform_meta_perms(xform, user, queryset): """ Exclude instances from the queryset if meta-perms have been enabled """ - if user.has_perm(CAN_VIEW_XFORM_ALL, xform) or xform.shared_data \ - or not _check_meta_perms_enabled(xform): + if ( + user.has_perm(CAN_VIEW_XFORM_ALL, xform) + or xform.shared_data + or not _check_meta_perms_enabled(xform) + ): return queryset - elif user.has_perm(CAN_VIEW_XFORM_DATA, xform): + if user.has_perm(CAN_VIEW_XFORM_DATA, xform): if queryset.model is Attachment: - return queryset.exclude( - ~Q(instance__user=user), instance__xform=xform) - else: - return queryset.exclude( - ~Q(user=user), xform=xform) + return queryset.exclude(~Q(instance__user=user), instance__xform=xform) + return queryset.exclude(~Q(user=user), xform=xform) + return queryset.none() def filter_queryset_xform_meta_perms(xform, user, instance_queryset): """ - Check for the specific perms if meta-perms have been enabled - CAN_VIEW_XFORM_ALL ==> User should be able to view all the data - CAN_VIEW_XFORM_DATA ===> User should be able to view his/her submitted - data. Otherwise should raise forbidden error. - :param xform: - :param user: - :param instance_queryset: - :return: data - """ - if user.has_perm(CAN_VIEW_XFORM_ALL, xform) or xform.shared_data \ - or not _check_meta_perms_enabled(xform): + Check for the specific perms if meta-perms have been enabled + CAN_VIEW_XFORM_ALL ==> User should be able to view all the data + CAN_VIEW_XFORM_DATA ===> User should be able to view his/her submitted + data. Otherwise should raise forbidden error. + :param xform: + :param user: + :param instance_queryset: + :return: data + """ + if ( + user.has_perm(CAN_VIEW_XFORM_ALL, xform) + or xform.shared_data + or not _check_meta_perms_enabled(xform) + ): return instance_queryset - elif user.has_perm(CAN_VIEW_XFORM_DATA, xform): + if user.has_perm(CAN_VIEW_XFORM_DATA, xform): return instance_queryset.filter(user=user) return instance_queryset.none() @@ -540,33 +641,35 @@ def filter_queryset_xform_meta_perms(xform, user, instance_queryset): def filter_queryset_xform_meta_perms_sql(xform, user, query): """ - Check for the specific perms if meta-perms have been enabled - CAN_VIEW_XFORM_ALL ==> User should be able to view all the data - CAN_VIEW_XFORM_DATA ===> User should be able to view his/her submitted - data. Otherwise should raise forbidden error. - :param xform: - :param user: - :param instance_queryset: - :return: data - """ - if user.has_perm(CAN_VIEW_XFORM_ALL, xform) or xform.shared_data\ - or not _check_meta_perms_enabled(xform): + Check for the specific perms if meta-perms have been enabled + CAN_VIEW_XFORM_ALL ==> User should be able to view all the data + CAN_VIEW_XFORM_DATA ===> User should be able to view his/her submitted + data. Otherwise should raise forbidden error. + :param xform: + :param user: + :param instance_queryset: + :return: data + """ + if ( + user.has_perm(CAN_VIEW_XFORM_ALL, xform) + or xform.shared_data + or not _check_meta_perms_enabled(xform) + ): return query - elif user.has_perm(CAN_VIEW_XFORM_DATA, xform): + if user.has_perm(CAN_VIEW_XFORM_DATA, xform): try: if query and isinstance(query, six.string_types): query = json.loads(query) if isinstance(query, list): query = query[0] else: - query = dict() + query = {} query.update({"_submitted_by": user.username}) return query except (ValueError, AttributeError): - query_list = list() + query_list = [] query_list.append({"_submitted_by": user.username}) query_list.append(query) return query_list - else: - raise NoRecordsPermission() + raise NoRecordsPermission() diff --git a/onadata/libs/profiling/sql.py b/onadata/libs/profiling/sql.py index 1c26037464..c9b24fe5fc 100644 --- a/onadata/libs/profiling/sql.py +++ b/onadata/libs/profiling/sql.py @@ -6,14 +6,14 @@ from django.db import connection -sql_log = logging.getLogger('sql_logger') # pylint: disable=C0103 -totals_log = logging.getLogger('sql_totals_logger') # pylint: disable=C0103 +SQL_LOG = logging.getLogger("sql_logger") +TOTALS_LOG = logging.getLogger("sql_totals_logger") # modified from # http://johnparsons.net/index.php/2013/08/15/easy-sql-query-counting-in-django -class SqlTimingMiddleware(object): # pylint: disable=R0903 +class SqlTimingMiddleware: """ Logs the time taken by each sql query over requests. Logs the total time taken to run sql queries and the number of sql queries @@ -24,17 +24,16 @@ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): - path_info = '%s %s' % (request.method, request.path_info) + path_info = f"{request.method} {request.path_info}" response = self.get_response(request) sqltime = 0 # Variable to store execution time for query in connection.queries: # Add the time that the query took to the total sqltime += float(query["time"]) - sql_log.debug(path_info, extra=query) + SQL_LOG.debug(path_info, extra=query) - totals_log.debug( - path_info, - extra={'time': sqltime, - 'num_queries': len(connection.queries)}) + TOTALS_LOG.debug( + path_info, extra={"time": sqltime, "num_queries": len(connection.queries)} + ) return response diff --git a/onadata/libs/renderers/renderers.py b/onadata/libs/renderers/renderers.py index 8a0604f617..b068037977 100644 --- a/onadata/libs/renderers/renderers.py +++ b/onadata/libs/renderers/renderers.py @@ -8,14 +8,13 @@ from io import BytesIO, StringIO from typing import Tuple -import pytz -import six - from django.utils import timezone from django.utils.dateparse import parse_datetime -from django.utils.encoding import smart_str, force_str +from django.utils.encoding import force_str, smart_str from django.utils.xmlutils import SimplerXMLGenerator -from six import iteritems + +import pytz +import six from rest_framework import negotiation from rest_framework.renderers import ( BaseRenderer, @@ -25,6 +24,7 @@ ) from rest_framework.utils.encoders import JSONEncoder from rest_framework_xml.renderers import XMLRenderer +from six import iteritems from onadata.libs.utils.osm import get_combined_osm @@ -122,7 +122,6 @@ class XLSRenderer(BaseRenderer): format = "xls" charset = None - # pylint: disable=no-self-use,unused-argument def render(self, data, accepted_media_type=None, renderer_context=None): """ Encode ``data`` string to 'utf-8'. @@ -226,8 +225,7 @@ class MediaFileContentNegotiation(negotiation.DefaultContentNegotiation): matching format. """ - # pylint: disable=redefined-builtin,no-self-use - def filter_renderers(self, renderers, format): + def filter_renderers(self, renderers, format): # pylint: disable=redefined-builtin """ If there is a '.json' style format suffix, filter the renderers so that we only negotiation against those that accept that format. @@ -360,6 +358,7 @@ def _get_current_buffer_data(self): return None def stream_data(self, data, serializer): + """Returns a streaming response.""" if data is None: yield "" @@ -372,7 +371,7 @@ def stream_data(self, data, serializer): yield self._get_current_buffer_data() - data = data.__iter__() + data = iter(data) try: out = next(data) diff --git a/onadata/libs/serializers/attachment_serializer.py b/onadata/libs/serializers/attachment_serializer.py index 7c676b2fc4..174fc4ca6f 100644 --- a/onadata/libs/serializers/attachment_serializer.py +++ b/onadata/libs/serializers/attachment_serializer.py @@ -3,13 +3,11 @@ Attachments serializer. """ -from six import itervalues - from rest_framework import serializers +from six import itervalues from onadata.apps.logger.models.attachment import Attachment -from onadata.apps.logger.models.instance import get_attachment_url -from onadata.apps.logger.models.instance import Instance +from onadata.apps.logger.models.instance import Instance, get_attachment_url from onadata.libs.utils.decorators import check_obj @@ -110,7 +108,6 @@ def get_medium_download_url(self, obj): return request.build_absolute_uri(path) if request else path return "" - # pylint: disable=no-self-use def get_field_xpath(self, obj): """ Return question xpath diff --git a/onadata/libs/serializers/clone_xform_serializer.py b/onadata/libs/serializers/clone_xform_serializer.py index 0392a41342..0ab1b589f1 100644 --- a/onadata/libs/serializers/clone_xform_serializer.py +++ b/onadata/libs/serializers/clone_xform_serializer.py @@ -19,7 +19,6 @@ class CloneXFormSerializer(serializers.Serializer): username = serializers.CharField(max_length=255) project = ProjectField(required=False) - # pylint: disable=no-self-use def create(self, validated_data): """Uses the CloneXForm class to clone/copy an XForm. @@ -29,7 +28,6 @@ def create(self, validated_data): return instance - # pylint: disable=no-self-use def update(self, instance, validated_data): instance.xform = validated_data.get("xform", instance.xform) instance.username = validated_data.get("username", instance.username) @@ -38,7 +36,6 @@ def update(self, instance, validated_data): return instance - # pylint: disable=no-self-use def validate_username(self, value): """Check that the username exists""" # pylint: disable=invalid-name diff --git a/onadata/libs/serializers/data_serializer.py b/onadata/libs/serializers/data_serializer.py index 1638ff493c..e2b3a47fb8 100644 --- a/onadata/libs/serializers/data_serializer.py +++ b/onadata/libs/serializers/data_serializer.py @@ -1,28 +1,43 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Submission data serializers module. """ -import xmltodict from io import BytesIO from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ + +import xmltodict from rest_framework import exceptions, serializers from rest_framework.reverse import reverse -from onadata.apps.logger.models.instance import Instance, InstanceHistory from onadata.apps.logger.models import Project, XForm +from onadata.apps.logger.models.instance import Instance, InstanceHistory from onadata.libs.serializers.fields.json_field import JsonField +from onadata.libs.utils.analytics import TrackObjectEvent from onadata.libs.utils.common_tags import ( - METADATA_FIELDS, NOTES, TAGS, DATE_MODIFIED, VERSION, GEOLOCATION, - XFORM_ID, ATTACHMENTS, XFORM_ID_STRING, UUID) -from onadata.libs.utils.logger_tools import remove_metadata_fields -from onadata.libs.utils.dict_tools import (dict_lists2strings, dict_paths2dict, - query_list_to_dict, - floip_response_headers_dict) -from onadata.libs.utils.logger_tools import dict2xform, safe_create_instance -from onadata.libs.utils.analytics import track_object_event - + ATTACHMENTS, + DATE_MODIFIED, + GEOLOCATION, + METADATA_FIELDS, + NOTES, + TAGS, + UUID, + VERSION, + XFORM_ID, + XFORM_ID_STRING, +) +from onadata.libs.utils.dict_tools import ( + dict_lists2strings, + dict_paths2dict, + floip_response_headers_dict, + query_list_to_dict, +) +from onadata.libs.utils.logger_tools import ( + dict2xform, + remove_metadata_fields, + safe_create_instance, +) NUM_FLOIP_COLUMNS = 6 @@ -31,11 +46,11 @@ def get_request_and_username(context): """ Returns request object and username """ - request = context['request'] - view = context['view'] - username = view.kwargs.get('username') - form_pk = view.kwargs.get('xform_pk') - project_pk = view.kwargs.get('project_pk') + request = context["request"] + view = context["view"] + username = view.kwargs.get("username") + form_pk = view.kwargs.get("xform_pk") + project_pk = view.kwargs.get("project_pk") if not username: # get the username from the XForm object if form_id is @@ -45,22 +60,19 @@ def get_request_and_username(context): elif project_pk: username = Project.objects.get(pk=project_pk).user.username else: - username = (request.user and request.user.username) + username = request.user and request.user.username return (request, username) -def create_submission( - request, username, data_dict, xform_id, gen_uuid: bool = False): +def create_submission(request, username, data_dict, xform_id, gen_uuid: bool = False): """ Returns validated data object instances """ - xml_string = dict2xform( - data_dict, xform_id, username=username, gen_uuid=gen_uuid) - xml_file = BytesIO(xml_string.encode('utf-8')) + xml_string = dict2xform(data_dict, xform_id, username=username, gen_uuid=gen_uuid) + xml_file = BytesIO(xml_string.encode("utf-8")) - error, instance = safe_create_instance(username, xml_file, [], None, - request) + error, instance = safe_create_instance(username, xml_file, [], None, request) if error: raise serializers.ValidationError(error.message) @@ -72,19 +84,25 @@ class DataSerializer(serializers.HyperlinkedModelSerializer): DataSerializer class - used for the list view to show `id`, `id_string`, `title` and `description`. """ - url = serializers.HyperlinkedIdentityField( - view_name='data-list', lookup_field='pk') + + url = serializers.HyperlinkedIdentityField(view_name="data-list", lookup_field="pk") class Meta: model = XForm - fields = ('id', 'id_string', 'title', 'description', 'url') + fields = ("id", "id_string", "title", "description", "url") -class JsonDataSerializer(serializers.Serializer): # pylint: disable=W0223 +class JsonDataSerializer(serializers.Serializer): """ JSON DataSerializer class - for json field data representation. """ + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + pass + def to_representation(self, instance): return instance @@ -93,17 +111,17 @@ class InstanceHistorySerializer(serializers.ModelSerializer): """ InstanceHistorySerializer class - for the json field data representation. """ + json = JsonField() class Meta: model = InstanceHistory - fields = ('json', ) + fields = ("json",) def to_representation(self, instance): - ret = super(InstanceHistorySerializer, - self).to_representation(instance) + ret = super().to_representation(instance) - return ret['json'] if 'json' in ret else ret + return ret["json"] if "json" in ret else ret class DataInstanceXMLSerializer(serializers.ModelSerializer): @@ -114,51 +132,56 @@ class DataInstanceXMLSerializer(serializers.ModelSerializer): class Meta: model = Instance - fields = ('xml', ) + fields = ("xml",) def _convert_metadata_field_to_attribute(self, field: str) -> str: """ Converts a metadata field such as `_review_status` into a camel cased attribute `reviewStatus` """ - split_field = field.split('_')[1:] - return split_field[0] + ''.join( - word.title() for word in split_field[1:]) + split_field = field.split("_")[1:] + return split_field[0] + "".join(word.title() for word in split_field[1:]) def to_representation(self, instance): - ret = super( - DataInstanceXMLSerializer, self).to_representation(instance) - if 'xml' in ret: - ret = xmltodict.parse(ret['xml'], cdata_key="") + ret = super().to_representation(instance) + if "xml" in ret: + ret = xmltodict.parse(ret["xml"], cdata_key="") # Add Instance attributes to representation instance_attributes = { - '@formVersion': instance.version, - '@lastModified': instance.date_modified.isoformat(), - '@dateCreated': instance.date_created.isoformat(), - '@objectID': str(instance.id) + "@formVersion": instance.version, + "@lastModified": instance.date_modified.isoformat(), + "@dateCreated": instance.date_created.isoformat(), + "@objectID": str(instance.id), } ret.update(instance_attributes) excluded_metadata = [ - NOTES, TAGS, GEOLOCATION, XFORM_ID, DATE_MODIFIED, VERSION, - ATTACHMENTS, XFORM_ID_STRING, UUID] + NOTES, + TAGS, + GEOLOCATION, + XFORM_ID, + DATE_MODIFIED, + VERSION, + ATTACHMENTS, + XFORM_ID_STRING, + UUID, + ] additional_attributes = [ (self._convert_metadata_field_to_attribute(metadata), metadata) for metadata in METADATA_FIELDS - if metadata not in excluded_metadata] + if metadata not in excluded_metadata + ] for attrib, meta_field in additional_attributes: meta_value = instance.json.get(meta_field, "") if not isinstance(meta_value, str): meta_value = str(meta_value) - ret.update({ - f"@{attrib}": meta_value - }) + ret.update({f"@{attrib}": meta_value}) # Include linked resources linked_resources = { - 'linked-resources': { - 'attachments': instance.json.get(ATTACHMENTS), - 'notes': instance.json.get(NOTES) + "linked-resources": { + "attachments": instance.json.get(ATTACHMENTS), + "notes": instance.json.get(NOTES), } } ret.update(linked_resources) @@ -170,16 +193,17 @@ class DataInstanceSerializer(serializers.ModelSerializer): DataInstanceSerializer class - for json field data representation on the Instance (submission) model. """ + json = JsonField() class Meta: model = Instance - fields = ('json', ) + fields = ("json",) def to_representation(self, instance): - ret = super(DataInstanceSerializer, self).to_representation(instance) - if 'json' in ret: - ret = ret['json'] + ret = super().to_representation(instance) + if "json" in ret: + ret = ret["json"] return ret @@ -187,23 +211,24 @@ class TableauDataSerializer(serializers.ModelSerializer): """ TableauDataSerializer class - cleans out instance fields. """ + json = JsonField() class Meta: model = Instance - fields = ('json', ) + fields = ("json",) def to_representation(self, instance): - ret = super(TableauDataSerializer, self).to_representation(instance) - if 'json' in ret: - ret = ret['json'] + ret = super().to_representation(instance) + if "json" in ret: + ret = ret["json"] # Remove metadata fields from the instance remove_metadata_fields(ret) return ret -class SubmissionSuccessMixin(object): # pylint: disable=R0903 +class SubmissionSuccessMixin: """ SubmissionSuccessMixin - prepares submission success data/message. """ @@ -213,65 +238,77 @@ def to_representation(self, instance): Returns a dict with a successful submission message. """ if instance is None: - return super(SubmissionSuccessMixin, self)\ - .to_representation(instance) + return super().to_representation(instance) return { - 'message': _("Successful submission."), - 'formid': instance.xform.id_string, - 'encrypted': instance.xform.encrypted, - 'instanceID': u'uuid:%s' % instance.uuid, - 'submissionDate': instance.date_created.isoformat(), - 'markedAsCompleteDate': instance.date_modified.isoformat() + "message": _("Successful submission."), + "formid": instance.xform.id_string, + "encrypted": instance.xform.encrypted, + "instanceID": f"uuid:{instance.uuid}", + "submissionDate": instance.date_created.isoformat(), + "markedAsCompleteDate": instance.date_modified.isoformat(), } -class BaseRapidProSubmissionSerializer(SubmissionSuccessMixin, - serializers.Serializer): +class BaseRapidProSubmissionSerializer(SubmissionSuccessMixin, serializers.Serializer): """ Base Rapidpro SubmissionSerializer - Implements the basic functionalities of a Rapidpro webhook serializer """ + + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + pass + def validate(self, attrs): """ Validate that the XForm ID is passed in view kwargs """ - view = self.context['view'] + view = self.context["view"] - if 'xform_pk' in view.kwargs: - xform_pk = view.kwargs.get('xform_pk') + if "xform_pk" in view.kwargs: + xform_pk = view.kwargs.get("xform_pk") xform = get_object_or_404(XForm, pk=xform_pk) - attrs.update({'id_string': xform.id_string}) + attrs.update({"id_string": xform.id_string}) else: - raise serializers.ValidationError({ - 'xform_pk': - _(u'Incorrect url format. Use format ' - u'https://api.ona.io/username/formid/submission')}) + raise serializers.ValidationError( + { + "xform_pk": _( + "Incorrect url format. Use format " + "https://api.ona.io/username/formid/submission" + ) + } + ) - return super(BaseRapidProSubmissionSerializer, self).validate(attrs) + return super().validate(attrs) -# pylint: disable=W0223 class SubmissionSerializer(SubmissionSuccessMixin, serializers.Serializer): """ XML SubmissionSerializer - handles creating a submission from XML. """ + def update(self, instance, validated_data): + pass + def validate(self, attrs): request, __ = get_request_and_username(self.context) - if not request.FILES or 'xml_submission_file' not in request.FILES: + if not request.FILES or "xml_submission_file" not in request.FILES: raise serializers.ValidationError(_("No XML submission file.")) - return super(SubmissionSerializer, self).validate(attrs) + return super().validate(attrs) - @track_object_event( - user_field='xform__user', + @TrackObjectEvent( + user_field="xform__user", properties={ - 'submitted_by': 'user', - 'xform_id': 'xform__pk', - 'project_id': 'xform__project__pk', - 'organization': 'xform__user__profile__organization'}, - additional_context={'from': 'XML Submissions'} + "submitted_by": "user", + "xform_id": "xform__pk", + "project_id": "xform__project__pk", + "organization": "xform__user__profile__organization", + }, + additional_context={"from": "XML Submissions"}, ) def create(self, validated_data): """ @@ -279,12 +316,13 @@ def create(self, validated_data): """ request, username = get_request_and_username(self.context) - xml_file_list = request.FILES.pop('xml_submission_file', []) + xml_file_list = request.FILES.pop("xml_submission_file", []) xml_file = xml_file_list[0] if xml_file_list else None media_files = request.FILES.values() - error, instance = safe_create_instance(username, xml_file, media_files, - None, request) + error, instance = safe_create_instance( + username, xml_file, media_files, None, request + ) if error: exc = exceptions.APIException(detail=error) exc.response = error @@ -300,24 +338,30 @@ class OSMSerializer(serializers.Serializer): OSM Serializer - represents OSM data. """ + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + pass + def to_representation(self, instance): """ Return a list of osm file objects from attachments. """ return instance - # pylint: disable=W0201 @property def data(self): """ Returns the serialized data on the serializer. """ - if not hasattr(self, '_data'): - if self.instance is not None and \ - not getattr(self, '_errors', None): + # pylint: disable=attribute-defined-outside-init + if not hasattr(self, "_data"): + if self.instance is not None and not getattr(self, "_errors", None): self._data = self.to_representation(self.instance) - elif hasattr(self, '_validated_data') and \ - not getattr(self, '_errors', None): + elif hasattr(self, "_validated_data") and not getattr( + self, "_errors", None + ): self._data = self.to_representation(self.validated_data) else: self._data = self.get_initial() @@ -330,28 +374,27 @@ class OSMSiteMapSerializer(serializers.Serializer): OSM SiteMap Serializer. """ + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + pass + def to_representation(self, instance): """ Return a list of osm file objects from attachments. """ if instance is None: - return super(OSMSiteMapSerializer, self)\ - .to_representation(instance) + return super().to_representation(instance) - id_string = instance.get('instance__xform__id_string') - title = instance.get('instance__xform__title') - user = instance.get('instance__xform__user__username') + id_string = instance.get("instance__xform__id_string") + title = instance.get("instance__xform__title") + user = instance.get("instance__xform__user__username") - kwargs = {'pk': instance.get('instance__xform')} - url = reverse( - 'osm-list', kwargs=kwargs, request=self.context.get('request')) + kwargs = {"pk": instance.get("instance__xform")} + url = reverse("osm-list", kwargs=kwargs, request=self.context.get("request")) - return { - 'url': url, - 'title': title, - 'id_string': id_string, - 'user': user - } + return {"url": url, "title": title, "id_string": id_string, "user": user} class JSONSubmissionSerializer(SubmissionSuccessMixin, serializers.Serializer): @@ -359,52 +402,58 @@ class JSONSubmissionSerializer(SubmissionSuccessMixin, serializers.Serializer): JSON SubmissionSerializer - handles JSON submission data. """ + def update(self, instance, validated_data): + pass + def validate(self, attrs): """ Custom submission validator in request data. """ - request = self.context['request'] + request = self.context["request"] - if 'submission' not in request.data: - raise serializers.ValidationError({ - 'submission': - _(u"No submission key provided.") - }) + if "submission" not in request.data: + raise serializers.ValidationError( + {"submission": _("No submission key provided.")} + ) - submission = request.data.get('submission') + submission = request.data.get("submission") if not submission: - raise serializers.ValidationError({ - 'submission': - _(u"Received empty submission. No instance was created") - }) + raise serializers.ValidationError( + {"submission": _("Received empty submission. No instance was created")} + ) - return super(JSONSubmissionSerializer, self).validate(attrs) + return super().validate(attrs) - @track_object_event( - user_field='xform__user', + @TrackObjectEvent( + user_field="xform__user", properties={ - 'submitted_by': 'user', - 'xform_id': 'xform__pk', - 'project_id': 'xform__project__pk', - 'organization': 'xform__user__profile__organization'}, - additional_context={'from': 'JSON Submission'} + "submitted_by": "user", + "xform_id": "xform__pk", + "project_id": "xform__project__pk", + "organization": "xform__user__profile__organization", + }, + additional_context={"from": "JSON Submission"}, ) def create(self, validated_data): """ Returns object instances based on the validated data """ request, username = get_request_and_username(self.context) - submission = request.data.get('submission') + submission = request.data.get("submission") # convert lists in submission dict to joined strings try: submission_joined = dict_paths2dict(dict_lists2strings(submission)) - except AttributeError: + except AttributeError as exc: raise serializers.ValidationError( - _(u'Incorrect format, see format details here,' - u'https://api.ona.io/static/docs/submissions.html.')) + _( + "Incorrect format, see format details here," + "https://api.ona.io/static/docs/submissions.html." + ) + ) from exc - instance = create_submission(request, username, submission_joined, - request.data.get('id')) + instance = create_submission( + request, username, submission_joined, request.data.get("id") + ) return instance @@ -413,24 +462,28 @@ class RapidProSubmissionSerializer(BaseRapidProSubmissionSerializer): """ Rapidpro SubmissionSerializer - handles Rapidpro webhook post. """ - @track_object_event( - user_field='xform__user', + + def update(self, instance, validated_data): + pass + + @TrackObjectEvent( + user_field="xform__user", properties={ - 'submitted_by': 'user', - 'xform_id': 'xform__pk', - 'project_id': 'xform__project__pk', - }, - additional_context={'from': 'RapidPro'} + "submitted_by": "user", + "xform_id": "xform__pk", + "project_id": "xform__project__pk", + }, + additional_context={"from": "RapidPro"}, ) def create(self, validated_data): """ Returns object instances based on the validated data. """ request, username = get_request_and_username(self.context) - rapidpro_dict = query_list_to_dict(request.data.get('values')) - instance = create_submission(request, username, rapidpro_dict, - validated_data['id_string'], - gen_uuid=True) + rapidpro_dict = query_list_to_dict(request.data.get("values")) + instance = create_submission( + request, username, rapidpro_dict, validated_data["id_string"], gen_uuid=True + ) return instance @@ -439,26 +492,33 @@ class RapidProJSONSubmissionSerializer(BaseRapidProSubmissionSerializer): """ Rapidpro SubmissionSerializer - handles RapidPro JSON webhook posts """ - @track_object_event( - user_field='xform__user', + + def update(self, instance, validated_data): + pass + + @TrackObjectEvent( + user_field="xform__user", properties={ - 'submitted_by': 'user', - 'xform_id': 'xform__pk', - 'project_id': 'xform__project__pk', - }, - additional_context={'from': 'RapidPro(JSON)'} + "submitted_by": "user", + "xform_id": "xform__pk", + "project_id": "xform__project__pk", + }, + additional_context={"from": "RapidPro(JSON)"}, ) def create(self, validated_data): """ Returns object instances based on validated data. """ request, username = get_request_and_username(self.context) - post_data = request.data.get('results') - instance_data_dict = { - k: post_data[k].get('value') for k in post_data.keys()} + post_data = request.data.get("results") + instance_data_dict = {k: post_data[k].get("value") for k in post_data.keys()} instance = create_submission( - request, username, instance_data_dict, - validated_data['id_string'], gen_uuid=True) + request, + username, + instance_data_dict, + validated_data["id_string"], + gen_uuid=True, + ) return instance @@ -466,39 +526,48 @@ class FLOIPListSerializer(serializers.ListSerializer): """ Custom ListSerializer for a FLOIP submission. """ - @track_object_event( - user_field='xform__user', + + def update(self, instance, validated_data): + pass + + @TrackObjectEvent( + user_field="xform__user", properties={ - 'submitted_by': 'user', - 'xform_id': 'xform__pk', - 'project_id': 'xform__project__pk', - }, - additional_context={'from': 'FLOIP'} + "submitted_by": "user", + "xform_id": "xform__pk", + "project_id": "xform__project__pk", + }, + additional_context={"from": "FLOIP"}, ) def create(self, validated_data): """ Returns object instances based on the validated data. """ request, username = get_request_and_username(self.context) - xform_pk = self.context['view'].kwargs['xform_pk'] + xform_pk = self.context["view"].kwargs["xform_pk"] xform = get_object_or_404(XForm, pk=xform_pk) xform_headers = xform.get_keys() flow_dict = floip_response_headers_dict(request.data, xform_headers) - instance = create_submission(request, username, flow_dict, - xform) + instance = create_submission(request, username, flow_dict, xform) return [instance] -class FLOIPSubmissionSerializer(SubmissionSuccessMixin, - serializers.Serializer): +class FLOIPSubmissionSerializer(SubmissionSuccessMixin, serializers.Serializer): """ FLOIP SubmmissionSerializer - Handles a row of FLOIP specification format. """ + + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + pass + def run_validators(self, value): # Only run default run_validators if we have validators attached to the # serializer. if self.validators: - return super(FLOIPSubmissionSerializer, self).run_validators(value) + return super().run_validators(value) return [] @@ -506,25 +575,29 @@ def validate(self, attrs): """ Custom list data validator. """ - data = self.context['request'].data + data = self.context["request"].data error_msg = None if not isinstance(data, list): - error_msg = u'Invalid format. Expecting a list.' + error_msg = "Invalid format. Expecting a list." elif data: for row_i, row in enumerate(data): if len(row) != NUM_FLOIP_COLUMNS: - error_msg = _(u"Wrong number of values (%(values)d) in row" - " %(row)d, expecting %(expected)d values" - % {'row': row_i, - 'values': (len(row)), - 'expected': NUM_FLOIP_COLUMNS}) + error_msg = _( + "Wrong number of values (%(values)d) in row" + " %(row)d, expecting %(expected)d values" + % { + "row": row_i, + "values": (len(row)), + "expected": NUM_FLOIP_COLUMNS, + } + ) break if error_msg: raise serializers.ValidationError(_(error_msg)) - return super(FLOIPSubmissionSerializer, self).validate(attrs) + return super().validate(attrs) def to_internal_value(self, data): """ @@ -539,4 +612,5 @@ class Meta: """ Call the list serializer class to create an instance. """ + list_serializer_class = FLOIPListSerializer diff --git a/onadata/libs/serializers/dataview_serializer.py b/onadata/libs/serializers/dataview_serializer.py index 0a0e323a39..90f7e94262 100644 --- a/onadata/libs/serializers/dataview_serializer.py +++ b/onadata/libs/serializers/dataview_serializer.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +The DataViewSerializer - manage DataView objects. +""" import datetime from django.utils.translation import gettext as _ @@ -11,58 +15,62 @@ from onadata.apps.logger.models.data_view import SUPPORTED_FILTERS from onadata.apps.logger.models.xform import XForm from onadata.apps.logger.models.project import Project -from onadata.libs.utils.cache_tools import ( - DATAVIEW_COUNT, - DATAVIEW_LAST_SUBMISSION_TIME) +from onadata.libs.utils.cache_tools import DATAVIEW_COUNT, DATAVIEW_LAST_SUBMISSION_TIME from onadata.libs.utils.common_tags import MONGO_STRFTIME, DATE_FORMAT from onadata.libs.utils.model_tools import get_columns_with_hxl from onadata.libs.utils.api_export_tools import include_hxl_row -LAST_SUBMISSION_TIME = '_submission_time' +LAST_SUBMISSION_TIME = "_submission_time" def validate_date(value): + """Returns True if the ``value`` is a date string.""" try: datetime.datetime.strptime(value, DATE_FORMAT) - return True except ValueError: return False + return True def validate_datetime(value): + """Returns True if the ``value`` is a datetime string.""" try: datetime.datetime.strptime(value, MONGO_STRFTIME) - return True except ValueError: return False + return True def match_columns(data, instance=None): - matches_parent = data.get('matches_parent') - xform = data.get('xform', instance.xform if instance else None) - columns = data.get('columns', instance.columns if instance else None) + """Checks if the fields in two forms are a match.""" + matches_parent = data.get("matches_parent") + xform = data.get("xform", instance.xform if instance else None) + columns = data.get("columns", instance.columns if instance else None) if xform and columns: fields = xform.get_field_name_xpaths_only() matched = [col for col in columns if col in fields] matches_parent = len(matched) == len(columns) == len(fields) - data['matches_parent'] = matches_parent + data["matches_parent"] = matches_parent return data class DataViewMinimalSerializer(serializers.HyperlinkedModelSerializer): - dataviewid = serializers.ReadOnlyField(source='id') + """ + The DataViewMinimalSerializer - manage DataView objects. + """ + + dataviewid = serializers.ReadOnlyField(source="id") name = serializers.CharField(max_length=255) - url = serializers.HyperlinkedIdentityField(view_name='dataviews-detail', - lookup_field='pk') + url = serializers.HyperlinkedIdentityField( + view_name="dataviews-detail", lookup_field="pk" + ) xform = serializers.HyperlinkedRelatedField( - view_name='xform-detail', lookup_field='pk', - queryset=XForm.objects.all() + view_name="xform-detail", lookup_field="pk", queryset=XForm.objects.all() ) project = serializers.HyperlinkedRelatedField( - view_name='project-detail', lookup_field='pk', - queryset=Project.objects.all() + view_name="project-detail", lookup_field="pk", queryset=Project.objects.all() ) columns = JsonField() query = JsonField(required=False) @@ -70,23 +78,36 @@ class DataViewMinimalSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = DataView - fields = ('dataviewid', 'name', 'url', 'xform', 'project', 'columns', - 'query', 'matches_parent', 'date_created', - 'instances_with_geopoints', 'date_modified') + fields = ( + "dataviewid", + "name", + "url", + "xform", + "project", + "columns", + "query", + "matches_parent", + "date_created", + "instances_with_geopoints", + "date_modified", + ) class DataViewSerializer(serializers.HyperlinkedModelSerializer): - dataviewid = serializers.ReadOnlyField(source='id') + """ + The DataViewSerializer - manage DataView objects. + """ + + dataviewid = serializers.ReadOnlyField(source="id") name = serializers.CharField(max_length=255) - url = serializers.HyperlinkedIdentityField(view_name='dataviews-detail', - lookup_field='pk') + url = serializers.HyperlinkedIdentityField( + view_name="dataviews-detail", lookup_field="pk" + ) xform = serializers.HyperlinkedRelatedField( - view_name='xform-detail', lookup_field='pk', - queryset=XForm.objects.all() + view_name="xform-detail", lookup_field="pk", queryset=XForm.objects.all() ) project = serializers.HyperlinkedRelatedField( - view_name='project-detail', lookup_field='pk', - queryset=Project.objects.all() + view_name="project-detail", lookup_field="pk", queryset=Project.objects.all() ) columns = JsonField() query = JsonField(required=False) @@ -98,85 +119,93 @@ class DataViewSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = DataView - fields = ('dataviewid', 'name', 'xform', 'project', 'columns', 'query', - 'matches_parent', 'count', 'instances_with_geopoints', - 'last_submission_time', 'has_hxl_support', 'url', - 'date_created', 'deleted_at', 'deleted_by') + fields = ( + "dataviewid", + "name", + "xform", + "project", + "columns", + "query", + "matches_parent", + "count", + "instances_with_geopoints", + "last_submission_time", + "has_hxl_support", + "url", + "date_created", + "deleted_at", + "deleted_by", + ) validators = [ serializers.UniqueTogetherValidator( - queryset=DataView.objects.all(), - fields=('name', 'xform') + queryset=DataView.objects.all(), fields=("name", "xform") ) ] def create(self, validated_data): validated_data = match_columns(validated_data) - return super(DataViewSerializer, self).create(validated_data) + return super().create(validated_data) def update(self, instance, validated_data): validated_data = match_columns(validated_data, instance) - return super(DataViewSerializer, self).update(instance, validated_data) + return super().update(instance, validated_data) def validate_query(self, value): + """Checks if the query filters in ``value`` are known.""" if value: - for q in value: - if 'column' not in q: - raise serializers.ValidationError(_( - u"`column` not set in query" - )) + for query in value: + if "column" not in query: + raise serializers.ValidationError(_("`column` not set in query")) - if 'filter' not in q: - raise serializers.ValidationError(_( - u"`filter` not set in query" - )) + if "filter" not in query: + raise serializers.ValidationError(_("`filter` not set in query")) - if 'value' not in q: - raise serializers.ValidationError(_( - u"`value` not set in query" - )) + if "value" not in query: + raise serializers.ValidationError(_("`value` not set in query")) - comp = q.get('filter') + comp = query.get("filter") if comp not in SUPPORTED_FILTERS: - raise serializers.ValidationError(_( - u"Filter not supported" - )) + raise serializers.ValidationError(_("Filter not supported")) return value def validate_columns(self, value): + """Checks the ``value`` is a list.""" if not isinstance(value, list): - raise serializers.ValidationError(_( - u"`columns` should be a list of columns" - )) + raise serializers.ValidationError( + _("`columns` should be a list of columns") + ) return value def validate(self, attrs): - if 'xform' in attrs and attrs.get('xform'): - xform = attrs.get('xform') - know_dates = [e.name for e in - xform.get_survey_elements_of_type('date')] - know_dates.append('_submission_time') - for q in attrs.get('query', []): - column = q.get('column') - value = q.get('value') - - if column in know_dates and not \ - (validate_datetime(value) or validate_date(value)): - raise serializers.ValidationError(_( - u"Date value in {} should be yyyy-mm-ddThh:m:s or " - u"yyyy-mm-dd" - .format(column) - )) - - return super(DataViewSerializer, self).validate(attrs) + if "xform" in attrs and attrs.get("xform"): + xform = attrs.get("xform") + know_dates = [e.name for e in xform.get_survey_elements_of_type("date")] + know_dates.append("_submission_time") + for query in attrs.get("query", []): + column = query.get("column") + value = query.get("value") + + if column in know_dates and not ( + validate_datetime(value) or validate_date(value) + ): + raise serializers.ValidationError( + _( + f"Date value in {column} should be yyyy-mm-ddThh:m:s or " + "yyyy-mm-dd" + ) + ) + + return super().validate(attrs) def get_count(self, obj): + """Returns the submission count for the data view,""" if obj: - count_dict = cache.get('{}{}'.format(DATAVIEW_COUNT, obj.xform.pk)) + count_dict = cache.get(f"{DATAVIEW_COUNT}{obj.xform.pk}") if count_dict: if obj.pk in count_dict: @@ -185,50 +214,52 @@ def get_count(self, obj): count_dict = {} count_rows = DataView.query_data(obj, count=True) - if 'error' in count_rows: - raise ParseError(count_rows.get('error')) + if "error" in count_rows: + raise ParseError(count_rows.get("error")) count_row = count_rows[0] - if 'count' in count_row: - count = count_row.get('count') + if "count" in count_row: + count = count_row.get("count") count_dict.setdefault(obj.pk, count) - cache.set('{}{}'.format(DATAVIEW_COUNT, obj.xform.pk), - count_dict) + cache.set(f"{DATAVIEW_COUNT}{obj.xform.pk}", count_dict) return count return None def get_last_submission_time(self, obj): + """Returns the last submission timestamp.""" if obj: - last_submission_time = cache.get('{}{}'.format( - DATAVIEW_LAST_SUBMISSION_TIME, obj.xform.pk)) + last_submission_time = cache.get( + f"{DATAVIEW_LAST_SUBMISSION_TIME}{obj.xform.pk}" + ) if last_submission_time: return last_submission_time last_submission_rows = DataView.query_data( - obj, last_submission_time=True) # data is returned as list + obj, last_submission_time=True + ) # data is returned as list - if 'error' in last_submission_rows: - raise ParseError(last_submission_rows.get('error')) + if "error" in last_submission_rows: + raise ParseError(last_submission_rows.get("error")) if len(last_submission_rows): last_submission_row = last_submission_rows[0] if LAST_SUBMISSION_TIME in last_submission_row: - last_submission_time = last_submission_row.get( - LAST_SUBMISSION_TIME) + last_submission_time = last_submission_row.get(LAST_SUBMISSION_TIME) cache.set( - '{}{}'.format( - DATAVIEW_LAST_SUBMISSION_TIME, obj.xform.pk), - last_submission_time) + f"{DATAVIEW_LAST_SUBMISSION_TIME}{obj.xform.pk}", + last_submission_time, + ) return last_submission_time return None def get_instances_with_geopoints(self, obj): + """Returns True if a DataView has submissions with geopoints.""" if obj: check_geo = obj.has_geo_columnn_n_data() @@ -241,8 +272,7 @@ def get_instances_with_geopoints(self, obj): return False def get_has_hxl_support(self, obj): - columns_with_hxl = get_columns_with_hxl( - obj.xform.survey.get('children') - ) + """Returns true if a DataView has columns with HXL tags.""" + columns_with_hxl = get_columns_with_hxl(obj.xform.survey.get("children")) return include_hxl_row(obj.columns, list(columns_with_hxl)) diff --git a/onadata/libs/serializers/export_serializer.py b/onadata/libs/serializers/export_serializer.py index c10eab1029..237bc9b0e9 100644 --- a/onadata/libs/serializers/export_serializer.py +++ b/onadata/libs/serializers/export_serializer.py @@ -1,40 +1,59 @@ +# -*- coding: utf-8 -*- +""" +The ExportSerializer class - create, list exports. +""" from rest_framework import serializers +from rest_framework.reverse import reverse from onadata.apps.viewer.models.export import Export from onadata.libs.utils.async_status import status_msg -from rest_framework.reverse import reverse - class ExportSerializer(serializers.HyperlinkedModelSerializer): - date_created = serializers.ReadOnlyField(source='created_on') + """ + The ExportSerializer class - create, list exports. + """ + + date_created = serializers.ReadOnlyField(source="created_on") job_status = serializers.SerializerMethodField() type = serializers.SerializerMethodField() export_url = serializers.SerializerMethodField() class Meta: model = Export - fields = ('id', 'job_status', 'type', 'task_id', 'xform', - 'date_created', 'filename', 'options', 'export_url', - 'error_message') + fields = ( + "id", + "job_status", + "type", + "task_id", + "xform", + "date_created", + "filename", + "options", + "export_url", + "error_message", + ) def get_job_status(self, obj): + """Returns export async status text.""" return status_msg.get(obj.internal_status) def get_type(self, obj): + """Returns export type - CSV,XLS,...""" return obj.export_type def get_export_url(self, obj): + """Returns the export download URL.""" if obj.export_url: return obj.export_url - request = self.context.get('request') + request = self.context.get("request") if request: - export_url = reverse( - 'export-detail', - kwargs={'pk': obj.pk}, + return reverse( + "export-detail", + kwargs={"pk": obj.pk}, request=request, - format=obj.export_type.replace('_', '') + format=obj.export_type.replace("_", ""), ) - return export_url + return None diff --git a/onadata/libs/serializers/fields/hyperlinked_multi_identity_field.py b/onadata/libs/serializers/fields/hyperlinked_multi_identity_field.py index 89d81b7dff..5d977c5515 100644 --- a/onadata/libs/serializers/fields/hyperlinked_multi_identity_field.py +++ b/onadata/libs/serializers/fields/hyperlinked_multi_identity_field.py @@ -1,22 +1,29 @@ +# -*- coding: utf-8 -*- +""" +The HyperlinkedIdentityField class - multi-lookup identity fields. +""" from rest_framework import serializers from rest_framework.reverse import reverse class HyperlinkedMultiIdentityField(serializers.HyperlinkedIdentityField): - lookup_fields = (('pk', 'pk'), ) + """ + The HyperlinkedIdentityField class - multi-lookup identity fields. + """ + + lookup_fields = (("pk", "pk"),) def __init__(self, *args, **kwargs): - lookup_fields = kwargs.pop('lookup_fields', None) + lookup_fields = kwargs.pop("lookup_fields", None) self.lookup_fields = lookup_fields or self.lookup_fields - super(HyperlinkedMultiIdentityField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) + # pylint: disable=redefined-builtin def get_url(self, obj, view_name, request, format): kwargs = {} for slug, field in self.lookup_fields: lookup_field = getattr(obj, field) kwargs[slug] = lookup_field - return reverse( - view_name, kwargs=kwargs, request=request, format=format - ) + return reverse(view_name, kwargs=kwargs, request=request, format=format) diff --git a/onadata/libs/serializers/fields/hyperlinked_multi_related_field.py b/onadata/libs/serializers/fields/hyperlinked_multi_related_field.py index 4133bb5c03..eeb909429f 100644 --- a/onadata/libs/serializers/fields/hyperlinked_multi_related_field.py +++ b/onadata/libs/serializers/fields/hyperlinked_multi_related_field.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +The HyperlinkedRelatedField class - multi-lookup fields. +""" from rest_framework import serializers from rest_framework.reverse import reverse @@ -12,25 +16,30 @@ def get_obj_property_value(obj, field): <<< list """ - _attr_list = field.split('.') + _attr_list = field.split(".") if len(_attr_list) > 1: tmp_obj = getattr(obj, _attr_list[0]) - return get_obj_property_value(tmp_obj, '.'.join(_attr_list[1:])) + return get_obj_property_value(tmp_obj, ".".join(_attr_list[1:])) return getattr(obj, field) class HyperlinkedMultiRelatedField(serializers.HyperlinkedRelatedField): - lookup_fields = (('pk', 'pk'), ) + """ + The HyperlinkedRelatedField class - multi-lookup fields. + """ + + lookup_fields = (("pk", "pk"),) def __init__(self, *args, **kwargs): - lookup_fields = kwargs.pop('lookup_fields', None) + lookup_fields = kwargs.pop("lookup_fields", None) self.lookup_fields = lookup_fields or self.lookup_fields - super(HyperlinkedMultiRelatedField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) + # pylint: disable=redefined-builtin def get_url(self, obj, view_name, request, format): kwargs = {} @@ -38,5 +47,4 @@ def get_url(self, obj, view_name, request, format): lookup_field = get_obj_property_value(obj, field) kwargs[slug] = lookup_field - return reverse( - view_name, kwargs=kwargs, request=request, format=format) + return reverse(view_name, kwargs=kwargs, request=request, format=format) diff --git a/onadata/libs/serializers/fields/json_field.py b/onadata/libs/serializers/fields/json_field.py index d247f84f13..75ea8fa9e6 100644 --- a/onadata/libs/serializers/fields/json_field.py +++ b/onadata/libs/serializers/fields/json_field.py @@ -12,7 +12,6 @@ class JsonField(serializers.Field): Deserialize a string instance containing a JSON document to a Python object. """ - # pylint: disable=no-self-use def to_representation(self, value): """ Deserialize ``value`` a `str` instance containing a @@ -22,7 +21,6 @@ def to_representation(self, value): return json.loads(value) return value - # pylint: disable=no-self-use def to_internal_value(self, data): """ Deserialize ``value`` a `str` instance containing a @@ -32,8 +30,8 @@ def to_internal_value(self, data): try: return json.loads(data) except ValueError as e: - # invalid json - raise serializers.ValidationError(str(e)) + # invalid JSON + raise serializers.ValidationError(str(e)) from e return data @classmethod diff --git a/onadata/libs/serializers/fields/organization_field.py b/onadata/libs/serializers/fields/organization_field.py index 50cd9932f6..37b751b06b 100644 --- a/onadata/libs/serializers/fields/organization_field.py +++ b/onadata/libs/serializers/fields/organization_field.py @@ -12,12 +12,10 @@ class OrganizationField(serializers.Field): """organization serializer field""" - # pylint: disable=no-self-use def to_representation(self, value): """Return the organization pk.""" return value.pk - # pylint: disable=no-self-use def to_internal_value(self, data): """Validate the organization exists.""" if data is not None: @@ -25,10 +23,7 @@ def to_internal_value(self, data): organization = OrganizationProfile.objects.get(pk=data) except OrganizationProfile.DoesNotExist as e: raise serializers.ValidationError( - _( - "Organization with id '%(value)s' does not exist." - % {"value": data} - ) + _(f"Organization with id '{data}' does not exist.") ) from e except ValueError as e: raise serializers.ValidationError(str(e)) from e diff --git a/onadata/libs/serializers/fields/project_field.py b/onadata/libs/serializers/fields/project_field.py index e6673ae752..4b2f973032 100644 --- a/onadata/libs/serializers/fields/project_field.py +++ b/onadata/libs/serializers/fields/project_field.py @@ -12,12 +12,10 @@ class ProjectField(serializers.Field): """Project field for use with a Project object/instance.""" - # pylint: disable=no-self-use def to_representation(self, value): """Returns the project pk.""" return value.pk - # pylint: disable=no-self-use def to_internal_value(self, data): """Validates that a project exists.""" if data is not None: diff --git a/onadata/libs/serializers/fields/team_field.py b/onadata/libs/serializers/fields/team_field.py index 18f6b110df..f73dfd1346 100644 --- a/onadata/libs/serializers/fields/team_field.py +++ b/onadata/libs/serializers/fields/team_field.py @@ -1,10 +1,19 @@ +# -*- coding: utf-8 -*- +""" +The TeamField class. +""" from rest_framework import serializers + from onadata.apps.api.models.team import Team class TeamField(serializers.Field): - def to_representation(self, obj): - return obj.pk + """ + The TeamField class. + """ + + def to_representation(self, value): + return value.pk def to_internal_value(self, data): return Team.objects.get(pk=data) diff --git a/onadata/libs/serializers/fields/utils.py b/onadata/libs/serializers/fields/utils.py index 0cf67b4736..efbce124e3 100644 --- a/onadata/libs/serializers/fields/utils.py +++ b/onadata/libs/serializers/fields/utils.py @@ -6,7 +6,7 @@ def get_object_id_by_content_type(instance, model_class): """Return instance.object_id from a cached model's content type""" - key = "{}-content_type_id".format(model_class.__name__) + key = f"{model_class.__name__}-content_type_id" content_type_id = cache.get(key) if not content_type_id: try: diff --git a/onadata/libs/serializers/fields/xform_field.py b/onadata/libs/serializers/fields/xform_field.py index 18b18a3f06..b75b9aae27 100644 --- a/onadata/libs/serializers/fields/xform_field.py +++ b/onadata/libs/serializers/fields/xform_field.py @@ -1,14 +1,23 @@ +# -*- coding: utf-8 -*- +""" +The XFormField class +""" from rest_framework import serializers + from onadata.apps.logger.models import XForm class XFormField(serializers.Field): - def to_representation(self, obj): - return obj.pk + """ + The XFormField class + """ + + def to_representation(self, value): + return value.pk def to_internal_value(self, data): try: int(data) - except ValueError: - raise serializers.ValidationError(u"Invalid form id") + except ValueError as exc: + raise serializers.ValidationError("Invalid form id") from exc return XForm.objects.get(pk=data) diff --git a/onadata/libs/serializers/fields/xform_related_field.py b/onadata/libs/serializers/fields/xform_related_field.py index c330e1c45f..cb946422b4 100644 --- a/onadata/libs/serializers/fields/xform_related_field.py +++ b/onadata/libs/serializers/fields/xform_related_field.py @@ -21,10 +21,10 @@ def get_attribute(self, instance): def to_internal_value(self, data): try: return XForm.objects.get(id=data) - except ValueError: - raise serializers.ValidationError("xform id should be an integer") - except XForm.DoesNotExist: - raise serializers.ValidationError("XForm does not exist") + except ValueError as exc: + raise serializers.ValidationError("xform id should be an integer") from exc + except XForm.DoesNotExist as exc: + raise serializers.ValidationError("XForm does not exist") from exc def to_representation(self, value): """Serialize xform object""" diff --git a/onadata/libs/serializers/floip_serializer.py b/onadata/libs/serializers/floip_serializer.py index 6dc9994b59..37497a0431 100644 --- a/onadata/libs/serializers/floip_serializer.py +++ b/onadata/libs/serializers/floip_serializer.py @@ -5,12 +5,12 @@ """ import json import os -from uuid import UUID from copy import deepcopy from io import BytesIO +from uuid import UUID from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.core.files.uploadedfile import InMemoryUploadedFile from django.db.models import Q @@ -31,6 +31,8 @@ QUESTION_INDEX = getattr(settings, "FLOW_RESULTS_QUESTION_INDEX", 4) ANSWER_INDEX = getattr(settings, "FLOW_RESULTS_ANSWER_INDEX", 5) +User = get_user_model() + def _get_user(username): users = User.objects.filter(username__iexact=username) @@ -45,7 +47,7 @@ def _get_owner(request): owner_obj = _get_user(owner) if owner_obj is None: - raise ValidationError(_("User with username %s does not exist." % owner)) + raise ValidationError(_(f"User with username {owner} does not exist.")) return owner_obj return owner @@ -70,7 +72,7 @@ def parse_responses( current_key = row[session_id_index] if "meta" not in submission: submission["meta"] = { - "instanceID": "uuid:%s" % current_key, + "instanceID": f"uuid:{current_key}", "sessionID": current_key, "contactID": row[contact_id_index], } @@ -88,8 +90,11 @@ class ReadOnlyUUIDField(serializers.ReadOnlyField): Custom ReadOnlyField for UUID """ - def to_representation(self, obj): # pylint: disable=no-self-use - return str(UUID(obj)) + def to_internal_value(self, data): + pass + + def to_representation(self, value): + return str(UUID(value)) # pylint: disable=too-many-ancestors @@ -101,12 +106,12 @@ class FloipListSerializer(serializers.HyperlinkedModelSerializer): url = serializers.HyperlinkedIdentityField( view_name="flow-results-detail", lookup_field="uuid" ) - id = ReadOnlyUUIDField(source="uuid") # pylint: disable=C0103 + id = ReadOnlyUUIDField(source="uuid") # pylint: disable=invalid-name name = serializers.ReadOnlyField(source="id_string") created = serializers.ReadOnlyField(source="date_created") modified = serializers.ReadOnlyField(source="date_modified") - class JSONAPIMeta: # pylint: disable=old-style-class,no-init,R0903 + class JSONAPIMeta: """ JSON API metaclass. """ @@ -133,7 +138,7 @@ class FloipSerializer(serializers.HyperlinkedModelSerializer): flow_results_specification_version = serializers.SerializerMethodField() resources = serializers.SerializerMethodField() - class JSONAPIMeta: # pylint: disable=old-style-class,no-init,R0903 + class JSONAPIMeta: """ JSON API metaclass. """ @@ -154,20 +159,20 @@ class Meta: "resources", ) - def get_profile(self, value): # pylint: disable=no-self-use,W0613 + def get_profile(self, value): # pylint: disable=unused-argument """ Returns the data-package profile. """ return "data-package" - # pylint: disable=no-self-use,unused-argument + # pylint: disable=unused-argument def get_flow_results_specification_version(self, value): """ Returns the flow results specification version. """ return "1.0.0-rc1" - def get_resources(self, value): # pylint: disable=no-self-use,W0613 + def get_resources(self, value): # pylint: disable=unused-argument """ Returns empty dict, a dummy holder for the eventually generated data package resources. @@ -234,7 +239,7 @@ def to_representation(self, instance): return data -class FlowResultsResponse(object): # pylint: disable=too-few-public-methods +class FlowResultsResponse: # pylint: disable=too-few-public-methods """ FLowResultsResponse class to hold a list of submission ids. """ @@ -259,7 +264,7 @@ class FlowResultsResponseSerializer(serializers.Serializer): responses = serializers.ListField() duplicates = serializers.IntegerField(read_only=True) - class JSONAPIMeta: # pylint: disable=old-style-class,no-init,R0903 + class JSONAPIMeta: """ JSON API metaclass. """ diff --git a/onadata/libs/serializers/geojson_serializer.py b/onadata/libs/serializers/geojson_serializer.py index e3e9ce9dcb..aba4133939 100644 --- a/onadata/libs/serializers/geojson_serializer.py +++ b/onadata/libs/serializers/geojson_serializer.py @@ -1,6 +1,10 @@ -import geojson +# -*- coding: utf-8 -*- +""" +The GeoJsonSerializer class - uses the GeoJSON structure for submission data. +""" import json +import geojson from rest_framework_gis import serializers from onadata.apps.logger.models.instance import Instance @@ -82,6 +86,10 @@ def geometry_from_string(points, simple_style): class GeometryField(serializers.GeometryField): + """ + The GeometryField class - representation for single GeometryField. + """ + def to_representation(self, value): if isinstance(value, dict) or value is None: return None @@ -90,6 +98,9 @@ def to_representation(self, value): class GeoJsonSerializer(serializers.GeoFeatureModelSerializer): + """ + The GeoJsonSerializer class - uses the GeoJSON structure for submission data. + """ geom = GeometryField() @@ -100,17 +111,17 @@ class Meta: id_field = False fields = ("id", "xform") - def to_representation(self, obj): - ret = super().to_representation(obj) + def to_representation(self, instance): + ret = super().to_representation(instance) request = self.context.get("request") - if obj and ret and "properties" in ret and request is not None: + if instance and ret and "properties" in ret and request is not None: fields = request.query_params.get("fields") if fields: for field in fields.split(","): - ret["properties"][field] = obj.json.get(field) + ret["properties"][field] = instance.json.get(field) - if obj and ret and request: + if instance and ret and request: fields = request.query_params.get("fields") geo_field = request.query_params.get("geo_field") simple_style = request.query_params.get("simple_style") @@ -118,11 +129,11 @@ def to_representation(self, obj): if geo_field: if "properties" in ret: if title: - ret["properties"]["title"] = obj.json.get(title) + ret["properties"]["title"] = instance.json.get(title) if fields: for field in fields.split(","): - ret["properties"][field] = obj.json.get(field) - points = obj.json.get(geo_field) + ret["properties"][field] = instance.json.get(field) + points = instance.json.get(geo_field) geometry = ( geometry_from_string(points, simple_style) if points @@ -139,30 +150,30 @@ class GeoJsonListSerializer(GeoJsonSerializer): Creates a FeatureCollections """ - def to_representation(self, obj): + def to_representation(self, instance): - if obj is None: - return super().to_representation(obj) + if instance is None: + return super().to_representation(instance) geo_field = None fields = None - if "fields" in obj and obj.get("fields"): - fields = obj.get("fields").split(",") + if "fields" in instance and instance.get("fields"): + fields = instance.get("fields").split(",") - if "instances" in obj and obj.get("instances"): - insts = obj.get("instances") + if "instances" in instance and instance.get("instances"): + insts = instance.get("instances") - if "geo_field" in obj and obj.get("geo_field"): - geo_field = obj.get("geo_field") + if "geo_field" in instance and instance.get("geo_field"): + geo_field = instance.get("geo_field") # Get the instances from the form - instances = [inst for inst in insts[0].instances.all()] + instances = insts[0].instances.all() if not geo_field: return geojson.FeatureCollection( [ super().to_representation( - {"instance": ret, "fields": obj.get("fields")} + {"instance": ret, "fields": instance.get("fields")} ) for ret in instances ] diff --git a/onadata/libs/serializers/merged_xform_serializer.py b/onadata/libs/serializers/merged_xform_serializer.py index 556d3af8c7..be57e792cb 100644 --- a/onadata/libs/serializers/merged_xform_serializer.py +++ b/onadata/libs/serializers/merged_xform_serializer.py @@ -9,10 +9,10 @@ from django.db import transaction from django.utils.translation import gettext as _ -from rest_framework import serializers from pyxform.builder import create_survey_element_from_dict from pyxform.errors import PyXFormError +from rest_framework import serializers from onadata.apps.logger.models import MergedXForm, XForm from onadata.apps.logger.models.xform import XFORM_TITLE_LENGTH @@ -50,6 +50,11 @@ def _get_elements(elements, intersect, parent_prefix=None): return new_elements +def _list_with_name(name, children): + """Returns all children where the name is the same value as ``name``""" + return list(filter(lambda x: x["name"] == name, children)) + + def get_merged_xform_survey(xforms): """ Genertates a new pyxform survey object from the intersection of fields of @@ -67,7 +72,7 @@ def get_merged_xform_survey(xforms): merged_xform_dict["children"] = [] intersect = set(xform_sets[0]).intersection(*xform_sets[1:]) - intersect = set([__ for (__, ___) in intersect]) + intersect = set(__ for (__, ___) in intersect) merged_xform_dict["children"] = _get_elements(children, intersect) @@ -87,10 +92,7 @@ def get_merged_xform_survey(xforms): if "children" in child and child["type"] in SELECTS: children = [] for xform_dict in xform_dicts: - element_name = child["name"] - element_list = list( - filter(lambda x: x["name"] == element_name, xform_dict["children"]) - ) + element_list = _list_with_name(child["name"], xform_dict["children"]) if element_list and element_list[0]: children += element_list[0]["children"] # remove duplicates @@ -194,7 +196,6 @@ class Meta: ) write_only_fields = ("uuid",) - # pylint: disable=no-self-use def get_num_of_submissions(self, obj): """Return number of submissions either from the aggregate 'number_of_submissions' in the queryset or from the xform field @@ -214,6 +215,7 @@ def get_last_submission_time(self, obj): ] if values: return sorted(values, reverse=True)[0] + return None def create(self, validated_data): request = self.context["request"] @@ -230,20 +232,20 @@ def create(self, validated_data): validated_data["xml"] = survey.to_xml() except PyXFormError as error: raise serializers.ValidationError( - {"xforms": _("Problem Merging the Form: {}".format(error))} + {"xforms": _(f"Problem Merging the Form: {error}")} ) validated_data["user"] = validated_data["project"].user validated_data["created_by"] = request.user validated_data["is_merged_dataset"] = True validated_data["num_of_submissions"] = sum( - [__.num_of_submissions for __ in validated_data.get("xforms")] + __.num_of_submissions for __ in validated_data.get("xforms") ) validated_data["instances_with_geopoints"] = any( - [__.instances_with_geopoints for __ in validated_data.get("xforms")] + __.instances_with_geopoints for __ in validated_data.get("xforms") ) with transaction.atomic(): - instance = super(MergedXFormSerializer, self).create(validated_data) + instance = super().create(validated_data) if instance.xforms.all().count() == 0 and xforms: for xform in xforms: diff --git a/onadata/libs/serializers/metadata_serializer.py b/onadata/libs/serializers/metadata_serializer.py index cf7ca3c67b..2d3e05033a 100644 --- a/onadata/libs/serializers/metadata_serializer.py +++ b/onadata/libs/serializers/metadata_serializer.py @@ -13,24 +13,24 @@ from django.db.utils import IntegrityError from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ -from six.moves.urllib.parse import urlparse + from rest_framework import serializers from rest_framework.reverse import reverse - +from six.moves.urllib.parse import urlparse from onadata.apps.api.tools import update_role_by_meta_xform_perms -from onadata.libs.utils.api_export_tools import get_metadata_format from onadata.apps.logger.models import DataView, Instance, Project, XForm from onadata.apps.main.models import MetaData from onadata.libs.permissions import ROLES, ManagerRole -from onadata.libs.serializers.fields.json_field import JsonField from onadata.libs.serializers.fields.instance_related_field import InstanceRelatedField +from onadata.libs.serializers.fields.json_field import JsonField from onadata.libs.serializers.fields.project_related_field import ProjectRelatedField from onadata.libs.serializers.fields.xform_related_field import XFormRelatedField +from onadata.libs.utils.api_export_tools import get_metadata_format from onadata.libs.utils.common_tags import ( - XFORM_META_PERMS, - SUBMISSION_REVIEW, IMPORTED_VIA_CSV_BY, + SUBMISSION_REVIEW, + XFORM_META_PERMS, ) UNIQUE_TOGETHER_ERROR = "Object already exists" @@ -174,7 +174,7 @@ def validate(self, attrs): raise serializers.ValidationError( { "missing_field": _( - "`xform` or `project` or `instance`" "field is required." + "`xform` or `project` or `instance` field is required." ) } ) @@ -182,7 +182,7 @@ def validate(self, attrs): if data_file: allowed_types = settings.SUPPORTED_MEDIA_UPLOAD_TYPES # add geojson mimetype - mimetypes.add_type('application/geo+json', '.geojson') + mimetypes.add_type("application/geo+json", ".geojson") data_content_type = ( data_file.content_type if data_file.content_type in allowed_types @@ -219,11 +219,7 @@ def validate(self, attrs): ) if not has_perm: raise serializers.ValidationError( - { - "data_value": _( - "User has no permission to " "the dataview." - ) - } + {"data_value": _("User has no permission to the dataview.")} ) from e else: raise serializers.ValidationError( @@ -251,7 +247,6 @@ def validate(self, attrs): return attrs - # pylint: disable=no-self-use def get_content_object(self, validated_data): """ Returns the validated 'xform' or 'project' or 'instance' ids being @@ -270,7 +265,7 @@ def create(self, validated_data): data_type = validated_data.get("data_type") data_file = validated_data.get("data_file") data_file_type = validated_data.get("data_file_type") - extra_data = validated_data.get('extra_data') + extra_data = validated_data.get("extra_data") content_object = self.get_content_object(validated_data) data_value = data_file.name if data_file else validated_data.get("data_value") diff --git a/onadata/libs/serializers/monthly_submissions_serializer.py b/onadata/libs/serializers/monthly_submissions_serializer.py index 2fa779f690..4f102556a0 100644 --- a/onadata/libs/serializers/monthly_submissions_serializer.py +++ b/onadata/libs/serializers/monthly_submissions_serializer.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Monthly submissions serializer """ @@ -5,22 +6,36 @@ class MonthlySubmissionsListSerializer(serializers.ListSerializer): + """ + Monthly submissions serializer + """ + + def update(self, instance, validated_data): + pass def to_representation(self, data): - result = super(MonthlySubmissionsListSerializer, - self).to_representation(data) + result = super().to_representation(data) result_dictionary = {} for i in result: - label = 'public' if i['xform__shared'] else 'private' - result_dictionary[label] = i['num_instances'] + label = "public" if i["xform__shared"] else "private" + result_dictionary[label] = i["num_instances"] return [result_dictionary] class MonthlySubmissionsSerializer(serializers.Serializer): + """ + Monthly submissions serializer + """ class Meta: list_serializer_class = MonthlySubmissionsListSerializer + def update(self, instance, validated_data): + pass + + def create(self, validated_data): + pass + def to_representation(self, instance): """ Returns the total number of private/public submissions for a user diff --git a/onadata/libs/serializers/note_serializer.py b/onadata/libs/serializers/note_serializer.py index 8eb77c9003..b21c88c877 100644 --- a/onadata/libs/serializers/note_serializer.py +++ b/onadata/libs/serializers/note_serializer.py @@ -14,15 +14,25 @@ class NoteSerializer(serializers.ModelSerializer): """ NoteSerializer class """ + owner = serializers.SerializerMethodField() class Meta: """ Meta Options for NoteSerializer """ + model = Note - fields = ('id', 'note', 'instance', 'instance_field', 'created_by', - 'date_created', 'date_modified', 'owner') + fields = ( + "id", + "note", + "instance", + "instance_field", + "created_by", + "date_created", + "date_modified", + "owner", + ) def get_owner(self, obj): """ @@ -35,14 +45,14 @@ def get_owner(self, obj): return None def create(self, validated_data): - request = self.context.get('request') - obj = super(NoteSerializer, self).create(validated_data) + request = self.context.get("request") + obj = super().create(validated_data) if request: - assign_perm('add_note', request.user, obj) - assign_perm('change_note', request.user, obj) - assign_perm('delete_note', request.user, obj) - assign_perm('view_note', request.user, obj) + assign_perm("add_note", request.user, obj) + assign_perm("change_note", request.user, obj) + assign_perm("delete_note", request.user, obj) + assign_perm("view_note", request.user, obj) # should update instance json obj.instance.save() @@ -50,19 +60,21 @@ def create(self, validated_data): return obj def validate(self, attrs): - instance = attrs.get('instance') - request = self.context.get('request') + instance = attrs.get("instance") + request = self.context.get("request") if request and request.user.is_anonymous: raise exceptions.ParseError( - _(u"You are not authorized to add/change notes on this form.")) + _("You are not authorized to add/change notes on this form.") + ) - attrs['created_by'] = request.user + attrs["created_by"] = request.user - field = attrs.get('instance_field') + field = attrs.get("instance_field") if field and instance.xform.get_label(field) is None: raise exceptions.ValidationError( - "instance_field must be a field on the form") + "instance_field must be a field on the form" + ) return attrs diff --git a/onadata/libs/serializers/open_data_serializer.py b/onadata/libs/serializers/open_data_serializer.py index 479278991a..818acb41b9 100644 --- a/onadata/libs/serializers/open_data_serializer.py +++ b/onadata/libs/serializers/open_data_serializer.py @@ -1,45 +1,55 @@ +# -*- coding: utf-8 -*- +""" +The OpenDataSerializer class - create/list OpenData model data. +""" import collections from django.contrib.contenttypes.models import ContentType +from django.shortcuts import get_object_or_404 + from rest_framework import serializers from onadata.apps.logger.models import OpenData, XForm -from django.shortcuts import get_object_or_404 def get_data(request_data, update=False): - ''' + """ return a namedtuple with error, message and data values. - ''' - fields = ['object_id', 'data_type', 'name'] - results = collections.namedtuple('results', 'error message data') + """ + fields = ["object_id", "data_type", "name"] + results = collections.namedtuple("results", "error message data") if not update: if not set(fields).issubset(list(request_data)): return results( error=True, message="Fields object_id, data_type and name are required.", - data=None + data=None, ) - fields.append('active') + fields.append("active") # check if invalid fields are provided if any(a not in fields for a in list(request_data)): return results( error=True, message="Valid fields are object_id, data_type and name.", - data=None + data=None, ) data = {} for key in fields: available = request_data.get(key) is not None - available and data.update({key: request_data.get(key)}) + if available: + data.update({key: request_data.get(key)}) return results(error=False, message=None, data=data) class OpenDataSerializer(serializers.ModelSerializer): + """ + The OpenDataSerializer class - create/list OpenData model data. + """ + name = serializers.CharField(max_length=255, required=True) data_type = serializers.CharField(max_length=50, required=False) object_id = serializers.IntegerField(required=False) @@ -47,43 +57,41 @@ class OpenDataSerializer(serializers.ModelSerializer): class Meta: model = OpenData - exclude = ('date_created', 'date_modified', 'content_type', 'id') + exclude = ("date_created", "date_modified", "content_type", "id") def create(self, validated_data): results = get_data(validated_data) if results.error: raise serializers.ValidationError(results.message) - name = validated_data.get('name') - data_type = validated_data.get('data_type') - object_id = validated_data.get('object_id') - op = None + name = validated_data.get("name") + data_type = validated_data.get("data_type") + object_id = validated_data.get("object_id") - if data_type == 'xform': + if data_type == "xform": xform = get_object_or_404(XForm, id=object_id) - ct = ContentType.objects.get_for_model(xform) + content_type = ContentType.objects.get_for_model(xform) - op, created = OpenData.objects.get_or_create( + open_data, _created = OpenData.objects.get_or_create( object_id=object_id, defaults={ - 'name': name, - 'content_type': ct, - 'content_object': xform, - } + "name": name, + "content_type": content_type, + "content_object": xform, + }, ) - return op + return open_data + return None def update(self, instance, validated_data): results = get_data(validated_data, update=True) if results.error: raise serializers.ValidationError(results.message) - instance.name = validated_data.get('name', instance.name) - instance.object_id = validated_data.get( - 'object_id', instance.object_id - ) - instance.active = validated_data.get('active', instance.active) + instance.name = validated_data.get("name", instance.name) + instance.object_id = validated_data.get("object_id", instance.object_id) + instance.active = validated_data.get("active", instance.active) instance.save() return instance diff --git a/onadata/libs/serializers/organization_member_serializer.py b/onadata/libs/serializers/organization_member_serializer.py index 09df7131fe..3917b22cb8 100644 --- a/onadata/libs/serializers/organization_member_serializer.py +++ b/onadata/libs/serializers/organization_member_serializer.py @@ -1,36 +1,37 @@ -from django.contrib.auth.models import User -from django.utils.translation import gettext as _ +# -*- coding: utf-8 -*- +""" +The OrganizationMemberSerializer - manages a users access in an organization +""" +from django.contrib.auth import get_user_model from django.core.mail import send_mail +from django.utils.translation import gettext as _ from rest_framework import serializers -from onadata.libs.serializers.fields.organization_field import \ - OrganizationField -from onadata.libs.permissions import ROLES -from onadata.libs.permissions import OwnerRole -from onadata.libs.permissions import is_organization -from onadata.apps.api.tools import add_user_to_organization -from onadata.apps.api.tools import get_or_create_organization_owners_team -from onadata.apps.api.tools import add_user_to_team -from onadata.apps.api.tools import remove_user_from_team -from onadata.apps.api.tools import _get_owners -from onadata.apps.api.tools import get_organization_members -from onadata.apps.api.tools import remove_user_from_organization -from onadata.settings.common import (DEFAULT_FROM_EMAIL, SHARE_ORG_SUBJECT) +from onadata.apps.api.tools import ( + _get_owners, + add_user_to_organization, + add_user_to_team, + get_or_create_organization_owners_team, + get_organization_members, + remove_user_from_organization, + remove_user_from_team, +) from onadata.libs.models.share_project import ShareProject +from onadata.libs.permissions import ROLES, OwnerRole, is_organization +from onadata.libs.serializers.fields.organization_field import OrganizationField +from onadata.settings.common import DEFAULT_FROM_EMAIL, SHARE_ORG_SUBJECT + +User = get_user_model() def _compose_send_email(organization, user, email_msg, email_subject=None): if not email_subject: - email_subject = SHARE_ORG_SUBJECT.format(user.username, - organization.name) + email_subject = SHARE_ORG_SUBJECT.format(user.username, organization.name) # send out email message. - send_mail(email_subject, - email_msg, - DEFAULT_FROM_EMAIL, - (user.email, )) + send_mail(email_subject, email_msg, DEFAULT_FROM_EMAIL, (user.email,)) def _set_organization_role_to_user(organization, user, role): @@ -52,6 +53,10 @@ def _set_organization_role_to_user(organization, user, role): class OrganizationMemberSerializer(serializers.Serializer): + """ + The OrganizationMemberSerializer - manages a users access in an organization + """ + organization = OrganizationField() username = serializers.CharField(max_length=255, required=False) role = serializers.CharField(max_length=50, required=False) @@ -59,41 +64,43 @@ class OrganizationMemberSerializer(serializers.Serializer): email_subject = serializers.CharField(max_length=255, required=False) remove = serializers.BooleanField(default=False) + def update(self, instance, validated_data): + # Do nothing + pass + def validate_username(self, value): """Check that the username exists""" user = None try: user = User.objects.get(username=value) - except User.DoesNotExist: - raise serializers.ValidationError(_( - u"User '%(value)s' does not exist." % {"value": value} - )) + except User.DoesNotExist as exc: + raise serializers.ValidationError( + _(f"User '{value}' does not exist.") + ) from exc else: if not user.is_active: - raise serializers.ValidationError(_(u"User is not active")) + raise serializers.ValidationError(_("User is not active")) if is_organization(user.profile): raise serializers.ValidationError( - _(u"Cannot add org account `{}` as member." - .format(user.username))) + _(f"Cannot add org account `{user.username}` as member.") + ) return value def validate_role(self, value): """check that the role exists""" if value not in ROLES: - raise serializers.ValidationError(_( - u"Unknown role '%(role)s'." % {"role": value} - )) + raise serializers.ValidationError(_(f"Unknown role '{value}'.")) return value def validate(self, attrs): - remove = attrs.get('remove') - role = attrs.get('role') - organization = attrs.get('organization') - username = attrs.get('username') + remove = attrs.get("remove") + role = attrs.get("role") + organization = attrs.get("organization") + username = attrs.get("username") # check if roles are downgrading and the user is the last admin if username and (remove or role != OwnerRole.name): @@ -102,17 +109,18 @@ def validate(self, attrs): owners = _get_owners(organization) if user in owners and len(owners) <= 1: raise serializers.ValidationError( - _("Organization cannot be without an owner")) + _("Organization cannot be without an owner") + ) return attrs def create(self, validated_data): organization = validated_data.get("organization") username = validated_data.get("username") - role = validated_data.get('role') - email_msg = validated_data.get('email_msg') - email_subject = validated_data.get('email_subject') - remove = validated_data.get('remove') + role = validated_data.get("role") + email_msg = validated_data.get("email_msg") + email_subject = validated_data.get("email_subject") + remove = validated_data.get("remove") if username: user = User.objects.get(username=username) @@ -123,14 +131,14 @@ def create(self, validated_data): _set_organization_role_to_user(organization, user, role) if email_msg: - _compose_send_email(organization, user, email_msg, - email_subject) + _compose_send_email(organization, user, email_msg, email_subject) if remove: remove_user_from_organization(organization, user) return organization + @property def data(self): organization = self.validated_data.get("organization") members = get_organization_members(organization) diff --git a/onadata/libs/serializers/organization_serializer.py b/onadata/libs/serializers/organization_serializer.py index a000e611df..eb604f0d0b 100644 --- a/onadata/libs/serializers/organization_serializer.py +++ b/onadata/libs/serializers/organization_serializer.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Organization Serializer """ @@ -92,7 +92,7 @@ def create(self, validated_data): return profile - def validate_org(self, value): # pylint: disable=no-self-use + def validate_org(self, value): """ Validate organization name. """ @@ -116,7 +116,7 @@ def validate_org(self, value): # pylint: disable=no-self-use raise serializers.ValidationError(_(f"Organization {org} already exists.")) - def get_users(self, obj): # pylint: disable=no-self-use + def get_users(self, obj): """ Return organization members. """ diff --git a/onadata/libs/serializers/password_reset_serializer.py b/onadata/libs/serializers/password_reset_serializer.py index 01ab28888c..e014abb34f 100644 --- a/onadata/libs/serializers/password_reset_serializer.py +++ b/onadata/libs/serializers/password_reset_serializer.py @@ -176,7 +176,6 @@ class PasswordResetSerializer(serializers.Serializer): label=_("Email Subject"), required=False, max_length=78, allow_blank=True ) - # pylint: disable=no-self-use def validate_email(self, value): """ Validates the email. @@ -214,7 +213,6 @@ class PasswordResetChangeSerializer(serializers.Serializer): new_password = serializers.CharField(min_length=4, max_length=128) token = serializers.CharField(max_length=128) - # pylint: disable=no-self-use def validate_uid(self, value): """ Validate the user uid. diff --git a/onadata/libs/serializers/project_serializer.py b/onadata/libs/serializers/project_serializer.py index 24c611ac3e..29bdd404ef 100644 --- a/onadata/libs/serializers/project_serializer.py +++ b/onadata/libs/serializers/project_serializer.py @@ -2,8 +2,6 @@ """ Project Serializer module. """ -from six import itervalues - from django.conf import settings from django.contrib.auth import get_user_model from django.core.cache import cache @@ -11,13 +9,15 @@ from django.utils.translation import gettext as _ from rest_framework import serializers +from six import itervalues -from onadata.apps.api.models import OrganizationProfile -from onadata.apps.api.tools import ( +from onadata.apps.api.models.organization_profile import ( + OrganizationProfile, get_or_create_organization_owners_team, get_organization_members_team, ) -from onadata.apps.logger.models import Project, XForm +from onadata.apps.logger.models.project import Project +from onadata.apps.logger.models.xform import XForm from onadata.libs.permissions import ( ManagerRole, OwnerRole, @@ -25,10 +25,9 @@ get_role, is_organization, ) -from onadata.libs.serializers.dataview_serializer import DataViewMinimalSerializer from onadata.libs.serializers.fields.json_field import JsonField from onadata.libs.serializers.tag_list_serializer import TagListSerializer -from onadata.libs.utils.analytics import track_object_event +from onadata.libs.utils.analytics import TrackObjectEvent from onadata.libs.utils.cache_tools import ( PROJ_BASE_FORMS_CACHE, PROJ_FORMS_CACHE, @@ -250,7 +249,7 @@ class Meta: "is_merged_dataset", ) - def get_published_by_formbuilder(self, obj): # pylint: disable=no-self-use + def get_published_by_formbuilder(self, obj): """ Returns true if the form was published by formbuilder. """ @@ -346,19 +345,19 @@ def get_forms(self, obj): return forms - def get_num_datasets(self, obj): # pylint: disable=no-self-use + def get_num_datasets(self, obj): """ Return the number of datasets attached to the project. """ return get_num_datasets(obj) - def get_last_submission_date(self, obj): # pylint: disable=no-self-use + def get_last_submission_date(self, obj): """ Return the most recent submission date to any of the projects datasets. """ return get_last_submission_date(obj) - def get_teams(self, obj): # pylint: disable=no-self-use + def get_teams(self, obj): """ Return the teams with access to the project. """ @@ -448,7 +447,7 @@ def validate(self, attrs): ) return attrs - def validate_public(self, value): # pylint: disable=no-self-use + def validate_public(self, value): """ Validate the public field """ @@ -458,7 +457,7 @@ def validate_public(self, value): # pylint: disable=no-self-use ) return value - def validate_metadata(self, value): # pylint: disable=no-self-use + def validate_metadata(self, value): """ Validate metadaata is a valid JSON value. """ @@ -520,7 +519,7 @@ def update(self, instance, validated_data): return instance - @track_object_event( + @TrackObjectEvent( user_field="created_by", properties={ "created_by": "created_by", @@ -557,7 +556,7 @@ def create(self, validated_data): cache.set(f"{PROJ_OWNER_CACHE}{project.pk}", response) return project - def get_users(self, obj): # pylint: disable=no-self-use + def get_users(self, obj): """ Return a list of users and organizations that have access to the project. @@ -565,7 +564,7 @@ def get_users(self, obj): # pylint: disable=no-self-use return get_users(obj, self.context) @check_obj - def get_forms(self, obj): # pylint: disable=no-self-use + def get_forms(self, obj): """ Return list of xforms in the project. """ @@ -583,25 +582,25 @@ def get_forms(self, obj): # pylint: disable=no-self-use return forms - def get_num_datasets(self, obj): # pylint: disable=no-self-use + def get_num_datasets(self, obj): """ Return the number of datasets attached to the project. """ return get_num_datasets(obj) - def get_last_submission_date(self, obj): # pylint: disable=no-self-use + def get_last_submission_date(self, obj): """ Return the most recent submission date to any of the projects datasets. """ return get_last_submission_date(obj) - def get_starred(self, obj): # pylint: disable=no-self-use + def get_starred(self, obj): """ Return True if request user has starred this project. """ return is_starred(obj, self.context["request"]) - def get_teams(self, obj): # pylint: disable=no-self-use + def get_teams(self, obj): """ Return the teams with access to the project. """ @@ -623,6 +622,11 @@ def get_data_views(self, obj): else obj.dataview_set.filter(deleted_at__isnull=True) ) + # pylint: disable=import-outside-toplevel + from onadata.libs.serializers.dataview_serializer import ( + DataViewMinimalSerializer, + ) + serializer = DataViewMinimalSerializer( data_views_obj, many=True, context=self.context ) diff --git a/onadata/libs/serializers/restservices_serializer.py b/onadata/libs/serializers/restservices_serializer.py index c137fe95b9..a5b2aacaa2 100644 --- a/onadata/libs/serializers/restservices_serializer.py +++ b/onadata/libs/serializers/restservices_serializer.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +The RestServiceSerializer class - create, list a rest service. +""" from rest_framework import serializers from onadata.apps.logger.models import XForm @@ -5,14 +9,25 @@ class RestServiceSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(source='pk', read_only=True) - xform = serializers.PrimaryKeyRelatedField( - queryset=XForm.objects.all() - ) + """ + The RestServiceSerializer class - create, list a rest service. + """ + + # pylint: disable=invalid-name + id = serializers.IntegerField(source="pk", read_only=True) + xform = serializers.PrimaryKeyRelatedField(queryset=XForm.objects.all()) name = serializers.CharField(max_length=50) service_url = serializers.URLField(required=True) class Meta: model = RestService - fields = ('id', 'xform', 'name', 'service_url', 'date_created', - 'date_modified', 'active', 'inactive_reason') + fields = ( + "id", + "xform", + "name", + "service_url", + "date_created", + "date_modified", + "active", + "inactive_reason", + ) diff --git a/onadata/libs/serializers/share_project_serializer.py b/onadata/libs/serializers/share_project_serializer.py index 342d91eea3..0e8eddf1b2 100644 --- a/onadata/libs/serializers/share_project_serializer.py +++ b/onadata/libs/serializers/share_project_serializer.py @@ -1,24 +1,34 @@ -from django.contrib.auth.models import User +# -*- coding: utf-8 -*- +""" +The ShareProjectSerializer class - support sharing a project. +""" +from django.contrib.auth import get_user_model from django.utils.translation import gettext as _ from rest_framework import serializers + from onadata.libs.models.share_project import ShareProject -from onadata.libs.permissions import get_object_users_with_permissions -from onadata.libs.permissions import ROLES -from onadata.libs.permissions import OwnerRole +from onadata.libs.permissions import ROLES, OwnerRole, get_object_users_with_permissions from onadata.libs.serializers.fields.project_field import ProjectField +User = get_user_model() + def attrs_to_instance(attrs, instance): - instance.project = attrs.get('project', instance.project) - instance.username = attrs.get('username', instance.username) - instance.role = attrs.get('role', instance.role) - instance.remove = attrs.get('remove', instance.remove) + """Apply attributes into a class object from a dict.""" + instance.project = attrs.get("project", instance.project) + instance.username = attrs.get("username", instance.username) + instance.role = attrs.get("role", instance.role) + instance.remove = attrs.get("remove", instance.remove) return instance class ShareProjectSerializer(serializers.Serializer): + """ + The ShareProjectSerializer class - support sharing a project. + """ + project = ProjectField() username = serializers.CharField(max_length=255) role = serializers.CharField(max_length=50) @@ -26,8 +36,8 @@ class ShareProjectSerializer(serializers.Serializer): def create(self, validated_data): created_instances = [] - for username in validated_data.pop('username').split(','): - validated_data['username'] = username + for username in validated_data.pop("username").split(","): + validated_data["username"] = username instance = ShareProject(**validated_data) instance.save() created_instances.append(instance) @@ -41,24 +51,26 @@ def update(self, instance, validated_data): return instance def validate(self, attrs): - for username in attrs.get('username').split(','): + for username in attrs.get("username").split(","): user = User.objects.get(username=username) - project = attrs.get('project') + project = attrs.get("project") # check if the user is the owner of the project if user and project: if user == project.organization: - raise serializers.ValidationError({ - 'username': - _(u"Cannot share project with the owner (%(value)s)" % - {"value": user.username}) - }) + raise serializers.ValidationError( + { + "username": _( + f"Cannot share project with the owner ({user.username})" + ) + } + ) return attrs def validate_username(self, value): """Check that the username exists""" - usernames = [u.strip() for u in value.split(',')] + usernames = [u.strip() for u in value.split(",")] user = None non_existent_users = [] inactive_users = [] @@ -73,31 +85,30 @@ def validate_username(self, value): inactive_users.append(username) if non_existent_users: - non_existent_users = ', '.join(non_existent_users) + non_existent_users = ", ".join(non_existent_users) raise serializers.ValidationError( - _('The following user(s) does/do not exist:' - f' {non_existent_users}' - )) + _(f"The following user(s) does/do not exist: {non_existent_users}") + ) if inactive_users: - inactive_users = ', '.join(inactive_users) + inactive_users = ", ".join(inactive_users) raise serializers.ValidationError( - _(f'The following user(s) is/are not active: {inactive_users}') + _(f"The following user(s) is/are not active: {inactive_users}") ) - return (',').join(usernames) + return (",").join(usernames) def validate_role(self, value): """check that the role exists""" if value not in ROLES: - raise serializers.ValidationError(_( - u"Unknown role '%(role)s'." % {"role": value} - )) + raise serializers.ValidationError(_(f"Unknown role '{value}'.")) return value class RemoveUserFromProjectSerializer(ShareProjectSerializer): + """RemoveUserFromProjectSerializer class - removes a user's access to a project.""" + remove = serializers.BooleanField() def update(self, instance, validated_data): @@ -113,20 +124,18 @@ def create(self, validated_data): return instance def validate(self, attrs): - """ Check and confirm that the project will be left with at least one - owner. Raises a validation error if only one owner found""" + """Check and confirm that the project will be left with at least one + owner. Raises a validation error if only one owner found""" - if attrs.get('role') == OwnerRole.name: - results = get_object_users_with_permissions(attrs.get('project')) + if attrs.get("role") == OwnerRole.name: + results = get_object_users_with_permissions(attrs.get("project")) # count all the owners - count = len( - [res for res in results if res.get('role') == OwnerRole.name] - ) + count = len([res for res in results if res.get("role") == OwnerRole.name]) if count <= 1: - raise serializers.ValidationError({ - 'remove': _(u"Project requires at least one owner") - }) + raise serializers.ValidationError( + {"remove": _("Project requires at least one owner")} + ) return attrs diff --git a/onadata/libs/serializers/share_team_project_serializer.py b/onadata/libs/serializers/share_team_project_serializer.py index 780be95304..bf0c472e63 100644 --- a/onadata/libs/serializers/share_team_project_serializer.py +++ b/onadata/libs/serializers/share_team_project_serializer.py @@ -19,7 +19,6 @@ class ShareTeamProjectSerializer(serializers.Serializer): project = ProjectField() role = serializers.CharField(max_length=50) - # pylint: disable=no-self-use def update(self, instance, validated_data): """Update project sharing properties.""" instance.team = validated_data.get("team", instance.team) @@ -29,7 +28,6 @@ def update(self, instance, validated_data): return instance - # pylint: disable=no-self-use def create(self, validated_data): """Shares a project to a team.""" instance = ShareTeamProject(**validated_data) @@ -37,7 +35,6 @@ def create(self, validated_data): return instance - # pylint: disable=no-self-use def validate_role(self, value): """check that the role exists""" @@ -52,7 +49,6 @@ class RemoveTeamFromProjectSerializer(ShareTeamProjectSerializer): remove = serializers.BooleanField() - # pylint: disable=no-self-use def update(self, instance, validated_data): """Remove a team from a project""" instance.remove = validated_data.get("remove", instance.remove) @@ -60,7 +56,6 @@ def update(self, instance, validated_data): return instance - # pylint: disable=no-self-use def create(self, validated_data): """Remove a team from a project""" instance = ShareTeamProject(**validated_data) diff --git a/onadata/libs/serializers/share_xform_serializer.py b/onadata/libs/serializers/share_xform_serializer.py index c1c0f9e9e9..d89df7826d 100644 --- a/onadata/libs/serializers/share_xform_serializer.py +++ b/onadata/libs/serializers/share_xform_serializer.py @@ -19,7 +19,7 @@ class ShareXFormSerializer(serializers.Serializer): username = serializers.CharField(max_length=255) role = serializers.CharField(max_length=50) - # pylint: disable=unused-argument,no-self-use + # pylint: disable=unused-argument def update(self, instance, validated_data): """Make changes to form share to a user.""" instance.xform = validated_data.get("xform", instance.xform) @@ -29,7 +29,7 @@ def update(self, instance, validated_data): return instance - # pylint: disable=unused-argument,no-self-use + # pylint: disable=unused-argument def create(self, validated_data): """Assign role permission for a form to a user.""" instance = ShareXForm(**validated_data) @@ -37,7 +37,6 @@ def create(self, validated_data): return instance - # pylint: disable=no-self-use def validate_username(self, value): """Check that the username exists""" # pylint: disable=invalid-name @@ -51,7 +50,6 @@ def validate_username(self, value): return value - # pylint: disable=no-self-use def validate_role(self, value): """check that the role exists""" if value not in ROLES: diff --git a/onadata/libs/serializers/stats_serializer.py b/onadata/libs/serializers/stats_serializer.py index 2db6c85f6f..36e2b394d8 100644 --- a/onadata/libs/serializers/stats_serializer.py +++ b/onadata/libs/serializers/stats_serializer.py @@ -2,27 +2,24 @@ """ Stats API endpoint serializer. """ -from django.utils.translation import gettext as _ -from django.core.cache import cache from django.conf import settings +from django.core.cache import cache +from django.utils.translation import gettext as _ -from rest_framework import exceptions -from rest_framework import serializers +from rest_framework import exceptions, serializers from rest_framework.utils.serializer_helpers import ReturnList +from onadata.apps.logger.models.xform import XForm +from onadata.libs.data.query import get_form_submissions_grouped_by_field from onadata.libs.data.statistics import ( - get_median_for_numeric_fields_in_form, + get_all_stats, get_mean_for_numeric_fields_in_form, - get_mode_for_numeric_fields_in_form, + get_median_for_numeric_fields_in_form, get_min_max_range, - get_all_stats, + get_mode_for_numeric_fields_in_form, ) -from onadata.apps.logger.models.xform import XForm -from onadata.libs.data.query import get_form_submissions_grouped_by_field - from onadata.libs.utils.cache_tools import XFORM_SUBMISSION_STAT - SELECT_FIELDS = ["select one", "select multiple"] STATS_FUNCTIONS = { @@ -61,7 +58,7 @@ def to_representation(self, instance): if field is None: raise exceptions.ParseError( - _("Expecting `group` and `name`" " query parameters.") + _("Expecting `group` and `name` query parameters.") ) cache_key = f"{XFORM_SUBMISSION_STAT}{instance.pk}{field}{name}" diff --git a/onadata/libs/serializers/submission_review_serializer.py b/onadata/libs/serializers/submission_review_serializer.py index 8ed9e0c52f..5487e2142e 100644 --- a/onadata/libs/serializers/submission_review_serializer.py +++ b/onadata/libs/serializers/submission_review_serializer.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Submission Review Serializer Module """ @@ -6,55 +7,66 @@ from rest_framework import exceptions, serializers from onadata.apps.logger.models import Note, SubmissionReview -from onadata.libs.utils.common_tags import (COMMENT_REQUIRED, - SUBMISSION_REVIEW_INSTANCE_FIELD) +from onadata.libs.utils.common_tags import ( + COMMENT_REQUIRED, + SUBMISSION_REVIEW_INSTANCE_FIELD, +) class SubmissionReviewSerializer(serializers.ModelSerializer): """ SubmissionReviewSerializer Class """ + note = serializers.CharField( - source='note.note', required=False, allow_blank=True, - allow_null=True) + source="note.note", required=False, allow_blank=True, allow_null=True + ) class Meta: """ Meta Options for SubmissionReviewSerializer """ + model = SubmissionReview - fields = ('id', 'instance', 'created_by', 'status', 'date_created', - 'note', 'date_modified') + fields = ( + "id", + "instance", + "created_by", + "status", + "date_created", + "note", + "date_modified", + ) def validate(self, attrs): """ Custom Validate Method for SubmissionReviewSerializer """ - status = attrs.get('status') - note = attrs.get('note') + status = attrs.get("status") + note = attrs.get("note") if status == SubmissionReview.REJECTED and not note: - raise exceptions.ValidationError({'note': COMMENT_REQUIRED}) + raise exceptions.ValidationError({"note": COMMENT_REQUIRED}) return attrs def create(self, validated_data): """ Custom create method for SubmissionReviewSerializer """ - request = self.context.get('request') + request = self.context.get("request") if request: - validated_data['created_by'] = request.user + validated_data["created_by"] = request.user - if 'note' in validated_data: - note_data = validated_data.pop('note') - if note_data['note']: - note_data['instance'] = validated_data.get('instance') - note_data['created_by'] = validated_data.get('created_by') - note_data['instance_field'] = SUBMISSION_REVIEW_INSTANCE_FIELD + if "note" in validated_data: + note_data = validated_data.pop("note") + if note_data["note"]: + note_data["instance"] = validated_data.get("instance") + note_data["created_by"] = validated_data.get("created_by") + note_data["instance_field"] = SUBMISSION_REVIEW_INSTANCE_FIELD note = Note.objects.create(**note_data) - validated_data['note'] = note + validated_data["note"] = note submission_review = SubmissionReview.objects.create(**validated_data) @@ -65,12 +77,12 @@ def update(self, instance, validated_data): Custom update method for SubmissionReviewSerializer """ note = instance.note - note_data = validated_data.pop('note') + note_data = validated_data.pop("note") - note.note = note_data['note'] + note.note = note_data["note"] note.save() - instance.status = validated_data.get('status', instance.status) + instance.status = validated_data.get("status", instance.status) instance.save() diff --git a/onadata/libs/serializers/team_serializer.py b/onadata/libs/serializers/team_serializer.py index 6ce5888b5b..8adf09fd61 100644 --- a/onadata/libs/serializers/team_serializer.py +++ b/onadata/libs/serializers/team_serializer.py @@ -1,31 +1,43 @@ -from django.contrib.auth.models import User +# -*- coding: utf-8 -*- +""" +The TeamSerializer class - access and update Team model objects. +""" +from django.contrib.auth import get_user_model + from rest_framework import serializers -from onadata.libs.serializers.fields.hyperlinked_multi_identity_field import\ - HyperlinkedMultiIdentityField -from onadata.libs.serializers.user_serializer import UserSerializer from onadata.apps.api.models import OrganizationProfile, Team from onadata.apps.logger.models import Project from onadata.libs.permissions import get_team_project_default_permissions +from onadata.libs.serializers.fields.hyperlinked_multi_identity_field import ( + HyperlinkedMultiIdentityField, +) +from onadata.libs.serializers.user_serializer import UserSerializer + +User = get_user_model() class TeamSerializer(serializers.Serializer): - teamid = serializers.ReadOnlyField(source='id') - url = HyperlinkedMultiIdentityField(view_name='team-detail') - name = serializers.CharField(max_length=100, source='team_name', - required=True) + """ + The TeamSerializer class - access and update Team model objects. + """ + + teamid = serializers.ReadOnlyField(source="id") + url = HyperlinkedMultiIdentityField(view_name="team-detail") + name = serializers.CharField(max_length=100, source="team_name", required=True) organization = serializers.SlugRelatedField( - slug_field='username', - queryset=User.objects.filter( - pk__in=OrganizationProfile.objects.values('user'))) + slug_field="username", + queryset=User.objects.filter(pk__in=OrganizationProfile.objects.values("user")), + ) projects = serializers.SerializerMethodField() users = serializers.SerializerMethodField() def get_users(self, obj): + """Returns a users in a team.""" users = [] if obj: - for user in obj.user_set.all(): + for user in obj.user_set.filter(is_active=True): users.append(UserSerializer(instance=user).data) return users @@ -35,23 +47,23 @@ def get_projects(self, obj): projects = [] if obj: - for project in Project.objects.filter( - organization__id=obj.organization.id): + for project in Project.objects.filter(organization__id=obj.organization.id): project_map = {} - project_map['name'] = project.name - project_map['projectid'] = project.pk - project_map['default_role'] = \ - get_team_project_default_permissions(obj, project) + project_map["name"] = project.name + project_map["projectid"] = project.pk + project_map["default_role"] = get_team_project_default_permissions( + obj, project + ) projects.append(project_map) return projects def update(self, instance, validated_data): - org = validated_data.get('organization', None) - projects = validated_data.get('projects', []) + org = validated_data.get("organization", None) + projects = validated_data.get("projects", []) instance.organization = org if org else instance.organization - instance.name = validated_data.get('team_name', instance.name) + instance.name = validated_data.get("team_name", instance.name) instance.projects.clear() for project in projects: @@ -62,9 +74,9 @@ def update(self, instance, validated_data): return instance def create(self, validated_data): - org = validated_data.get('organization', None) - team_name = validated_data.get('team_name', None) - request = self.context.get('request') + org = validated_data.get("organization", None) + team_name = validated_data.get("team_name", None) + request = self.context.get("request") created_by = request.user return Team.objects.create( diff --git a/onadata/libs/serializers/textit_serializer.py b/onadata/libs/serializers/textit_serializer.py index 6293c1cf2e..982b5a24f9 100644 --- a/onadata/libs/serializers/textit_serializer.py +++ b/onadata/libs/serializers/textit_serializer.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +The TextItSerializer - supports creating TextIt integration service. +""" from django.conf import settings from rest_framework import serializers @@ -8,7 +12,12 @@ class TextItSerializer(serializers.Serializer): - id = serializers.IntegerField(source='pk', read_only=True) + """ + The TextItSerializer - supports creating TextIt integration service. + """ + + # pylint: disable=invalid-name + id = serializers.IntegerField(source="pk", read_only=True) xform = XFormField() auth_token = serializers.CharField(max_length=255, required=True) flow_title = serializers.CharField(max_length=255, default="") @@ -23,36 +32,47 @@ class TextItSerializer(serializers.Serializer): def to_representation(self, instance): meta_data = MetaData.objects.filter( - data_type=TEXTIT_DETAILS, object_id=instance.xform.pk).first() + data_type=TEXTIT_DETAILS, object_id=instance.xform.pk + ).first() flow_title = "" if meta_data: flow_title = meta_data.data_value - text_it = TextItService(pk=instance.pk, xform=instance.xform, - service_url=instance.service_url, - name=instance.name, - flow_title=flow_title) + text_it = TextItService( + pk=instance.pk, + xform=instance.xform, + service_url=instance.service_url, + name=instance.name, + flow_title=flow_title, + ) text_it.date_modified = instance.date_modified text_it.date_created = instance.date_created text_it.active = instance.active text_it.inactive_reason = instance.inactive_reason text_it.retrieve() - return super(TextItSerializer, self).to_representation(text_it) + return super().to_representation(text_it) def update(self, instance, validated_data): - data_value = MetaData.textit(instance.xform) or '' + data_value = MetaData.textit(instance.xform) or "" values = data_value.split(settings.METADATA_SEPARATOR) if len(values) < 3: - values = ['', '', ''] - xform = validated_data.get('xform', instance.xform) - auth_token = validated_data.get('auth_token', values[0]) - flow_uuid = validated_data.get('flow_uuid', values[1]) - contacts = validated_data.get('contacts', values[2]) - name = validated_data.get('name', instance.name) - service_url = validated_data.get('service_url', instance.service_url) + values = ["", "", ""] + xform = validated_data.get("xform", instance.xform) + auth_token = validated_data.get("auth_token", values[0]) + flow_uuid = validated_data.get("flow_uuid", values[1]) + contacts = validated_data.get("contacts", values[2]) + name = validated_data.get("name", instance.name) + service_url = validated_data.get("service_url", instance.service_url) - instance = TextItService(xform, service_url, name, auth_token, - flow_uuid, contacts, instance.pk, - flow_title=validated_data.get('flow_title')) + instance = TextItService( + xform, + service_url, + name, + auth_token, + flow_uuid, + contacts, + instance.pk, + flow_title=validated_data.get("flow_title"), + ) instance.save() return instance diff --git a/onadata/libs/serializers/user_profile_serializer.py b/onadata/libs/serializers/user_profile_serializer.py index 8172a3fb6a..b31aa16535 100644 --- a/onadata/libs/serializers/user_profile_serializer.py +++ b/onadata/libs/serializers/user_profile_serializer.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ UserProfile Serializers. """ @@ -26,7 +26,7 @@ from onadata.libs.authentication import expired from onadata.libs.permissions import CAN_VIEW_PROFILE, is_organization from onadata.libs.serializers.fields.json_field import JsonField -from onadata.libs.utils.analytics import track_object_event +from onadata.libs.utils.analytics import TrackObjectEvent from onadata.libs.utils.cache_tools import IS_ORG from onadata.libs.utils.email import get_verification_email_data, get_verification_url @@ -42,7 +42,7 @@ def _get_first_last_names(name, limit=30): if not isinstance(name, six.string_types): return name, name - if name.__len__() > (limit * 2): + if len(name) > (limit * 2): # since we are using the default django User Model, there is an # imposition of 30 characters on both first_name and last_name hence # ensure we only have 30 characters for either field @@ -179,7 +179,7 @@ def __init__(self, *args, **kwargs): for field in getattr(self.Meta, "owner_only_fields"): self.fields.pop(field) - def get_is_org(self, obj): # pylint: disable=no-self-use + def get_is_org(self, obj): """ Returns True if it is an organization profile. """ @@ -255,7 +255,7 @@ def update(self, instance, validated_data): return super().update(instance, params) - @track_object_event( + @TrackObjectEvent( user_field="user", properties={"name": "name", "country": "country"} ) def create(self, validated_data): @@ -348,7 +348,7 @@ def validate_email(self, value): return value - def validate_twitter(self, value): # pylint: disable=no-self-use + def validate_twitter(self, value): """ Checks if the twitter handle is valid. """ @@ -412,14 +412,12 @@ class Meta: "temp_token", ) - # pylint: disable=no-self-use def get_api_token(self, obj): """ Returns user's API Token. """ return obj.user.auth_token.key - # pylint: disable=no-self-use def get_temp_token(self, obj): """ This should return a valid temp token for this user profile. diff --git a/onadata/libs/serializers/user_serializer.py b/onadata/libs/serializers/user_serializer.py index 9f47ec52fc..e0767fbded 100644 --- a/onadata/libs/serializers/user_serializer.py +++ b/onadata/libs/serializers/user_serializer.py @@ -1,8 +1,19 @@ -from django.contrib.auth.models import User +# -*- coding: utf-8 -*- +""" +The UserSerializer class - Users serializer +""" +from django.contrib.auth import get_user_model from rest_framework import serializers +User = get_user_model() + + class UserSerializer(serializers.HyperlinkedModelSerializer): + """ + The UserSerializer class - Users serializer + """ + class Meta: model = User - fields = ('id', 'username', 'first_name', 'last_name') + fields = ("id", "username", "first_name", "last_name") diff --git a/onadata/libs/serializers/widget_serializer.py b/onadata/libs/serializers/widget_serializer.py index 88d1ac63ae..f531153dcb 100644 --- a/onadata/libs/serializers/widget_serializer.py +++ b/onadata/libs/serializers/widget_serializer.py @@ -63,8 +63,7 @@ def to_representation(self, value): self._setup_field(self.view_name) - # pylint: disable=bad-super-call - return super(GenericRelatedField, self).to_representation(value) + return super().to_representation(value) def to_internal_value(self, data): """Verifies that ``data`` is a valid URL.""" diff --git a/onadata/libs/serializers/xform_serializer.py b/onadata/libs/serializers/xform_serializer.py index 6bcb4789e6..c6d054157f 100644 --- a/onadata/libs/serializers/xform_serializer.py +++ b/onadata/libs/serializers/xform_serializer.py @@ -166,7 +166,6 @@ class XFormMixin: XForm mixins """ - # pylint: disable=no-self-use def get_xls_available(self, obj): """ Returns True if ``obj.xls.url`` is not None, indicates XLS is present. @@ -187,7 +186,6 @@ def _get_metadata(self, obj, key): return None - # pylint: disable=no-self-use def get_users(self, obj): """ Returns a list of users based on XForm permissions. @@ -305,7 +303,6 @@ def get_data_views(self, obj): return data_views return [] - # pylint: disable=no-self-use def get_num_of_submissions(self, obj): """ Returns number of submissions. @@ -476,7 +473,6 @@ class Meta: "deleted_by", ) - # pylint: disable=no-self-use def get_metadata(self, obj): """ Returns XForn ``obj`` metadata. @@ -496,7 +492,7 @@ def get_metadata(self, obj): return xform_metadata - def validate_public_key(self, value): # pylint: disable=no-self-use + def validate_public_key(self, value): """ Checks that the given RSA public key is a valid key by trying to use the key data to create an RSA key object using the cryptography @@ -510,7 +506,7 @@ def validate_public_key(self, value): # pylint: disable=no-self-use ) from e return clean_public_key(value) - def _check_if_allowed_public(self, value): # pylint: disable=no-self-use + def _check_if_allowed_public(self, value): """ Verify that users are allowed to create public forms @@ -531,7 +527,6 @@ def validate_public(self, value): """ return self._check_if_allowed_public(value) - # pylint: disable=no-self-use def get_form_versions(self, obj): """ Returns all form versions. @@ -565,7 +560,6 @@ class XFormCreateSerializer(XFormSerializer): has_id_string_changed = serializers.SerializerMethodField() - # pylint: disable=no-self-use def get_has_id_string_changed(self, obj): """ Returns the value of ``obj.has_id_string_changed`` @@ -646,7 +640,6 @@ def get_url(self, obj): return url - # pylint: disable=no-self-use @check_obj def get_hash(self, obj): """ @@ -683,7 +676,6 @@ def get_hash(self, obj): return f"{hsh or 'md5:'}" - # pylint: disable=no-self-use @check_obj def get_filename(self, obj): """ diff --git a/onadata/libs/test_utils/md_table.py b/onadata/libs/test_utils/md_table.py index 01a4850e8b..c79de9606e 100644 --- a/onadata/libs/test_utils/md_table.py +++ b/onadata/libs/test_utils/md_table.py @@ -22,8 +22,7 @@ def _extract_array(mdtablerow): mtchstr = match.groups()[0] if re.match(r"^[\|-]+$", mtchstr): return False - else: - return [_strp_cell(c) for c in mtchstr.split("|")] + return [_strp_cell(c) for c in mtchstr.split("|")] return False @@ -37,6 +36,7 @@ def _is_null_row(r_arr): def md_table_to_ss_structure(mdstr: str) -> List[Tuple[str, List[List[str]]]]: + """Transform markdown to an ss structure""" ss_arr = [] for item in mdstr.split("\n"): arr = _extract_array(item) @@ -61,12 +61,13 @@ def md_table_to_ss_structure(mdstr: str) -> List[Tuple[str, List[List[str]]]]: def md_table_to_workbook(mdstr: str) -> Workbook: """ - Convert Markdown table string to an openpyxl.Workbook. Call wb.save() to persist. + Convert Markdown table string to an openpyxl.Workbook. Call workbook.save() to + persist. """ md_data = md_table_to_ss_structure(mdstr=mdstr) - wb = Workbook(write_only=True) + workbook = Workbook(write_only=True) for key, rows in md_data: - sheet = wb.create_sheet(title=key) - for r in rows: - sheet.append(r) - return wb + sheet = workbook.create_sheet(title=key) + for row in rows: + sheet.append(row) + return workbook diff --git a/onadata/libs/test_utils/pyxform_test_case.py b/onadata/libs/test_utils/pyxform_test_case.py index 6fd6126430..4d46c83bf9 100644 --- a/onadata/libs/test_utils/pyxform_test_case.py +++ b/onadata/libs/test_utils/pyxform_test_case.py @@ -13,20 +13,21 @@ from lxml import etree -# noinspection PyProtectedMember -from lxml.etree import _Element - from pyxform.builder import create_survey_element_from_dict from pyxform.errors import PyXFormError from pyxform.utils import NSMAP from pyxform.validators.odk_validate import ODKValidateError, check_xform from pyxform.xls2json import workbook_to_json + from onadata.libs.test_utils.md_table import md_table_to_ss_structure logger = logging.getLogger(__name__) logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG) +# noinspection PyProtectedMember +_Element = etree._Element # pylint: disable=protected-access + if TYPE_CHECKING: from typing import Dict, List, Set, Tuple, Union @@ -35,11 +36,13 @@ class PyxformTestError(Exception): - pass + """Pyxform test errors exception class.""" @dataclass class MatcherContext: + """Data class to store assertion context information.""" + debug: bool nsmap_xpath: "Dict[str, str]" nsmap_subs: "NSMAPSubs" @@ -47,9 +50,10 @@ class MatcherContext: class PyxformMarkdown: - """Transform markdown formatted xlsform to a pyxform survey object""" + """Transform markdown formatted XLSForm to a pyxform survey object""" def md_to_pyxform_survey(self, md_raw, kwargs=None, autoname=True, warnings=None): + """Transform markdown formatted XLSForm to pyxform survey object.""" if kwargs is None: kwargs = {} if autoname: @@ -59,13 +63,13 @@ def md_to_pyxform_survey(self, md_raw, kwargs=None, autoname=True, warnings=None if re.match(r"^\s+#", line): # ignore lines which start with pound sign continue - elif re.match(r"^(.*)(#[^|]+)$", line): + if re.match(r"^(.*)(#[^|]+)$", line): # keep everything before the # outside of the last occurrence # of | _md.append(re.match(r"^(.*)(#[^|]+)$", line).groups()[0].strip()) else: _md.append(line.strip()) - md = "\n".join(_md) + md = "\n".join(_md) # pylint: disable=invalid-name if kwargs.get("debug"): logger.debug(md) @@ -75,8 +79,7 @@ def list_to_dicts(arr): def _row_to_dict(row): out_dict = {} - for i in range(0, len(row)): - col = row[i] + for i, col in enumerate(row): if col not in [None, ""]: out_dict[headers[i]] = col return out_dict @@ -106,12 +109,13 @@ def _ss_structure_to_pyxform_survey(ss_structure, kwargs, warnings=None): def _run_odk_validate(xml): # On Windows, NamedTemporaryFile must be opened exclusively. # So it must be explicitly created, opened, closed, and removed + # pylint: disable=consider-using-with tmp = tempfile.NamedTemporaryFile(suffix=".xml", delete=False) tmp.close() try: - with codecs.open(tmp.name, mode="w", encoding="utf-8") as fp: - fp.write(xml) - fp.close() + with codecs.open(tmp.name, mode="w", encoding="utf-8") as file_handle: + file_handle.write(xml) + file_handle.close() check_xform(tmp.name) finally: # Clean up the temporary file @@ -137,9 +141,12 @@ def _autoname_inputs(kwargs): class PyxformTestCase(PyxformMarkdown, TestCase): + """The pyxform markdown TestCase class""" + maxDiff = None - def assertPyxformXform(self, **kwargs): + # pylint: disable=invalid-name,too-many-locals,too-many-branches,too-many-statements + def assertPyxformXform(self, **kwargs): # noqa """ PyxformTestCase.assertPyxformXform() named arguments: ----------------------------------------------------- @@ -213,12 +220,12 @@ def assertPyxformXform(self, **kwargs): survey_valid = True try: - if "md" in kwargs.keys(): + if "md" in kwargs: kwargs = self._autoname_inputs(kwargs) survey = self.md_to_pyxform_survey( kwargs.get("md"), kwargs, warnings=warnings ) - elif "ss_structure" in kwargs.keys(): + elif "ss_structure" in kwargs: kwargs = self._autoname_inputs(kwargs) survey = self._ss_structure_to_pyxform_survey( kwargs.get("ss_structure"), @@ -228,7 +235,7 @@ def assertPyxformXform(self, **kwargs): else: survey = kwargs.get("survey") - xml = survey._to_pretty_xml() + xml = survey._to_pretty_xml() # pylint: disable=protected-access root = etree.fromstring(xml.encode("utf-8")) # Ensure all namespaces are present, even if unused @@ -249,7 +256,7 @@ def assertPyxformXform(self, **kwargs): def _pull_xml_node_from_root(element_selector): _r = root.findall( - ".//n:%s" % element_selector, + f".//n:{element_selector}", namespaces={"n": "http://www.w3.org/2002/xforms"}, ) if _r: @@ -279,7 +286,7 @@ def _pull_xml_node_from_root(element_selector): + "'odk_validate_error__contains'" + " was empty:" + str(e) - ) + ) from e for v_err in odk_validate_error__contains: self.assertContains( e.args[0], v_err, msg_prefix="odk_validate_error__contains" @@ -288,22 +295,21 @@ def _pull_xml_node_from_root(element_selector): if survey_valid: def _check(keyword, verb): - verb_str = "%s__%s" % (keyword, verb) + verb_str = f"{keyword}__{verb}" - bad_kwarg = "%s_%s" % (code, verb) + bad_kwarg = f"{code}_{verb}" if bad_kwarg in kwargs: - good_kwarg = "%s__%s" % (code, verb) + good_kwarg = f"{code}__{verb}" raise SyntaxError( ( - "'%s' is not a valid parameter. " - "Use double underscores: '%s'" + f"'{bad_kwarg}' is not a valid parameter. " + f"Use double underscores: '{good_kwarg}'" ) - % (bad_kwarg, good_kwarg) ) def check_content(content, expected): if content is None: - self.fail(msg="No '{}' found in document.".format(keyword)) + self.fail(msg=f"No '{keyword}' found in document.") cstr = etree.tostring(content, encoding=str, pretty_print=True) matcher_context = MatcherContext( debug=debug, @@ -342,7 +348,7 @@ def check_content(content, expected): if "body_contains" in kwargs or "body__contains" in kwargs: raise SyntaxError( - "Invalid parameter: 'body__contains'." "Use 'xml__contains' instead" + "Invalid parameter: 'body__contains'.Use 'xml__contains' instead" ) for code in ["xml", "instance", "model", "itext"]: @@ -364,7 +370,7 @@ def check_content(content, expected): "and or optionally 'error__contains=[...]'" "\nError(s): " + "\n".join(errors) ) - elif survey_valid and expecting_invalid_survey: + if survey_valid and expecting_invalid_survey: raise PyxformTestError("Expected survey to be invalid.") search_test_kwargs = ( @@ -381,13 +387,13 @@ def check_content(content, expected): elif k.endswith("__not_contains"): assertion = self.assertNotContains else: - raise PyxformTestError("Unexpected search test kwarg: {}".format(k)) + raise PyxformTestError(f"Unexpected search test kwarg: {k}") if k.startswith("error"): joined = "\n".join(errors) elif k.startswith("warnings"): joined = "\n".join(warnings) else: - raise PyxformTestError("Unexpected search test kwarg: {}".format(k)) + raise PyxformTestError(f"Unexpected search test kwarg: {k}") for text in kwargs[k]: assertion(joined, text, msg_prefix=k) if "warnings_count" in kwargs: @@ -408,7 +414,7 @@ def _assert_contains(content, text, msg_prefix): return text_repr, real_count, msg_prefix - def assertContains(self, content, text, count=None, msg_prefix=""): + def assertContains(self, content, text, count=None, msg_prefix=""): # noqa """ FROM: django source- testcases.py @@ -424,16 +430,16 @@ def assertContains(self, content, text, count=None, msg_prefix=""): self.assertEqual( real_count, count, - msg_prefix + "Found %d instances of %s in content" - " (expected %d)" % (real_count, text_repr, count), + msg_prefix + f"Found {real_count} instances of {text_repr} in content" + f" (expected {count})", ) else: self.assertTrue( real_count != 0, - msg_prefix + "Couldn't find %s in content:\n" % text_repr + content, + msg_prefix + f"Couldn't find {text_repr + content} in content:\n", ) - def assertNotContains(self, content, text, msg_prefix=""): + def assertNotContains(self, content, text, msg_prefix=""): # noqa """ Asserts that a content indicates that some content was retrieved successfully, (i.e., the HTTP status code was as expected), and that @@ -446,7 +452,7 @@ def assertNotContains(self, content, text, msg_prefix=""): self.assertEqual( real_count, 0, - msg_prefix + "Response should not contain %s" % text_repr, + msg_prefix + f"Response should not contain {text_repr}", ) def assert_xpath_exact( @@ -459,9 +465,10 @@ def assert_xpath_exact( """ Process an assertion for xml__xpath_exact. - Compares result strings since expected strings may contain xml namespace prefixes. - To allow parsing required to compare as ETrees would require injecting namespace - declarations into the expected match strings. + Compares result strings since expected strings may contain xml namespace + prefixes. + To allow parsing required to compare as ETrees would require injecting + namespace declarations into the expected match strings. :param matcher_context: A MatcherContext dataclass. :param content: XML to be examined. @@ -502,7 +509,10 @@ def assert_xpath_count( content=content, xpath=xpath, ) - msg = f"XPath found no matches:\n{xpath}\n\nXForm content:\n{matcher_context.content_str}" + msg = ( + f"XPath found no matches:\n{xpath}\n\n" + f"XForm content:\n{matcher_context.content_str}" + ) self.assertEqual(expected, len(observed), msg=msg) @@ -523,8 +533,8 @@ def reorder_attributes(root): In utils.node, it is based on xml.dom.minidom.Element objects. See https://github.com/XLSForm/pyxform/issues/414. """ - for el in root.iter(): - attrib = el.attrib + for elem in root.iter(): + attrib = elem.attrib if len(attrib) > 1: # Sort attributes. Attributes are represented as {namespace}name # so attributes with explicit namespaces will always sort after @@ -540,20 +550,21 @@ def xpath_clean_result_strings( """ Clean XPath results: stringify, remove namespace declarations, clean up whitespace. - :param nsmap_subs: namespace replacements e.g. [('x="http://www.w3.org/2002/xforms", "")] + :param nsmap_subs: namespace replacements e.g. + [('x="http://www.w3.org/2002/xforms", "")] :param results: XPath results to clean. """ xmlex = [(" >", ">"), (" />", "/>")] subs = nsmap_subs + xmlex cleaned = set() - for x in results: - if isinstance(x, _Element): - reorder_attributes(x) - x = etree.tostring(x, encoding=str, pretty_print=True) - x = x.strip() - for s in subs: - x = x.replace(*s) - cleaned.add(x) + for result in results: + if isinstance(result, _Element): + reorder_attributes(result) + result = etree.tostring(result, encoding=str, pretty_print=True) + result = result.strip() + for sub in subs: + result = result.replace(*sub) + cleaned.add(result) return cleaned @@ -580,15 +591,14 @@ def xpath_evaluate( raise PyxformTestError(msg) from e if matcher_context.debug: if 0 == len(results): - logger.debug(f"Results for XPath: {xpath}\n" + "(No matches)" + "\n") + logger.debug("Results for XPath: %s\n(No matches)\n", xpath) else: cleaned = xpath_clean_result_strings( nsmap_subs=matcher_context.nsmap_subs, results=results ) - logger.debug(f"Results for XPath: {xpath}\n" + "\n".join(cleaned) + "\n") + logger.debug("Results for XPath: %s\n%s\n", xpath, "\n".join(cleaned)) if for_exact: return xpath_clean_result_strings( nsmap_subs=matcher_context.nsmap_subs, results=results ) - else: - return set(results) + return set(results) diff --git a/onadata/libs/tests/data/test_tools.py b/onadata/libs/tests/data/test_tools.py index b6b6f18ef0..a7acf2b2b4 100644 --- a/onadata/libs/tests/data/test_tools.py +++ b/onadata/libs/tests/data/test_tools.py @@ -6,82 +6,91 @@ from onadata.apps.logger.models.instance import Instance from onadata.apps.main.tests.test_base import TestBase -from onadata.libs.data.query import get_form_submissions_grouped_by_field,\ - get_date_fields, get_field_records +from onadata.libs.data.query import ( + get_form_submissions_grouped_by_field, + get_date_fields, + get_field_records, +) class TestTools(TestBase): - def setUp(self): - super(self.__class__, self).setUp() + super().setUp() self._create_user_and_login() self._publish_transportation_form() - @patch('django.utils.timezone.now') + @patch("django.utils.timezone.now") def test_get_form_submissions_grouped_by_field(self, mock_time): mock_time.return_value = datetime.utcnow().replace(tzinfo=utc) self._make_submissions() - count_key = 'count' - fields = ['_submission_time', '_xform_id_string'] + count_key = "count" + fields = ["_submission_time", "_xform_id_string"] count = len(self.xform.instances.all()) for field in fields: - result = get_form_submissions_grouped_by_field( - self.xform, field)[0] + result = get_form_submissions_grouped_by_field(self.xform, field)[0] self.assertEqual([field, count_key], sorted(list(result))) self.assertEqual(result[count_key], count) - @patch('onadata.apps.logger.models.instance.submission_time') - def test_get_form_submissions_grouped_by_field_datetime_to_date( - self, mock_time): + @patch("onadata.apps.logger.models.instance.submission_time") + def test_get_form_submissions_grouped_by_field_datetime_to_date(self, mock_time): now = datetime(2014, 1, 1, tzinfo=utc) - times = [now, now + timedelta(seconds=1), now + timedelta(seconds=2), - now + timedelta(seconds=3)] + times = [ + now, + now + timedelta(seconds=1), + now + timedelta(seconds=2), + now + timedelta(seconds=3), + ] mock_time.side_effect = times self._make_submissions() - for i in self.xform.instances.all().order_by('-pk'): + for i in self.xform.instances.all().order_by("-pk"): i.date_created = times.pop() i.save() - count_key = 'count' - fields = ['_submission_time'] + count_key = "count" + fields = ["_submission_time"] count = len(self.xform.instances.all()) for field in fields: - result = get_form_submissions_grouped_by_field( - self.xform, field)[0] + result = get_form_submissions_grouped_by_field(self.xform, field)[0] self.assertEqual([field, count_key], sorted(list(result))) self.assertEqual(result[field], str(now.date())) self.assertEqual(result[count_key], count) - @patch('django.utils.timezone.now') + @patch("django.utils.timezone.now") def test_get_form_submissions_two_xforms(self, mock_time): mock_time.return_value = datetime.utcnow().replace(tzinfo=utc) self._make_submissions() - self._publish_xls_file(os.path.join( - "fixtures", - "gps", "gps.xlsx")) + self._publish_xls_file(os.path.join("fixtures", "gps", "gps.xlsx")) first_xform = self.xform - self.xform = self.user.xforms.all().order_by('-pk')[0] - - self._make_submission(os.path.join( - 'onadata', 'apps', 'main', 'tests', 'fixtures', 'gps', - 'instances', 'gps_1980-01-23_20-52-08.xml')) + xform = self.user.xforms.all().order_by("-pk")[0] + + self._make_submission( + os.path.join( + "onadata", + "apps", + "main", + "tests", + "fixtures", + "gps", + "instances", + "gps_1980-01-23_20-52-08.xml", + ) + ) + + count_key = "count" + fields = ["_submission_time", "_xform_id_string"] - count_key = 'count' - fields = ['_submission_time', '_xform_id_string'] - - count = len(self.xform.instances.all()) + count = len(xform.instances.all()) for field in fields: - result = get_form_submissions_grouped_by_field( - self.xform, field)[0] + result = get_form_submissions_grouped_by_field(xform, field)[0] self.assertEqual([field, count_key], sorted(list(result))) self.assertEqual(result[count_key], count) @@ -89,46 +98,41 @@ def test_get_form_submissions_two_xforms(self, mock_time): count = len(first_xform.instances.all()) for field in fields: - result = get_form_submissions_grouped_by_field( - first_xform, field)[0] + result = get_form_submissions_grouped_by_field(first_xform, field)[0] self.assertEqual([field, count_key], sorted(list(result))) self.assertEqual(result[count_key], count) - @patch('django.utils.timezone.now') + @patch("django.utils.timezone.now") def test_get_form_submissions_xform_no_submissions(self, mock_time): mock_time.return_value = datetime.utcnow().replace(tzinfo=utc) self._make_submissions() - self._publish_xls_file(os.path.join( - "fixtures", - "gps", "gps.xlsx")) + self._publish_xls_file(os.path.join("fixtures", "gps", "gps.xlsx")) - self.xform = self.user.xforms.all().order_by('-pk')[0] + xform = self.user.xforms.all().order_by("-pk")[0] - fields = ['_submission_time', '_xform_id_string'] + fields = ["_submission_time", "_xform_id_string"] - count = len(self.xform.instances.all()) + count = len(xform.instances.all()) self.assertEqual(count, 0) for field in fields: - result = get_form_submissions_grouped_by_field( - self.xform, field) + result = get_form_submissions_grouped_by_field(xform, field) self.assertEqual(result, []) - @patch('django.utils.timezone.now') + @patch("django.utils.timezone.now") def test_get_form_submissions_grouped_by_field_sets_name(self, mock_time): mock_time.return_value = datetime.utcnow().replace(tzinfo=utc) self._make_submissions() - count_key = 'count' - fields = ['_submission_time', '_xform_id_string'] - name = '_my_name' + count_key = "count" + fields = ["_submission_time", "_xform_id_string"] + name = "_my_name" xform = self.user.xforms.all()[0] count = len(xform.instances.all()) for field in fields: - result = get_form_submissions_grouped_by_field( - xform, field, name)[0] + result = get_form_submissions_grouped_by_field(xform, field, name)[0] self.assertEqual([name, count_key], sorted(list(result))) self.assertEqual(result[count_key], count) @@ -145,49 +149,68 @@ def test_get_form_submissions_when_response_not_provided(self): # make submission that doesnt have a response for # `available_transportation_types_to_referral_facility` path = os.path.join( - self.this_directory, 'fixtures', 'transportation', - 'instances', 'transport_no_response', 'transport_no_response.xml') + self.this_directory, + "fixtures", + "transportation", + "instances", + "transport_no_response", + "transport_no_response.xml", + ) self._make_submission(path, self.user.username) self.assertEqual(Instance.objects.count(), count + 1) - field = 'transport/available_transportation_types_to_referral_facility' + field = "transport/available_transportation_types_to_referral_facility" xform = self.user.xforms.all()[0] results = get_form_submissions_grouped_by_field( - xform, field, - 'available_transportation_types_to_referral_facility') + xform, field, "available_transportation_types_to_referral_facility" + ) # we should have a similar number of aggregates as submissions as each # submission has a unique value for the field self.assertEqual(len(results), count + 1) # the count where the value is None should have a count of 1 - result = [r for r in results if - r['available_transportation_types_to_referral_facility'] - is None][0] - self.assertEqual(result['count'], 1) + result = [ + r + for r in results + if r["available_transportation_types_to_referral_facility"] is None + ][0] + self.assertEqual(result["count"], 1) def test_get_date_fields_includes_start_end(self): path = os.path.join( - os.path.dirname(__file__), "fixtures", "tutorial", "tutorial.xlsx") + os.path.dirname(__file__), "fixtures", "tutorial", "tutorial.xlsx" + ) self._publish_xls_file_and_set_xform(path) fields = get_date_fields(self.xform) expected_fields = sorted( - ['_submission_time', 'date', 'start_time', 'end_time', 'today', - 'exactly']) + ["_submission_time", "date", "start_time", "end_time", "today", "exactly"] + ) self.assertEqual(sorted(fields), expected_fields) def test_get_field_records_when_some_responses_are_empty(self): - submissions = ['1', '2', '3', 'no_age'] + submissions = ["1", "2", "3", "no_age"] path = os.path.join( - os.path.dirname(__file__), "fixtures", "tutorial", "tutorial.xlsx") + os.path.dirname(__file__), "fixtures", "tutorial", "tutorial.xlsx" + ) self._publish_xls_file_and_set_xform(path) for i in submissions: - self._make_submission(os.path.join( - 'onadata', 'apps', 'api', 'tests', 'fixtures', 'forms', - 'tutorial', 'instances', '{}.xml'.format(i))) - - field = 'age' + self._make_submission( + os.path.join( + "onadata", + "apps", + "api", + "tests", + "fixtures", + "forms", + "tutorial", + "instances", + f"{i}.xml", + ) + ) + + field = "age" records = get_field_records(field, self.xform) self.assertEqual(sorted(records), sorted([23, 23, 35])) diff --git a/onadata/libs/tests/test_renderers.py b/onadata/libs/tests/test_renderers.py index 037c6d6aba..69e75b5329 100644 --- a/onadata/libs/tests/test_renderers.py +++ b/onadata/libs/tests/test_renderers.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Test Renderer module. """ diff --git a/onadata/libs/tests/utils/test_analytics.py b/onadata/libs/tests/utils/test_analytics.py index e665cf4871..cd359dc24e 100644 --- a/onadata/libs/tests/utils/test_analytics.py +++ b/onadata/libs/tests/utils/test_analytics.py @@ -58,7 +58,7 @@ def test_submission_tracking(self): onadata.libs.utils.analytics.init_analytics() self.assertEqual(segment_mock.write_key, '123') - # Test out that the track_object_event decorator + # Test out that the TrackObjectEvent decorator # Tracks created submissions, XForms and Projects view = XFormSubmissionViewSet.as_view({ 'post': 'create', diff --git a/onadata/libs/tests/utils/test_api_export_tools.py b/onadata/libs/tests/utils/test_api_export_tools.py index 2a4514ed15..53c162ea10 100644 --- a/onadata/libs/tests/utils/test_api_export_tools.py +++ b/onadata/libs/tests/utils/test_api_export_tools.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Test api_export_tools module. """ diff --git a/onadata/libs/tests/utils/test_backup_tools.py b/onadata/libs/tests/utils/test_backup_tools.py index 4225b5b46c..a353645764 100644 --- a/onadata/libs/tests/utils/test_backup_tools.py +++ b/onadata/libs/tests/utils/test_backup_tools.py @@ -3,7 +3,8 @@ from onadata.apps.main.tests.test_base import TestBase from onadata.libs.utils.backup_tools import ( restore_backup_from_xml_file, - restore_backup_from_path) + restore_backup_from_path, +) from onadata.apps.logger.models import Instance @@ -17,14 +18,15 @@ def test_restore_from_xml_file(self): count = count_qs.count() xml_file_path = os.path.join( self.this_directory, - 'fixtures', - 'transportation', - 'backup_restore', - '2011', '07', '25', - '2011-07-25-19-05-36.xml') - num_restored = restore_backup_from_xml_file( - xml_file_path, - self.user.username) + "fixtures", + "transportation", + "backup_restore", + "2011", + "07", + "25", + "2011-07-25-19-05-36.xml", + ) + num_restored = restore_backup_from_xml_file(xml_file_path, self.user.username) self.assertEqual(num_restored, 1) self.assertEqual(count_qs.count(), count + 1) @@ -33,13 +35,14 @@ def test_restore_backup_from_path(self): count = count_qs.count() path = os.path.join( self.this_directory, - 'fixtures', - 'transportation', - 'backup_restore',) + "fixtures", + "transportation", + "backup_restore", + ) num_instances, num_restored = restore_backup_from_path( path, self.user.username, - None) + ) self.assertEqual(num_instances, 1) self.assertEqual(num_restored, 1) self.assertEqual(count_qs.count(), count + 1) diff --git a/onadata/libs/tests/utils/test_cache_tools.py b/onadata/libs/tests/utils/test_cache_tools.py index aebf48920d..8b5644a55b 100644 --- a/onadata/libs/tests/utils/test_cache_tools.py +++ b/onadata/libs/tests/utils/test_cache_tools.py @@ -2,17 +2,28 @@ """ Test onadata.libs.utils.cache_tools module. """ +from unittest import TestCase + +from django.contrib.auth import get_user_model from django.core.cache import cache -from django.contrib.auth.models import User from django.http.request import HttpRequest -from unittest import TestCase -from onadata.apps.main.models.user_profile import UserProfile from onadata.apps.logger.models.project import Project +from onadata.apps.main.models.user_profile import UserProfile +from onadata.libs.serializers.project_serializer import ProjectSerializer from onadata.libs.utils.cache_tools import ( - PROJ_PERM_CACHE, PROJ_NUM_DATASET_CACHE, PROJ_SUB_DATE_CACHE, - PROJ_FORMS_CACHE, PROJ_BASE_FORMS_CACHE, PROJ_OWNER_CACHE, - safe_key, reset_project_cache, project_cache_prefixes) + PROJ_BASE_FORMS_CACHE, + PROJ_FORMS_CACHE, + PROJ_NUM_DATASET_CACHE, + PROJ_OWNER_CACHE, + PROJ_PERM_CACHE, + PROJ_SUB_DATE_CACHE, + project_cache_prefixes, + reset_project_cache, + safe_key, +) + +User = get_user_model() class TestCacheTools(TestCase): @@ -22,75 +33,75 @@ def test_safe_key(self): """Test safe_key() function returns a hashed key""" self.assertEqual( safe_key("hello world"), - "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + ) def test_reset_project_cache(self): """ Test reset_project_cache() function actually resets all project cache entries """ - bob = User.objects.create(username='bob', first_name='bob') + bob = User.objects.create(username="bob", first_name="bob") UserProfile.objects.create(user=bob) project = Project.objects.create( - name='Some Project', created_by=bob, organization=bob) + name="Some Project", created_by=bob, organization=bob + ) # Set dummy values in cache for prefix in project_cache_prefixes: - cache.set(f'{prefix}{project.pk}', 'stale') + cache.set(f"{prefix}{project.pk}", "stale") request = HttpRequest() request.user = bob - request.META = { - 'SERVER_NAME': 'testserver', - 'SERVER_PORT': '80' - } - reset_project_cache(project, request) + request.META = {"SERVER_NAME": "testserver", "SERVER_PORT": "80"} + reset_project_cache(project, request, ProjectSerializer) expected_project_cache = { - 'url': f'http://testserver/api/v1/projects/{project.pk}', - 'projectid': project.pk, - 'owner': 'http://testserver/api/v1/users/bob', - 'created_by': 'http://testserver/api/v1/users/bob', - 'metadata': {}, - 'starred': False, - 'users': [{ - 'is_org': False, - 'metadata': {}, - 'first_name': 'bob', - 'last_name': '', - 'user': 'bob', - 'role': 'owner' - }], - 'forms': [], - 'public': False, - 'tags': [], - 'num_datasets': 0, - 'last_submission_date': None, - 'teams': [], - 'data_views': [], - 'name': 'Some Project', - 'deleted_at': None + "url": f"http://testserver/api/v1/projects/{project.pk}", + "projectid": project.pk, + "owner": "http://testserver/api/v1/users/bob", + "created_by": "http://testserver/api/v1/users/bob", + "metadata": {}, + "starred": False, + "users": [ + { + "is_org": False, + "metadata": {}, + "first_name": "bob", + "last_name": "", + "user": "bob", + "role": "owner", + } + ], + "forms": [], + "public": False, + "tags": [], + "num_datasets": 0, + "last_submission_date": None, + "teams": [], + "data_views": [], + "name": "Some Project", + "deleted_at": None, } self.assertEqual( - cache.get(f'{PROJ_PERM_CACHE}{project.pk}'), - expected_project_cache['users']) + cache.get(f"{PROJ_PERM_CACHE}{project.pk}"), expected_project_cache["users"] + ) self.assertEqual( - cache.get(f'{PROJ_NUM_DATASET_CACHE}{project.pk}'), - expected_project_cache['num_datasets']) + cache.get(f"{PROJ_NUM_DATASET_CACHE}{project.pk}"), + expected_project_cache["num_datasets"], + ) self.assertEqual( - cache.get(f'{PROJ_SUB_DATE_CACHE}{project.pk}'), - expected_project_cache['last_submission_date']) + cache.get(f"{PROJ_SUB_DATE_CACHE}{project.pk}"), + expected_project_cache["last_submission_date"], + ) self.assertEqual( - cache.get(f'{PROJ_FORMS_CACHE}{project.pk}'), - expected_project_cache['forms']) - self.assertEqual( - cache.get(f'{PROJ_BASE_FORMS_CACHE}{project.pk}'), - None) + cache.get(f"{PROJ_FORMS_CACHE}{project.pk}"), + expected_project_cache["forms"], + ) + self.assertEqual(cache.get(f"{PROJ_BASE_FORMS_CACHE}{project.pk}"), None) - project_cache = cache.get(f'{PROJ_OWNER_CACHE}{project.pk}') - project_cache.pop('date_created') - project_cache.pop('date_modified') - self.assertEqual( - project_cache, - expected_project_cache) + project_cache = cache.get(f"{PROJ_OWNER_CACHE}{project.pk}") + project_cache.pop("date_created") + project_cache.pop("date_modified") + self.assertEqual(project_cache, expected_project_cache) diff --git a/onadata/libs/tests/utils/test_export_tools.py b/onadata/libs/tests/utils/test_export_tools.py index 7e30c9554b..978255cc78 100644 --- a/onadata/libs/tests/utils/test_export_tools.py +++ b/onadata/libs/tests/utils/test_export_tools.py @@ -2,12 +2,11 @@ """ Test export_tools module """ -import os import json +import os import shutil import tempfile import zipfile -from builtins import open from datetime import date, datetime, timedelta from django.conf import settings @@ -16,31 +15,33 @@ from django.core.files.temp import NamedTemporaryFile from django.test.utils import override_settings from django.utils import timezone + from pyxform.builder import create_survey_from_xls from rest_framework import exceptions from rest_framework.authtoken.models import Token - from savReaderWriter import SavWriter from onadata.apps.api import tests as api_tests -from onadata.apps.api.tests.viewsets.test_abstract_viewset import ( - TestAbstractViewSet) +from onadata.apps.api.tests.viewsets.test_abstract_viewset import TestAbstractViewSet +from onadata.apps.api.viewsets.data_viewset import DataViewSet from onadata.apps.logger.models import Attachment, Instance, XForm from onadata.apps.main.tests.test_base import TestBase from onadata.apps.viewer.models.export import Export from onadata.apps.viewer.models.parsed_instance import query_data -from onadata.apps.api.viewsets.data_viewset import DataViewSet from onadata.libs.serializers.merged_xform_serializer import MergedXFormSerializer from onadata.libs.serializers.xform_serializer import XFormSerializer -from onadata.libs.utils.export_builder import encode_if_str, get_value_or_attachment_uri -from onadata.libs.utils.export_tools import ( +from onadata.libs.utils.export_builder import ( ExportBuilder, + encode_if_str, + get_value_or_attachment_uri, +) +from onadata.libs.utils.export_tools import ( check_pending_export, generate_attachments_zip_export, generate_export, + generate_geojson_export, generate_kml_export, generate_osm_export, - generate_geojson_export, get_repeat_index_tags, kml_export_data, parse_request_export_options, @@ -556,8 +557,8 @@ def test_geojson_exports(self): xform1 = self._publish_markdown(geo_md, self.user, id_string="a") xml = '-1.28 36.83 0 0orange' Instance(xform=xform1, xml=xml).save() - request = self.factory.get('/', **self.extra) - XFormSerializer(xform1, context={'request': request}).data + request = self.factory.get("/", **self.extra) + XFormSerializer(xform1, context={"request": request}).data xform1 = XForm.objects.get(id_string="a") export_type = "geojson" options = { @@ -566,15 +567,16 @@ def test_geojson_exports(self): self._publish_transportation_form_and_submit_instance() # set metadata to xform data_type = "media" - data_value = 'xform_geojson {} {}'.format(xform1.pk, xform1.id_string) + data_value = "xform_geojson {} {}".format(xform1.pk, xform1.id_string) extra_data = { "data_title": "fruit", "data_geo_field": "gps", "data_simple_style": True, - "data_fields": "fruit,gps" + "data_fields": "fruit,gps", } response = self._add_form_metadata( - self.xform, data_type, data_value, extra_data=extra_data) + self.xform, data_type, data_value, extra_data=extra_data + ) self.assertEqual(response.status_code, 201) username = self.xform.user.username id_string = self.xform.id_string @@ -582,36 +584,25 @@ def test_geojson_exports(self): self.assertEqual(self.xform.metadata_set.count(), 1) metadata = self.xform.metadata_set.all()[0] export = generate_geojson_export( - export_type, - username, - id_string, - metadata, - options=options, - xform=xform1 + export_type, username, id_string, metadata, options=options, xform=xform1 ) self.assertIsNotNone(export) self.assertTrue(export.is_successful) with default_storage.open(export.filepath) as f2: - content = f2.read().decode('utf-8') + content = f2.read().decode("utf-8") geojson = { "type": "FeatureCollection", "features": [ { "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 36.83, - -1.28 - ] - }, + "geometry": {"type": "Point", "coordinates": [36.83, -1.28]}, "properties": { "fruit": "orange", "gps": "-1.28 36.83 0 0", - "title": "orange" - } + "title": "orange", + }, } - ] + ], } content = json.loads(content) # remove xform and id from properties because they keep changing @@ -630,7 +621,7 @@ def test_geojson_exports(self): metadata, export_id=export_id, options=options, - xform=xform1 + xform=xform1, ) self.assertIsNotNone(export) diff --git a/onadata/libs/tests/utils/test_project_utils.py b/onadata/libs/tests/utils/test_project_utils.py index 92462e53f1..6b343dabff 100644 --- a/onadata/libs/tests/utils/test_project_utils.py +++ b/onadata/libs/tests/utils/test_project_utils.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Test onadata.libs.utils.project_utils """ diff --git a/onadata/libs/utils/analytics.py b/onadata/libs/utils/analytics.py index b58e96cfdc..eb4fec4bfd 100644 --- a/onadata/libs/utils/analytics.py +++ b/onadata/libs/utils/analytics.py @@ -1,30 +1,35 @@ # -*- coding: utf-8 -*- -# Analytics package for tracking and measuring with services like Segment. -# Heavily borrowed from RapidPro's temba.utils.analytics -import analytics as segment_analytics +""" +Analytics package for tracking and measuring with services like Segment. +""" from typing import Dict, List, Optional from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.utils import timezone -from onadata.apps.logger.models import Instance, XForm, Project +# Heavily borrowed from RapidPro's temba.utils.analytics +import analytics as segment_analytics + +from onadata.apps.logger.models import Instance, Project, XForm from onadata.apps.main.models import UserProfile from onadata.libs.utils.common_tags import ( INSTANCE_CREATE_EVENT, INSTANCE_UPDATE_EVENT, - XFORM_CREATION_EVENT, PROJECT_CREATION_EVENT, - USER_CREATION_EVENT) + USER_CREATION_EVENT, + XFORM_CREATION_EVENT, +) -_segment = False +_segment = False # pylint: disable=invalid-name +User = get_user_model() def init_analytics(): """Initialize the analytics agents with write credentials.""" - segment_write_key = getattr(settings, 'SEGMENT_WRITE_KEY', None) + segment_write_key = getattr(settings, "SEGMENT_WRITE_KEY", None) if segment_write_key: - global _segment + global _segment # pylint: disable=global-statement,invalid-name segment_analytics.write_key = segment_write_key _segment = True @@ -34,10 +39,10 @@ def get_user_id(user): if user: return user.username - return 'anonymous' + return "anonymous" -class track_object_event(object): +class TrackObjectEvent: # pylint: disable=invalid-name,too-many-instance-attributes """ Decorator that helps track create and update actions for model objects. @@ -47,13 +52,15 @@ class track_object_event(object): precise control of what is tracked utilize the track() function """ + # pylint: disable=too-many-arguments def __init__( - self, - user_field: str, - properties: Dict[str, str], - event_name: str = '', - event_label: str = '', - additional_context: Dict[str, str] = None): + self, + user_field: str, + properties: Dict[str, str], + event_name: str = "", + event_label: str = "", + additional_context: Dict[str, str] = None, + ): self.user_field = user_field self.properties = properties self.event_start = None @@ -71,31 +78,37 @@ def _get_field_from_path(self, field_path: List[str]): return value def set_user(self) -> Optional[User]: - if '__' in self.user_field: - field_path = self.user_field.split('__') + """Set's the user attribute.""" + # pylint: disable=attribute-defined-outside-init + if "__" in self.user_field: + field_path = self.user_field.split("__") self.user = self._get_field_from_path(field_path) else: self.user = self._getattr_or_none(self.user_field) def get_tracking_properties(self, label: str = None) -> dict: + """Returns tracking properties""" tracking_properties = {} for tracking_property, model_field in self.properties.items(): - if '__' in model_field: - field_path = model_field.split('__') - tracking_properties[tracking_property] = self. \ - _get_field_from_path(field_path) + if "__" in model_field: + field_path = model_field.split("__") + tracking_properties[tracking_property] = self._get_field_from_path( + field_path + ) else: - tracking_properties[tracking_property] = self. \ - _getattr_or_none(model_field) + tracking_properties[tracking_property] = self._getattr_or_none( + model_field + ) if self.additional_context: tracking_properties.update(self.additional_context) - if label and 'label' not in tracking_properties: - tracking_properties['label'] = label + if label and "label" not in tracking_properties: + tracking_properties["label"] = label return tracking_properties def get_event_name(self) -> str: + """Returns an event name.""" event_name = self.event_name if isinstance(self.tracked_obj, Instance) and not event_name: last_edited = self.tracked_obj.last_edited @@ -112,6 +125,7 @@ def get_event_name(self) -> str: return event_name def get_event_label(self) -> str: + """Returns an event label.""" event_label = self.event_label if isinstance(self.tracked_obj, Instance) and not event_label: form_id = self.tracked_obj.xform.pk @@ -120,41 +134,46 @@ def get_event_label(self) -> str: return event_label def get_request_origin(self, request, tracking_properties): + """Returns the request origin""" if isinstance(self.tracked_obj, Instance): try: - user_agent = request.META['HTTP_USER_AGENT'] - if 'Android' in user_agent: - event_source = 'Submission collected from ODK COLLECT' - elif 'Chrome' or 'Mozilla' or 'Safari' in user_agent: - event_source = 'Submission collected from Enketo' + user_agent = request.META["HTTP_USER_AGENT"] + if "Android" in user_agent: + event_source = "Submission collected from ODK COLLECT" + elif ( + "Chrome" in user_agent + or "Mozilla" in user_agent + or "Safari" in user_agent + ): + event_source = "Submission collected from Enketo" except KeyError: event_source = "" else: event_source = "" - tracking_properties.update({'from': event_source}) + tracking_properties.update({"from": event_source}) return tracking_properties def _track_object_event(self, obj, request=None) -> None: + # pylint: disable=attribute-defined-outside-init self.tracked_obj = obj self.set_user() event_name = self.get_event_name() label = self.get_event_label() tracking_properties = self.get_tracking_properties(label=label) try: - if tracking_properties['from'] == 'XML Submissions': + if tracking_properties["from"] == "XML Submissions": tracking_properties = self.get_request_origin( - request, tracking_properties) + request, tracking_properties + ) except KeyError: pass - track( - self.user, event_name, - properties=tracking_properties, request=request) + track(self.user, event_name, properties=tracking_properties, request=request) def __call__(self, func): def decorator(obj, *args): request = None if hasattr(obj, "context"): - request = obj.context.get('request') + request = obj.context.get("request") self.event_start = timezone.now() return_value = func(obj, *args) if isinstance(return_value, list): @@ -163,6 +182,7 @@ def decorator(obj, *args): else: self._track_object_event(return_value, request) return return_value + return decorator @@ -173,28 +193,28 @@ def track(user, event_name, properties=None, context=None, request=None): properties = properties or {} context = context or {} # Introduce inner page and campaign object within the context - context['page'] = {} - context['campaign'] = {} + context["page"] = {} + context["campaign"] = {} - if 'value' not in properties: - properties['value'] = 1 + if "value" not in properties: + properties["value"] = 1 - if 'submitted_by' in properties: - submitted_by_user = properties.pop('submitted_by') + if "submitted_by" in properties: + submitted_by_user = properties.pop("submitted_by") submitted_by = get_user_id(submitted_by_user) - properties['event_by'] = submitted_by + properties["event_by"] = submitted_by - context['active'] = True + context["active"] = True if request: - context['ip'] = request.META.get('REMOTE_ADDR', '') - context['userId'] = user.id - context['receivedAt'] = request.headers.get('Date', '') - context['userAgent'] = request.headers.get('User-Agent', '') - context['campaign']['source'] = settings.HOSTNAME - context['page']['path'] = request.path - context['page']['referrer'] = request.headers.get('Referer', '') - context['page']['url'] = request.build_absolute_uri() + context["ip"] = request.META.get("REMOTE_ADDR", "") + context["userId"] = user.id + context["receivedAt"] = request.headers.get("Date", "") + context["userAgent"] = request.headers.get("User-Agent", "") + context["campaign"]["source"] = settings.HOSTNAME + context["page"]["path"] = request.path + context["page"]["referrer"] = request.headers.get("Referer", "") + context["page"]["url"] = request.build_absolute_uri() if _segment: segment_analytics.track(user_id, event_name, properties, context) diff --git a/onadata/libs/utils/api_export_tools.py b/onadata/libs/utils/api_export_tools.py index 719c64eb3f..b59cb78205 100644 --- a/onadata/libs/utils/api_export_tools.py +++ b/onadata/libs/utils/api_export_tools.py @@ -1,24 +1,25 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ -API export util functions. +API export utility functions. """ import json import os import sys from datetime import datetime -import six -from celery.backends.rpc import BacklogLimitExceeded -from celery.result import AsyncResult from django.conf import settings from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ + +import six +from celery.backends.rpc import BacklogLimitExceeded +from celery.result import AsyncResult +from google.oauth2.credentials import Credentials # noqa from kombu.exceptions import OperationalError from rest_framework import exceptions, status from rest_framework.response import Response from rest_framework.reverse import reverse -from google.oauth2.credentials import Credentials try: from savReaderWriter import SPSSIOError @@ -36,7 +37,6 @@ ) from onadata.libs.permissions import filter_queryset_xform_meta_perms_sql from onadata.libs.utils import log -from onadata.libs.utils.google import create_flow from onadata.libs.utils.async_status import ( FAILED, PENDING, @@ -56,13 +56,14 @@ generate_attachments_zip_export, generate_export, generate_external_export, + generate_geojson_export, generate_kml_export, generate_osm_export, - generate_geojson_export, newest_export_for, parse_request_export_options, should_create_new_export, ) +from onadata.libs.utils.google import create_flow from onadata.libs.utils.logger_tools import response_with_mimetype_and_name from onadata.libs.utils.model_tools import get_columns_with_hxl @@ -188,8 +189,12 @@ def custom_response_handler( # noqa: C0901 # we always re-generate if a filter is specified def _new_export(): return _generate_new_export( - request, xform, query, export_type, - dataview_pk=dataview_pk, metadata=metadata + request, + xform, + query, + export_type, + dataview_pk=dataview_pk, + metadata=metadata, ) if should_create_new_export(xform, export_type, options, request=request): diff --git a/onadata/libs/utils/backup_tools.py b/onadata/libs/utils/backup_tools.py index 801a71dc07..6d62312c8f 100644 --- a/onadata/libs/utils/backup_tools.py +++ b/onadata/libs/utils/backup_tools.py @@ -1,11 +1,15 @@ +# -*- coding: utf-8 -*- +""" +Backup utilities. +""" import codecs -from datetime import datetime import errno import os import shutil import sys import tempfile import zipfile +from datetime import datetime from time import sleep from onadata.apps.logger.import_tools import django_file @@ -13,25 +17,27 @@ from onadata.libs.utils.logger_tools import create_instance from onadata.libs.utils.model_tools import queryset_iterator - DATE_FORMAT = "%Y-%m-%d-%H-%M-%S" def _date_created_from_filename(filename): - base_name, ext = os.path.splitext(filename) + base_name, _ext = os.path.splitext(filename) parts = base_name.split("-") if len(parts) < 6: raise ValueError( - "Inavlid filename - it must be in the form" - " 'YYYY-MM-DD-HH-MM-SS[-i].xml'") - parts_dict = dict( - zip(["year", "month", "day", "hour", "min", "sec"], parts)) + "Inavlid filename - it must be in the form 'YYYY-MM-DD-HH-MM-SS[-i].xml'" + ) + parts_dict = dict(zip(["year", "month", "day", "hour", "min", "sec"], parts)) + # pylint: disable=consider-using-f-string return datetime.strptime( - "%(year)s-%(month)s-%(day)s-%(hour)s-%(min)s-%(sec)s" % - parts_dict, DATE_FORMAT) + "%(year)s-%(month)s-%(day)s-%(hour)s-%(min)s-%(sec)s" % parts_dict, + DATE_FORMAT, + ) +# pylint: disable=too-many-locals def create_zip_backup(zip_output_file, user, xform=None): + """Create a ZIP file with a user's XForms and submissions.""" # create a temp dir that we'll create our structure within and zip it # when we are done tmp_dir_path = tempfile.mkdtemp() @@ -43,14 +49,14 @@ def create_zip_backup(zip_output_file, user, xform=None): # for each submission in the database - create an xml file in this # form # //YYYY/MM/DD/YYYY-MM-DD-HH-MM-SS.xml - qs = Instance.objects.filter(xform__user=user) + queryset = Instance.objects.filter(xform__user=user) if xform: - qs = qs.filter(xform=xform) + queryset = queryset.filter(xform=xform) - num_instances = qs.count() + num_instances = queryset.count() done = 0 sys.stdout.write("Creating XML Instances\n") - for instance in queryset_iterator(qs, 100): + for instance in queryset_iterator(queryset, 100): # get submission time date_time_str = instance.date_created.strftime(DATE_FORMAT) date_parts = date_time_str.split("-") @@ -68,96 +74,95 @@ def create_zip_backup(zip_output_file, user, xform=None): # check for duplicate file names file_index = 1 while os.path.exists(full_xml_path): - full_xml_path = os.path.join( - full_path, "%s-%d.xml" % (date_time_str, file_index)) + full_xml_path = os.path.join(full_path, f"{date_time_str}-{file_index}.xml") file_index += 1 # create the instance xml with codecs.open(full_xml_path, "wb", "utf-8") as f: f.write(instance.xml) done += 1 - sys.stdout.write("\r%.2f %% done" % ( - float(done)/float(num_instances) * 100)) + # pylint: disable=consider-using-f-string + sys.stdout.write("\r%.2f %% done" % (float(done) / float(num_instances) * 100)) sys.stdout.flush() sleep(0) # write zip file - sys.stdout.write("\nWriting to ZIP arhive.\n") - zf = zipfile.ZipFile(zip_output_file, "w", zipfile.ZIP_DEFLATED, - allowZip64=True) - done = 0 - for dir_path, dir_names, file_names in os.walk(tmp_dir_path): - for file_name in file_names: - archive_path = dir_path.replace(tmp_dir_path + os.path.sep, - "", 1) - zf.write(os.path.join(dir_path, file_name), - os.path.join(archive_path, file_name)) - done += 1 - sys.stdout.write("\r%.2f %% done" % ( - float(done)/float(num_instances) * 100)) - sys.stdout.flush() - sleep(0) - zf.close() + sys.stdout.write("\nWriting to ZIP archive.\n") + with zipfile.ZipFile( + zip_output_file, "w", zipfile.ZIP_DEFLATED, allowZip64=True + ) as zip_file: + done = 0 + for dir_path, _dir_names, file_names in os.walk(tmp_dir_path): + for file_name in file_names: + archive_path = dir_path.replace(tmp_dir_path + os.path.sep, "", 1) + zip_file.write( + os.path.join(dir_path, file_name), + os.path.join(archive_path, file_name), + ) + done += 1 + # pylint: disable=consider-using-f-string + sys.stdout.write( + "\r%.2f %% done" % (float(done) / float(num_instances) * 100) + ) + sys.stdout.flush() + sleep(0) # removed dir tree shutil.rmtree(tmp_dir_path) - sys.stdout.write("\nBackup saved to %s\n" % zip_output_file) + sys.stdout.write(f"\nBackup saved to {zip_output_file}\n") def restore_backup_from_zip(zip_file_path, username): + """Restores XForms and submission instances from a ZIP backup file.""" try: temp_directory = tempfile.mkdtemp() - zf = zipfile.ZipFile(zip_file_path) - - zf.extractall(temp_directory) + with zipfile.ZipFile(zip_file_path) as zip_file: + zip_file.extractall(temp_directory) except zipfile.BadZipfile: - sys.stderr.write("Bad zip arhcive.") + sys.stderr.write("Bad zip archive.") else: - return restore_backup_from_path(temp_directory, username, "backup") + return restore_backup_from_path(temp_directory, username) finally: shutil.rmtree(temp_directory) + return None def restore_backup_from_xml_file(xml_instance_path, username): + """Creates submission instances in the DB from a submission XML file.""" # check if its a valid xml instance file_name = os.path.basename(xml_instance_path) xml_file = django_file( - xml_instance_path, - field_name="xml_file", - content_type="text/xml") + xml_instance_path, field_name="xml_file", content_type="text/xml" + ) media_files = [] try: date_created = _date_created_from_filename(file_name) except ValueError: sys.stderr.write( - "Couldn't determine date created from filename: '%s'\n" % - file_name) + f"Couldn't determine date created from filename: '{file_name}'\n" + ) date_created = datetime.now() - sys.stdout.write("Creating instance from '%s'\n" % file_name) + sys.stdout.write(f"Creating instance from '{file_name}'\n") try: create_instance( - username, xml_file, media_files, - date_created_override=date_created) + username, xml_file, media_files, date_created_override=date_created + ) return 1 - except Exception as e: - sys.stderr.write( - "Could not restore %s, create instance said: %s\n" % - (file_name, e)) + except Exception as e: # pylint: disable=broad-except + sys.stderr.write(f"Could not restore {file_name}, create instance said: {e}\n") return 0 -def restore_backup_from_path(dir_path, username, status): +def restore_backup_from_path(dir_path, username): """ Only restores xml submissions, media files are assumed to still be in storage and will be retrieved by the filename stored within the submission """ num_instances = 0 num_restored = 0 - for dir_path, dir_names, file_names in os.walk(dir_path): + for _dir_path, _dir_names, file_names in os.walk(dir_path): for file_name in file_names: # check if its a valid xml instance - xml_instance_path = os.path.join(dir_path, file_name) + xml_instance_path = os.path.join(_dir_path, file_name) num_instances += 1 - num_restored += restore_backup_from_xml_file( - xml_instance_path, - username) + num_restored += restore_backup_from_xml_file(xml_instance_path, username) return num_instances, num_restored diff --git a/onadata/libs/utils/cache_tools.py b/onadata/libs/utils/cache_tools.py index 4698bf7dcc..033b37c6f5 100644 --- a/onadata/libs/utils/cache_tools.py +++ b/onadata/libs/utils/cache_tools.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Cache utilities. +""" import hashlib from django.core.cache import cache @@ -10,9 +14,14 @@ PROJ_FORMS_CACHE = "ps-project_forms-" PROJ_BASE_FORMS_CACHE = "ps-project_base_forms-" PROJ_OWNER_CACHE = "ps-project_owner-" -project_cache_prefixes = [PROJ_PERM_CACHE, PROJ_NUM_DATASET_CACHE, - PROJ_SUB_DATE_CACHE, PROJ_FORMS_CACHE, - PROJ_BASE_FORMS_CACHE, PROJ_OWNER_CACHE] +project_cache_prefixes = [ + PROJ_PERM_CACHE, + PROJ_NUM_DATASET_CACHE, + PROJ_SUB_DATE_CACHE, + PROJ_FORMS_CACHE, + PROJ_BASE_FORMS_CACHE, + PROJ_OWNER_CACHE, +] # Cache names used in user_profile_serializer IS_ORG = "ups-is_org-" @@ -33,13 +42,13 @@ PROJECT_LINKED_DATAVIEWS = "ps-project-linked_dataviews" # Cache names used in organization profile viewset -ORG_PROFILE_CACHE = 'org-profile-' +ORG_PROFILE_CACHE = "org-profile-" # cache login attempts LOCKOUT_IP = "lockout_ip-" LOGIN_ATTEMPTS = "login_attempts-" -LOCKOUT_CHANGE_PASSWORD_USER = 'lockout_change_password_user-' -CHANGE_PASSWORD_ATTEMPTS = 'change_password_attempts-' +LOCKOUT_CHANGE_PASSWORD_USER = "lockout_change_password_user-" # noqa +CHANGE_PASSWORD_ATTEMPTS = "change_password_attempts-" # noqa # Cache names used in XForm Model XFORM_SUBMISSION_COUNT_FOR_DAY = "xfm-get_submission_count-" @@ -58,18 +67,18 @@ def safe_key(key): return hashlib.sha256(force_bytes(key)).hexdigest() -def reset_project_cache(project, request): +def reset_project_cache(project, request, project_serializer_class): """ Clears and sets project cache """ - from onadata.libs.serializers.project_serializer import ProjectSerializer # Clear all project cache entries for prefix in project_cache_prefixes: - safe_delete(f'{prefix}{project.pk}') + safe_delete(f"{prefix}{project.pk}") # Reserialize project and cache value # Note: The ProjectSerializer sets all the other cache entries - project_cache_data = ProjectSerializer( - project, context={'request': request}).data - cache.set(f'{PROJ_OWNER_CACHE}{project.pk}', project_cache_data) + project_cache_data = project_serializer_class( + project, context={"request": request} + ).data + cache.set(f"{PROJ_OWNER_CACHE}{project.pk}", project_cache_data) diff --git a/onadata/libs/utils/chart_tools.py b/onadata/libs/utils/chart_tools.py index a26796c627..f5683e9a0f 100644 --- a/onadata/libs/utils/chart_tools.py +++ b/onadata/libs/utils/chart_tools.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Chart utility functions. """ diff --git a/onadata/libs/utils/common_tools.py b/onadata/libs/utils/common_tools.py index 9ae8533276..e5a9e051a1 100644 --- a/onadata/libs/utils/common_tools.py +++ b/onadata/libs/utils/common_tools.py @@ -19,7 +19,11 @@ import sentry_sdk import six +from celery import current_task +from onadata.libs.utils.common_tags import ATTACHMENTS + +DEFAULT_UPDATE_BATCH = 100 TRUE_VALUES = ["TRUE", "T", "1", 1] @@ -123,7 +127,7 @@ def json_stream(data, json_string): """ yield "[" try: - data = data.__iter__() + data = iter(data) item = next(data) while item: try: @@ -231,3 +235,132 @@ def __ne__(self, other): return mycmp(self.obj, other.obj) != 0 return ComparatorClass + + +def current_site_url(path): + """ + Returns fully qualified URL (no trailing slash) for the current site. + :param path + :return: complete url + """ + # pylint: disable=import-outside-toplevel + from django.contrib.sites.models import Site + + current_site = Site.objects.get_current() + protocol = getattr(settings, "ONA_SITE_PROTOCOL", "http") + port = getattr(settings, "ONA_SITE_PORT", "") + url = f"{protocol}://{current_site.domain}" + if port: + url += f":{port}" + if path: + url += f"{path}" + + return url + + +def get_choice_label(label, data_dictionary, language=None): + """ + Return the label matching selected language or simply just the label. + """ + if isinstance(label, dict): + languages = list(label.keys()) + _language = ( + language + if language in languages + else data_dictionary.get_language(languages) + ) + + return label[_language] + + return label + + +def get_choice_label_value(key, value, data_dictionary, language=None): + """ + Return the label of a choice matching the value if the key xpath is a + SELECT_ONE otherwise it returns the value unchanged. + """ + + def _get_choice_label_value(lookup): + _label = None + for choice in data_dictionary.get_survey_element(key).children: + if choice.name == lookup: + _label = get_choice_label(choice.label, data_dictionary, language) + break + + return _label + + label = None + if key in data_dictionary.get_select_one_xpaths(): + label = _get_choice_label_value(value) + + if key in data_dictionary.get_select_multiple_xpaths(): + answers = [] + for item in value.split(" "): + answer = _get_choice_label_value(item) + answers.append(answer or item) + if [_i for _i in answers if _i is not None]: + label = " ".join(answers) + + return label or value + + +# pylint: disable=too-many-arguments +def get_value_or_attachment_uri( + key, + value, + row, + data_dictionary, + media_xpaths, + attachment_list=None, + show_choice_labels=False, + language=None, +): + """ + Gets either the attachment value or the attachment url + :param key: used to retrieve survey element + :param value: filename + :param row: current records row + :param data_dictionary: form structure + :param include_images: boolean value to either inlcude images or not + :param attachment_list: to be used incase row doesn't have ATTACHMENTS key + :return: value + """ + if show_choice_labels: + value = get_choice_label_value(key, value, data_dictionary, language) + + if not media_xpaths: + return value + + if key in media_xpaths: + attachments = [ + a + for a in row.get(ATTACHMENTS, attachment_list or []) + if a.get("name") == value + ] + if attachments: + value = current_site_url(attachments[0].get("download_url", "")) + + return value + + +def track_task_progress(additions, total=None): + """ + Updates the current export task with number of submission processed. + Updates in batches of settings EXPORT_TASK_PROGRESS_UPDATE_BATCH defaults + to 100. + :param additions: + :param total: + :return: + """ + batch_size = getattr( + settings, "EXPORT_TASK_PROGRESS_UPDATE_BATCH", DEFAULT_UPDATE_BATCH + ) + if additions % batch_size == 0: + meta = {"progress": additions} + if total: + meta.update({"total": total}) + try: + current_task.update_state(state="PROGRESS", meta=meta) + except AttributeError: + pass diff --git a/onadata/libs/utils/country_field.py b/onadata/libs/utils/country_field.py index 6974a6cf23..3ebe612af8 100644 --- a/onadata/libs/utils/country_field.py +++ b/onadata/libs/utils/country_field.py @@ -1,268 +1,273 @@ +# -*- coding: utf-8 -*- +""" +CountryField - provides a list of countries. +""" from django.db import models from django.utils.translation import gettext_lazy as _ # http://www.unece.org/cefact/locode/service/location.html COUNTRIES = ( - ('AF', _('Afghanistan')), - ('AL', _('Albania')), - ('DZ', _('Algeria')), - ('AS', _('American Samoa')), - ('AD', _('Andorra')), - ('AO', _('Angola')), - ('AI', _('Anguilla')), - ('AQ', _('Antarctica')), - ('AG', _('Antigua and Barbuda')), - ('AR', _('Argentina')), - ('AM', _('Armenia')), - ('AW', _('Aruba')), - ('AU', _('Australia')), - ('AT', _('Austria')), - ('AZ', _('Azerbaijan')), - ('BS', _('Bahamas')), - ('BH', _('Bahrain')), - ('BD', _('Bangladesh')), - ('BB', _('Barbados')), - ('BY', _('Belarus')), - ('BE', _('Belgium')), - ('BZ', _('Belize')), - ('BJ', _('Benin')), - ('BM', _('Bermuda')), - ('BT', _('Bhutan')), - ('BO', _('Bolivia')), - ('BQ', _('Bonaire, Sint Eustatius and Saba')), - ('BA', _('Bosnia and Herzegovina')), - ('BW', _('Botswana')), - ('BR', _('Brazil')), - ('IO', _('British Indian Ocean Territory')), - ('BN', _('Brunei Darussalam')), - ('BG', _('Bulgaria')), - ('BF', _('Burkina Faso')), - ('BI', _('Burundi')), - ('KH', _('Cambodia')), - ('CM', _('Cameroon')), - ('CA', _('Canada')), - ('CV', _('Cape Verde')), - ('KY', _('Cayman Islands')), - ('CF', _('Central African Republic')), - ('TD', _('Chad')), - ('CL', _('Chile')), - ('CN', _('China')), - ('CX', _('Christmas Island')), - ('CC', _('Cocos (Keeling) Islands')), - ('CO', _('Colombia')), - ('KM', _('Comoros')), - ('CG', _('Congo')), - ('CD', _('Congo, The Democratic Republic of the')), - ('CK', _('Cook Islands')), - ('CR', _('Costa Rica')), - ('CI', _('Ivory Coast')), - ('HR', _('Croatia')), - ('CU', _('Cuba')), - ('CW', _('Curacao')), - ('CY', _('Cyprus')), - ('CZ', _('Czech Republic')), - ('DK', _('Denmark')), - ('DJ', _('Djibouti')), - ('DM', _('Dominica')), - ('DO', _('Dominican Republic')), - ('EC', _('Ecuador')), - ('EG', _('Egypt')), - ('SV', _('El Salvador')), - ('GQ', _('Equatorial Guinea')), - ('ER', _('Eritrea')), - ('EE', _('Estonia')), - ('ET', _('Ethiopia')), - ('FK', _('Falkland Islands (Malvinas)')), - ('FO', _('Faroe Islands')), - ('FJ', _('Fiji')), - ('FI', _('Finland')), - ('FR', _('France')), - ('GF', _('French Guiana')), - ('PF', _('French Polynesia')), - ('TF', _('French Southern Territories')), - ('GA', _('Gabon')), - ('GM', _('Gambia')), - ('GE', _('Georgia')), - ('DE', _('Germany')), - ('GH', _('Ghana')), - ('GI', _('Gibraltar')), - ('GR', _('Greece')), - ('GL', _('Greenland')), - ('GD', _('Grenada')), - ('GP', _('Guadeloupe')), - ('GU', _('Guam')), - ('GT', _('Guatemala')), - ('GG', _('Guernsey')), - ('GN', _('Guinea')), - ('GW', _('Guinea-Bissau')), - ('GY', _('Guyana')), - ('HT', _('Haiti')), - ('HM', _('Heard Island and McDonald Islands')), - ('VA', _('Holy See (Vatican City State)')), - ('HN', _('Honduras')), - ('HK', _('Hong Kong')), - ('HU', _('Hungary')), - ('IS', _('Iceland')), - ('IN', _('India')), - ('ID', _('Indonesia')), - ('XZ', _('Installations in International Waters')), - ('IR', _('Iran, Islamic Republic of')), - ('IQ', _('Iraq')), - ('IE', _('Ireland')), - ('IM', _('Isle of Man')), - ('IL', _('Israel')), - ('IT', _('Italy')), - ('JM', _('Jamaica')), - ('JP', _('Japan')), - ('JE', _('Jersey')), - ('JO', _('Jordan')), - ('KZ', _('Kazakhstan')), - ('KE', _('Kenya')), - ('KI', _('Kiribati')), - ('KP', _('Korea, Democratic People\'s Republic of')), - ('KR', _('Korea, Republic of')), + ("AF", _("Afghanistan")), + ("AL", _("Albania")), + ("DZ", _("Algeria")), + ("AS", _("American Samoa")), + ("AD", _("Andorra")), + ("AO", _("Angola")), + ("AI", _("Anguilla")), + ("AQ", _("Antarctica")), + ("AG", _("Antigua and Barbuda")), + ("AR", _("Argentina")), + ("AM", _("Armenia")), + ("AW", _("Aruba")), + ("AU", _("Australia")), + ("AT", _("Austria")), + ("AZ", _("Azerbaijan")), + ("BS", _("Bahamas")), + ("BH", _("Bahrain")), + ("BD", _("Bangladesh")), + ("BB", _("Barbados")), + ("BY", _("Belarus")), + ("BE", _("Belgium")), + ("BZ", _("Belize")), + ("BJ", _("Benin")), + ("BM", _("Bermuda")), + ("BT", _("Bhutan")), + ("BO", _("Bolivia")), + ("BQ", _("Bonaire, Sint Eustatius and Saba")), + ("BA", _("Bosnia and Herzegovina")), + ("BW", _("Botswana")), + ("BR", _("Brazil")), + ("IO", _("British Indian Ocean Territory")), + ("BN", _("Brunei Darussalam")), + ("BG", _("Bulgaria")), + ("BF", _("Burkina Faso")), + ("BI", _("Burundi")), + ("KH", _("Cambodia")), + ("CM", _("Cameroon")), + ("CA", _("Canada")), + ("CV", _("Cape Verde")), + ("KY", _("Cayman Islands")), + ("CF", _("Central African Republic")), + ("TD", _("Chad")), + ("CL", _("Chile")), + ("CN", _("China")), + ("CX", _("Christmas Island")), + ("CC", _("Cocos (Keeling) Islands")), + ("CO", _("Colombia")), + ("KM", _("Comoros")), + ("CG", _("Congo")), + ("CD", _("Congo, The Democratic Republic of the")), + ("CK", _("Cook Islands")), + ("CR", _("Costa Rica")), + ("CI", _("Ivory Coast")), + ("HR", _("Croatia")), + ("CU", _("Cuba")), + ("CW", _("Curacao")), + ("CY", _("Cyprus")), + ("CZ", _("Czech Republic")), + ("DK", _("Denmark")), + ("DJ", _("Djibouti")), + ("DM", _("Dominica")), + ("DO", _("Dominican Republic")), + ("EC", _("Ecuador")), + ("EG", _("Egypt")), + ("SV", _("El Salvador")), + ("GQ", _("Equatorial Guinea")), + ("ER", _("Eritrea")), + ("EE", _("Estonia")), + ("ET", _("Ethiopia")), + ("FK", _("Falkland Islands (Malvinas)")), + ("FO", _("Faroe Islands")), + ("FJ", _("Fiji")), + ("FI", _("Finland")), + ("FR", _("France")), + ("GF", _("French Guiana")), + ("PF", _("French Polynesia")), + ("TF", _("French Southern Territories")), + ("GA", _("Gabon")), + ("GM", _("Gambia")), + ("GE", _("Georgia")), + ("DE", _("Germany")), + ("GH", _("Ghana")), + ("GI", _("Gibraltar")), + ("GR", _("Greece")), + ("GL", _("Greenland")), + ("GD", _("Grenada")), + ("GP", _("Guadeloupe")), + ("GU", _("Guam")), + ("GT", _("Guatemala")), + ("GG", _("Guernsey")), + ("GN", _("Guinea")), + ("GW", _("Guinea-Bissau")), + ("GY", _("Guyana")), + ("HT", _("Haiti")), + ("HM", _("Heard Island and McDonald Islands")), + ("VA", _("Holy See (Vatican City State)")), + ("HN", _("Honduras")), + ("HK", _("Hong Kong")), + ("HU", _("Hungary")), + ("IS", _("Iceland")), + ("IN", _("India")), + ("ID", _("Indonesia")), + ("XZ", _("Installations in International Waters")), + ("IR", _("Iran, Islamic Republic of")), + ("IQ", _("Iraq")), + ("IE", _("Ireland")), + ("IM", _("Isle of Man")), + ("IL", _("Israel")), + ("IT", _("Italy")), + ("JM", _("Jamaica")), + ("JP", _("Japan")), + ("JE", _("Jersey")), + ("JO", _("Jordan")), + ("KZ", _("Kazakhstan")), + ("KE", _("Kenya")), + ("KI", _("Kiribati")), + ("KP", _("Korea, Democratic People's Republic of")), + ("KR", _("Korea, Republic of")), # see http://geonames.wordpress.com/2010/03/08/xk-country-code-for-kosovo/ - ('XK', _('Kosovo')), - ('KW', _('Kuwait')), - ('KG', _('Kyrgyzstan')), - ('LA', _('Lao People\'s Democratic Republic')), - ('LV', _('Latvia')), - ('LB', _('Lebanon')), - ('LS', _('Lesotho')), - ('LR', _('Liberia')), - ('LY', _('Libyan Arab Jamahiriya')), - ('LI', _('Liechtenstein')), - ('LT', _('Lithuania')), - ('LU', _('Luxembourg')), - ('MO', _('Macao')), - ('MK', _('Macedonia, The former Yugoslav Republic of')), - ('MG', _('Madagascar')), - ('MW', _('Malawi')), - ('MY', _('Malaysia')), - ('MV', _('Maldives')), - ('ML', _('Mali')), - ('MT', _('Malta')), - ('MH', _('Marshall Islands')), - ('MQ', _('Martinique')), - ('MR', _('Mauritania')), - ('MU', _('Mauritius')), - ('YT', _('Mayotte')), - ('MX', _('Mexico')), - ('FM', _('Micronesia, Federated States of')), - ('MD', _('Moldova, Republic of')), - ('MC', _('Monaco')), - ('MN', _('Mongolia')), - ('ME', _('Montenegro')), - ('MS', _('Montserrat')), - ('MA', _('Morocco')), - ('MZ', _('Mozambique')), - ('MM', _('Myanmar')), - ('NA', _('Namibia')), - ('NR', _('Nauru')), - ('NP', _('Nepal')), - ('NL', _('Netherlands')), - ('NC', _('New Caledonia')), - ('NZ', _('New Zealand')), - ('NI', _('Nicaragua')), - ('NE', _('Niger')), - ('NG', _('Nigeria')), - ('NU', _('Niue')), - ('NF', _('Norfolk Island')), - ('MP', _('Northern Mariana Islands')), - ('NO', _('Norway')), - ('OM', _('Oman')), - ('PK', _('Pakistan')), - ('PW', _('Palau')), - ('PS', _('Palestinian Territory, Occupied')), - ('PA', _('Panama')), - ('PG', _('Papua New Guinea')), - ('PY', _('Paraguay')), - ('PE', _('Peru')), - ('PH', _('Philippines')), - ('PN', _('Pitcairn')), - ('PL', _('Poland')), - ('PT', _('Portugal')), - ('PR', _('Puerto Rico')), - ('QA', _('Qatar')), - ('RE', _('Reunion')), - ('RO', _('Romania')), - ('RU', _('Russian Federation')), - ('RW', _('Rwanda')), - ('SH', _('Saint Helena')), - ('KN', _('Saint Kitts and Nevis')), - ('LC', _('Saint Lucia')), - ('PM', _('Saint Pierre and Miquelon')), - ('VC', _('Saint Vincent and the Grenadines')), - ('WS', _('Samoa')), - ('SM', _('San Marino')), - ('ST', _('Sao Tome and Principe')), - ('SA', _('Saudi Arabia')), - ('SN', _('Senegal')), - ('RS', _('Serbia')), - ('SC', _('Seychelles')), - ('SL', _('Sierra Leone')), - ('SG', _('Singapore')), - ('SX', _('Sint Maarten (Dutch Part)')), - ('SK', _('Slovakia')), - ('SI', _('Slovenia')), - ('SB', _('Solomon Islands')), - ('SO', _('Somalia')), - ('ZA', _('South Africa')), - ('GS', _('South Georgia and the South Sandwich Islands')), - ('SS', _('South Sudan')), - ('ES', _('Spain')), - ('LK', _('Sri Lanka')), - ('SD', _('Sudan')), - ('SR', _('Suriname')), - ('SJ', _('Svalbard and Jan Mayen')), - ('SZ', _('Swaziland')), - ('SE', _('Sweden')), - ('CH', _('Switzerland')), - ('SY', _('Syrian Arab Republic')), - ('TW', _('Taiwan, Province of China')), - ('TJ', _('Tajikistan')), - ('TZ', _('Tanzania, United Republic of')), - ('TH', _('Thailand')), - ('TL', _('Timor-Leste')), - ('TG', _('Togo')), - ('TK', _('Tokelau')), - ('TO', _('Tonga')), - ('TT', _('Trinidad and Tobago')), - ('TN', _('Tunisia')), - ('TR', _('Turkey')), - ('TM', _('Turkmenistan')), - ('TC', _('Turks and Caicos Islands')), - ('TV', _('Tuvalu')), - ('UG', _('Uganda')), - ('UA', _('Ukraine')), - ('AE', _('United Arab Emirates')), - ('GB', _('United Kingdom')), - ('US', _('United States')), - ('UM', _('United States Minor Outlying Islands')), - ('UY', _('Uruguay')), - ('UZ', _('Uzbekistan')), - ('VU', _('Vanuatu')), - ('VE', _('Venezuela')), - ('VN', _('Viet Nam')), - ('VG', _('Virgin Islands, British')), - ('VI', _('Virgin Islands, U.S.')), - ('WF', _('Wallis and Futuna')), - ('EH', _('Western Sahara')), - ('YE', _('Yemen')), - ('ZM', _('Zambia')), - ('ZW', _('Zimbabwe')), - ('ZZ', _('Unknown or unspecified country')), + ("XK", _("Kosovo")), + ("KW", _("Kuwait")), + ("KG", _("Kyrgyzstan")), + ("LA", _("Lao People's Democratic Republic")), + ("LV", _("Latvia")), + ("LB", _("Lebanon")), + ("LS", _("Lesotho")), + ("LR", _("Liberia")), + ("LY", _("Libyan Arab Jamahiriya")), + ("LI", _("Liechtenstein")), + ("LT", _("Lithuania")), + ("LU", _("Luxembourg")), + ("MO", _("Macao")), + ("MK", _("Macedonia, The former Yugoslav Republic of")), + ("MG", _("Madagascar")), + ("MW", _("Malawi")), + ("MY", _("Malaysia")), + ("MV", _("Maldives")), + ("ML", _("Mali")), + ("MT", _("Malta")), + ("MH", _("Marshall Islands")), + ("MQ", _("Martinique")), + ("MR", _("Mauritania")), + ("MU", _("Mauritius")), + ("YT", _("Mayotte")), + ("MX", _("Mexico")), + ("FM", _("Micronesia, Federated States of")), + ("MD", _("Moldova, Republic of")), + ("MC", _("Monaco")), + ("MN", _("Mongolia")), + ("ME", _("Montenegro")), + ("MS", _("Montserrat")), + ("MA", _("Morocco")), + ("MZ", _("Mozambique")), + ("MM", _("Myanmar")), + ("NA", _("Namibia")), + ("NR", _("Nauru")), + ("NP", _("Nepal")), + ("NL", _("Netherlands")), + ("NC", _("New Caledonia")), + ("NZ", _("New Zealand")), + ("NI", _("Nicaragua")), + ("NE", _("Niger")), + ("NG", _("Nigeria")), + ("NU", _("Niue")), + ("NF", _("Norfolk Island")), + ("MP", _("Northern Mariana Islands")), + ("NO", _("Norway")), + ("OM", _("Oman")), + ("PK", _("Pakistan")), + ("PW", _("Palau")), + ("PS", _("Palestinian Territory, Occupied")), + ("PA", _("Panama")), + ("PG", _("Papua New Guinea")), + ("PY", _("Paraguay")), + ("PE", _("Peru")), + ("PH", _("Philippines")), + ("PN", _("Pitcairn")), + ("PL", _("Poland")), + ("PT", _("Portugal")), + ("PR", _("Puerto Rico")), + ("QA", _("Qatar")), + ("RE", _("Reunion")), + ("RO", _("Romania")), + ("RU", _("Russian Federation")), + ("RW", _("Rwanda")), + ("SH", _("Saint Helena")), + ("KN", _("Saint Kitts and Nevis")), + ("LC", _("Saint Lucia")), + ("PM", _("Saint Pierre and Miquelon")), + ("VC", _("Saint Vincent and the Grenadines")), + ("WS", _("Samoa")), + ("SM", _("San Marino")), + ("ST", _("Sao Tome and Principe")), + ("SA", _("Saudi Arabia")), + ("SN", _("Senegal")), + ("RS", _("Serbia")), + ("SC", _("Seychelles")), + ("SL", _("Sierra Leone")), + ("SG", _("Singapore")), + ("SX", _("Sint Maarten (Dutch Part)")), + ("SK", _("Slovakia")), + ("SI", _("Slovenia")), + ("SB", _("Solomon Islands")), + ("SO", _("Somalia")), + ("ZA", _("South Africa")), + ("GS", _("South Georgia and the South Sandwich Islands")), + ("SS", _("South Sudan")), + ("ES", _("Spain")), + ("LK", _("Sri Lanka")), + ("SD", _("Sudan")), + ("SR", _("Suriname")), + ("SJ", _("Svalbard and Jan Mayen")), + ("SZ", _("Swaziland")), + ("SE", _("Sweden")), + ("CH", _("Switzerland")), + ("SY", _("Syrian Arab Republic")), + ("TW", _("Taiwan, Province of China")), + ("TJ", _("Tajikistan")), + ("TZ", _("Tanzania, United Republic of")), + ("TH", _("Thailand")), + ("TL", _("Timor-Leste")), + ("TG", _("Togo")), + ("TK", _("Tokelau")), + ("TO", _("Tonga")), + ("TT", _("Trinidad and Tobago")), + ("TN", _("Tunisia")), + ("TR", _("Turkey")), + ("TM", _("Turkmenistan")), + ("TC", _("Turks and Caicos Islands")), + ("TV", _("Tuvalu")), + ("UG", _("Uganda")), + ("UA", _("Ukraine")), + ("AE", _("United Arab Emirates")), + ("GB", _("United Kingdom")), + ("US", _("United States")), + ("UM", _("United States Minor Outlying Islands")), + ("UY", _("Uruguay")), + ("UZ", _("Uzbekistan")), + ("VU", _("Vanuatu")), + ("VE", _("Venezuela")), + ("VN", _("Viet Nam")), + ("VG", _("Virgin Islands, British")), + ("VI", _("Virgin Islands, U.S.")), + ("WF", _("Wallis and Futuna")), + ("EH", _("Western Sahara")), + ("YE", _("Yemen")), + ("ZM", _("Zambia")), + ("ZW", _("Zimbabwe")), + ("ZZ", _("Unknown or unspecified country")), ) class CountryField(models.CharField): + """A CharField that limits the choices to country codes.""" def __init__(self, *args, **kwargs): - kwargs.setdefault('maxlength', 2) - kwargs.setdefault('choices', COUNTRIES) + kwargs.setdefault("maxlength", 2) + kwargs.setdefault("choices", COUNTRIES) - super(CountryField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def get_internal_type(self): return "CharField" diff --git a/onadata/libs/utils/csv_builder.py b/onadata/libs/utils/csv_builder.py index 6ea7293454..a39025b18a 100644 --- a/onadata/libs/utils/csv_builder.py +++ b/onadata/libs/utils/csv_builder.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ CSV export utility functions. """ @@ -9,11 +9,10 @@ from django.db.models.query import QuerySet from django.utils.translation import gettext as _ -from six import iteritems - import unicodecsv as csv from pyxform.question import Question from pyxform.section import RepeatingSection, Section +from six import iteritems from onadata.apps.logger.models import OsmData from onadata.apps.logger.models.xform import XForm, question_types_to_exclude @@ -47,12 +46,12 @@ VERSION, XFORM_ID_STRING, ) -from onadata.libs.utils.export_builder import ( +from onadata.libs.utils.common_tools import ( get_choice_label, get_value_or_attachment_uri, + str_to_bool, track_task_progress, ) -from onadata.libs.utils.export_tools import str_to_bool from onadata.libs.utils.model_tools import get_columns_with_hxl # the bind type of select multiples that we use to compare @@ -465,9 +464,6 @@ def _split_gps_fields(cls, record, gps_fields): gps_parts = {xpath: None for xpath in gps_xpaths} # hack, check if its a list and grab the object within that parts = value.split(" ") - # TODO: check whether or not we can have a gps recording - # from ODKCollect that has less than four components, - # for now we are assuming that this is not the case. if len(parts) == 4: gps_parts = dict(zip(gps_xpaths, parts)) updated_gps_fields.update(gps_parts) @@ -512,8 +508,6 @@ def _query_data( "fields": fields, "start": self.start, "end": self.end, - # TODO: we might want to add this in for the user - # to sepcify a sort order "sort": "id", "start_index": start, "limit": limit, diff --git a/onadata/libs/utils/csv_import.py b/onadata/libs/utils/csv_import.py index b369592ea5..f75c3d5386 100644 --- a/onadata/libs/utils/csv_import.py +++ b/onadata/libs/utils/csv_import.py @@ -29,7 +29,7 @@ from onadata.apps.logger.models import Instance, XForm from onadata.apps.messaging.constants import SUBMISSION_DELETED, XFORM from onadata.apps.messaging.serializers import send_message -from onadata.celery import app +from onadata.celeryapp import app from onadata.libs.serializers.metadata_serializer import MetaDataSerializer from onadata.libs.utils import analytics from onadata.libs.utils.async_status import FAILED, async_status, celery_state_to_status diff --git a/onadata/libs/utils/csv_reader.py b/onadata/libs/utils/csv_reader.py index 7f3153c6c8..4c20086a74 100644 --- a/onadata/libs/utils/csv_reader.py +++ b/onadata/libs/utils/csv_reader.py @@ -1,9 +1,11 @@ -# vim: ai ts=4 sts=4 et sw=4 encoding=utf-8 - +# vim: ai ts=4 sts=4 et sw=4 fileencoding=utf-8 +""" +CsvReader class module. +""" import csv -class CsvReader(object): +class CsvReader: """ Typical usage:: @@ -17,16 +19,19 @@ def __init__(self, path): self.open(path) def open(self, path): - self._file = open(path, 'rU') # universal new-line mode + """Opens a file handle sets a CSV reader.""" + # pylint: disable=consider-using-with,unspecified-encoding + self._file = open(path, "rU") # universal new-line mode # http://stackoverflow.com/questions/904041/reading-a-utf8-csv-file-wit # h-python/904085#904085 self._csv_reader = csv.reader(self._file) def close(self): + """Closes the file handle.""" self._file.close() def __iter__(self): - return self + return iter(self) def next(self): """ @@ -35,12 +40,14 @@ def next(self): of data. """ row = self._csv_reader.next() - return [cell for cell in row] + return list(row) def _set_headers(self): + # pylint: disable=attribute-defined-outside-init self._headers = self.next() def iter_dicts(self): + """Iterate over CSV rows as dict items.""" self._set_headers() for row in self: result = {} diff --git a/onadata/libs/utils/dict_tools.py b/onadata/libs/utils/dict_tools.py index a348bf26aa..fe796c6632 100644 --- a/onadata/libs/utils/dict_tools.py +++ b/onadata/libs/utils/dict_tools.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Dict utility functions module. """ diff --git a/onadata/libs/utils/export_builder.py b/onadata/libs/utils/export_builder.py index 5084047231..91d2e17866 100644 --- a/onadata/libs/utils/export_builder.py +++ b/onadata/libs/utils/export_builder.py @@ -12,19 +12,14 @@ from zipfile import ZIP_DEFLATED, ZipFile from django.conf import settings -from django.contrib.sites.models import Site from django.core.files.temp import NamedTemporaryFile -from six import iteritems - -from celery import current_task from openpyxl.utils.datetime import to_excel from openpyxl.workbook import Workbook from pyxform.question import Question from pyxform.section import RepeatingSection, Section - - from savReaderWriter import SavWriter +from six import iteritems from onadata.apps.logger.models.osmdata import OsmData from onadata.apps.logger.models.xform import ( @@ -60,123 +55,23 @@ VERSION, XFORM_ID_STRING, ) -from onadata.libs.utils.common_tools import str_to_bool +from onadata.libs.utils.common_tools import ( + get_choice_label, + get_choice_label_value, + get_value_or_attachment_uri, + str_to_bool, + track_task_progress, +) from onadata.libs.utils.mongo import _decode_from_mongo, _is_invalid_for_mongo # the bind type of select multiples that we use to compare GEOPOINT_BIND_TYPE = "geopoint" OSM_BIND_TYPE = "osm" -DEFAULT_UPDATE_BATCH = 100 YES = 1 NO = 0 -def current_site_url(path): - """ - Returns fully qualified URL (no trailing slash) for the current site. - :param path - :return: complete url - """ - - current_site = Site.objects.get_current() - protocol = getattr(settings, "ONA_SITE_PROTOCOL", "http") - port = getattr(settings, "ONA_SITE_PORT", "") - url = f"{protocol}://{current_site.domain}" - if port: - url += f":{port}" - if path: - url += f"{path}" - - return url - - -def get_choice_label(label, data_dictionary, language=None): - """ - Return the label matching selected language or simply just the label. - """ - if isinstance(label, dict): - languages = list(label.keys()) - _language = ( - language - if language in languages - else data_dictionary.get_language(languages) - ) - - return label[_language] - - return label - - -def get_choice_label_value(key, value, data_dictionary, language=None): - """ - Return the label of a choice matching the value if the key xpath is a - SELECT_ONE otherwise it returns the value unchanged. - """ - - def _get_choice_label_value(lookup): - _label = None - for choice in data_dictionary.get_survey_element(key).children: - if choice.name == lookup: - _label = get_choice_label(choice.label, data_dictionary, language) - break - - return _label - - label = None - if key in data_dictionary.get_select_one_xpaths(): - label = _get_choice_label_value(value) - - if key in data_dictionary.get_select_multiple_xpaths(): - answers = [] - for item in value.split(" "): - answer = _get_choice_label_value(item) - answers.append(answer or item) - if [_i for _i in answers if _i is not None]: - label = " ".join(answers) - - return label or value - - -# pylint: disable=too-many-arguments -def get_value_or_attachment_uri( - key, - value, - row, - data_dictionary, - media_xpaths, - attachment_list=None, - show_choice_labels=False, - language=None, -): - """ - Gets either the attachment value or the attachment url - :param key: used to retrieve survey element - :param value: filename - :param row: current records row - :param data_dictionary: form structure - :param include_images: boolean value to either inlcude images or not - :param attachment_list: to be used incase row doesn't have ATTACHMENTS key - :return: value - """ - if show_choice_labels: - value = get_choice_label_value(key, value, data_dictionary, language) - - if not media_xpaths: - return value - - if key in media_xpaths: - attachments = [ - a - for a in row.get(ATTACHMENTS, attachment_list or []) - if a.get("name") == value - ] - if attachments: - value = current_site_url(attachments[0].get("download_url", "")) - - return value - - def get_data_dictionary_from_survey(survey): """Creates a DataDictionary instance from an XML survey instance.""" data_dicionary = DataDictionary() @@ -301,28 +196,6 @@ def is_all_numeric(items): ) -def track_task_progress(additions, total=None): - """ - Updates the current export task with number of submission processed. - Updates in batches of settings EXPORT_TASK_PROGRESS_UPDATE_BATCH defaults - to 100. - :param additions: - :param total: - :return: - """ - batch_size = getattr( - settings, "EXPORT_TASK_PROGRESS_UPDATE_BATCH", DEFAULT_UPDATE_BATCH - ) - if additions % batch_size == 0: - meta = {"progress": additions} - if total: - meta.update({"total": total}) - try: - current_task.update_state(state="PROGRESS", meta=meta) - except AttributeError: - pass - - # pylint: disable=invalid-name def string_to_date_with_xls_validation(date_str): """Try to convert a string to a date object. @@ -548,6 +421,7 @@ def get_choice_dict(xpath, label): return choices + # pylint: disable=too-many-statements def set_survey(self, survey, xform=None, include_reviews=False): """Set's the XForm XML ``survey`` instance.""" if self.INCLUDE_REVIEWS or include_reviews: @@ -557,7 +431,7 @@ def set_survey(self, survey, xform=None, include_reviews=False): REVIEW_COMMENT, REVIEW_DATE, ] - self.__init__() + self.__init__() # pylint: disable=unnecessary-dunder-call data_dicionary = get_data_dictionary_from_survey(survey) # pylint: disable=too-many-locals,too-many-branches,too-many-arguments @@ -875,6 +749,7 @@ def split_select_multiples( @classmethod def split_gps_components(cls, row, gps_fields): + """Splits GPS components into their own fields.""" # for each gps_field, get associated data and split it for (xpath, gps_components) in iteritems(gps_fields): data = row.get(xpath) @@ -886,6 +761,7 @@ def split_gps_components(cls, row, gps_fields): @classmethod def decode_mongo_encoded_fields(cls, row, encoded_fields): + """Update encoded fields with their corresponding xpath""" for (xpath, encoded_xpath) in iteritems(encoded_fields): if row.get(encoded_xpath): val = row.pop(encoded_xpath) diff --git a/onadata/libs/utils/export_tools.py b/onadata/libs/utils/export_tools.py index 6cc125c5be..61ebed378b 100644 --- a/onadata/libs/utils/export_tools.py +++ b/onadata/libs/utils/export_tools.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: disable=too-many-lines """ Export tools """ @@ -49,7 +50,6 @@ retry, str_to_bool, ) -from onadata.libs.utils.export_builder import ExportBuilder from onadata.libs.utils.model_tools import get_columns_with_hxl, queryset_iterator from onadata.libs.utils.osm import get_combined_osm from onadata.libs.utils.viewer_tools import create_attachments_zipfile, image_urls @@ -167,40 +167,49 @@ def generate_export(export_type, xform, export_id=None, options=None): # noqa C if isinstance(records, QuerySet): records = records.iterator() + # pylint: disable=import-outside-toplevel + from onadata.libs.utils.export_builder import ExportBuilder + export_builder = ExportBuilder() - export_builder.TRUNCATE_GROUP_TITLE = ( + export_builder.TRUNCATE_GROUP_TITLE = ( # noqa True if export_type == Export.SAV_ZIP_EXPORT else remove_group_name ) - export_builder.GROUP_DELIMITER = options.get( + export_builder.GROUP_DELIMITER = options.get( # noqa "group_delimiter", DEFAULT_GROUP_DELIMITER ) - export_builder.SPLIT_SELECT_MULTIPLES = options.get("split_select_multiples", True) - export_builder.BINARY_SELECT_MULTIPLES = options.get( + export_builder.SPLIT_SELECT_MULTIPLES = options.get( # noqa + "split_select_multiples", True + ) + export_builder.BINARY_SELECT_MULTIPLES = options.get( # noqa "binary_select_multiples", False ) - export_builder.INCLUDE_LABELS = options.get("include_labels", False) + export_builder.INCLUDE_LABELS = options.get("include_labels", False) # noqa include_reviews = options.get("include_reviews", False) - export_builder.INCLUDE_LABELS_ONLY = options.get("include_labels_only", False) - export_builder.INCLUDE_HXL = options.get("include_hxl", False) + export_builder.INCLUDE_LABELS_ONLY = options.get( # noqa + "include_labels_only", False + ) + export_builder.INCLUDE_HXL = options.get("include_hxl", False) # noqa - export_builder.INCLUDE_IMAGES = options.get( + export_builder.INCLUDE_IMAGES = options.get( # noqa "include_images", settings.EXPORT_WITH_IMAGE_DEFAULT ) - export_builder.VALUE_SELECT_MULTIPLES = options.get("value_select_multiples", False) + export_builder.VALUE_SELECT_MULTIPLES = options.get( # noqa + "value_select_multiples", False + ) - export_builder.REPEAT_INDEX_TAGS = options.get( + export_builder.REPEAT_INDEX_TAGS = options.get( # noqa "repeat_index_tags", DEFAULT_INDEX_TAGS ) - export_builder.SHOW_CHOICE_LABELS = options.get("show_choice_labels", False) + export_builder.SHOW_CHOICE_LABELS = options.get("show_choice_labels", False) # noqa export_builder.language = options.get("language") # 'win_excel_utf8' is only relevant for CSV exports if "win_excel_utf8" in options and export_type != Export.CSV_EXPORT: del options["win_excel_utf8"] - export_builder.INCLUDE_REVIEWS = include_reviews + export_builder.INCLUDE_REVIEWS = include_reviews # noqa export_builder.set_survey(xform.survey, xform, include_reviews=include_reviews) temp_file = NamedTemporaryFile(suffix=("." + extension)) @@ -213,7 +222,7 @@ def generate_export(export_type, xform, export_id=None, options=None): # noqa C func = getattr(export_builder, export_type_func_map[export_type]) # pylint: disable=broad-except try: - func.__call__( + func( temp_file.name, records, username, @@ -556,7 +565,7 @@ def generate_geojson_export( metadata=None, export_id=None, options=None, - xform=None + xform=None, ): """ Generates Linked Geojson export @@ -579,12 +588,12 @@ def generate_geojson_export( "geo_field": extra_data.get("data_geo_field"), "simple_style": extra_data.get("data_simple_style"), "title": extra_data.get("data_title"), - "fields": extra_data.get("data_fields") + "fields": extra_data.get("data_fields"), } _context = {} - _context['request'] = request + _context["request"] = request content = GeoJsonSerializer(xform.instances.all(), many=True, context=_context) - data_to_write = json.dumps(content.data).encode('utf-8') + data_to_write = json.dumps(content.data).encode("utf-8") timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S") basename = f"{id_string}_{timestamp}" filename = basename + "." + extension diff --git a/onadata/libs/utils/google.py b/onadata/libs/utils/google.py index 33fb4cd5f9..be323ff8bc 100644 --- a/onadata/libs/utils/google.py +++ b/onadata/libs/utils/google.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Google utility functions. """ diff --git a/onadata/libs/utils/image_tools.py b/onadata/libs/utils/image_tools.py index 3230a9953c..0d2ce32084 100644 --- a/onadata/libs/utils/image_tools.py +++ b/onadata/libs/utils/image_tools.py @@ -1,17 +1,22 @@ -import boto3 -import urllib +# -*- coding: utf-8 -*- +""" +Image utility functions module. +""" import logging +import urllib from datetime import datetime, timedelta from tempfile import NamedTemporaryFile +from wsgiref.util import FileWrapper -from PIL import Image from django.conf import settings from django.core.files.base import ContentFile from django.core.files.storage import get_storage_class from django.http import HttpResponse, HttpResponseRedirect -from botocore.exceptions import ClientError + +import boto3 from botocore.client import Config -from wsgiref.util import FileWrapper +from botocore.exceptions import ClientError +from PIL import Image from onadata.libs.utils.viewer_tools import get_path @@ -26,25 +31,29 @@ def flat(*nums): def generate_media_download_url(obj, expiration: int = 3600): + """ + Returns a HTTP response of a media object or a redirect to the image URL for S3 and + Azure storage objects. + """ file_path = obj.media_file.name default_storage = get_storage_class()() filename = file_path.split("/")[-1] - s3 = None + content_disposition = urllib.parse.quote(f"attachment; filename={filename}") + s3_class = None azure = None try: - s3 = get_storage_class("storages.backends.s3boto3.S3Boto3Storage")() + s3_class = get_storage_class("storages.backends.s3boto3.S3Boto3Storage")() except ModuleNotFoundError: pass try: azure = get_storage_class("storages.backends.azure_storage.AzureStorage")() except ModuleNotFoundError: - if s3 is None: + if s3_class is None: return HttpResponseRedirect(obj.media_file.url) - content_disposition = urllib.parse.quote(f"attachment; filename={filename}") - if isinstance(default_storage, type(s3)): + if isinstance(default_storage, type(s3_class)): try: url = generate_aws_media_url(file_path, content_disposition, expiration) except ClientError as e: @@ -52,22 +61,25 @@ def generate_media_download_url(obj, expiration: int = 3600): return None else: return HttpResponseRedirect(url) - elif isinstance(default_storage, type(azure)): + + if isinstance(default_storage, type(azure)): media_url = generate_media_url_with_sas(file_path, expiration) return HttpResponseRedirect(media_url) - else: - file_obj = open(settings.MEDIA_ROOT + file_path, "rb") - response = HttpResponse(FileWrapper(file_obj), content_type=obj.mimetype) - response["Content-Disposition"] = content_disposition - return response + # pylint: disable=consider-using-with + file_obj = open(settings.MEDIA_ROOT + file_path, "rb") + response = HttpResponse(FileWrapper(file_obj), content_type=obj.mimetype) + response["Content-Disposition"] = content_disposition + + return response def generate_aws_media_url( file_path: str, content_disposition: str, expiration: int = 3600 ): - s3 = get_storage_class("storages.backends.s3boto3.S3Boto3Storage")() - bucket_name = s3.bucket.name + """Generate S3 URL.""" + s3_class = get_storage_class("storages.backends.s3boto3.S3Boto3Storage")() + bucket_name = s3_class.bucket.name s3_config = Config( signature_version=getattr(settings, "AWS_S3_SIGNATURE_VERSION", "s3v4"), region_name=getattr(settings, "AWS_S3_REGION_NAME", ""), @@ -88,11 +100,17 @@ def generate_aws_media_url( def generate_media_url_with_sas(file_path: str, expiration: int = 3600): - from azure.storage.blob import generate_blob_sas, AccountSasPermissions + """ + Generate Azure storage URL. + """ + # pylint: disable=import-outside-toplevel + from azure.storage.blob import AccountSasPermissions, generate_blob_sas account_name = getattr(settings, "AZURE_ACCOUNT_NAME", "") container_name = getattr(settings, "AZURE_CONTAINER", "") - media_url = f"https://{account_name}.blob.core.windows.net/{container_name}/{file_path}" # noqa + media_url = ( + f"https://{account_name}.blob.core.windows.net/{container_name}/{file_path}" + ) sas_token = generate_blob_sas( account_name=account_name, account_key=getattr(settings, "AZURE_ACCOUNT_KEY", ""), @@ -105,6 +123,7 @@ def generate_media_url_with_sas(file_path: str, expiration: int = 3600): def get_dimensions(size, longest_side): + """Return integer tuple of width and height given size and longest_side length.""" width, height = size if width > height: @@ -121,23 +140,22 @@ def get_dimensions(size, longest_side): def _save_thumbnails(image, path, size, suffix, extension): - nm = NamedTemporaryFile(suffix=".%s" % extension) - default_storage = get_storage_class()() + with NamedTemporaryFile(suffix=f".{extension}") as temp_file: + default_storage = get_storage_class()() - try: - # Ensure conversion to float in operations - image.thumbnail(get_dimensions(image.size, float(size)), Image.ANTIALIAS) - except ZeroDivisionError: - pass + try: + # Ensure conversion to float in operations + image.thumbnail(get_dimensions(image.size, float(size)), Image.ANTIALIAS) + except ZeroDivisionError: + pass - image.save(nm.name) - default_storage.save(get_path(path, suffix), ContentFile(nm.read())) - nm.close() + image.save(temp_file.name) + default_storage.save(get_path(path, suffix), ContentFile(temp_file.read())) + temp_file.close() def resize(filename, extension): - if extension == "non": - extension = settings.DEFAULT_IMG_FILE_TYPE + """Resize an image into multiple sizes.""" default_storage = get_storage_class()() try: @@ -147,24 +165,31 @@ def resize(filename, extension): for key in settings.THUMB_ORDER: _save_thumbnails( - image, filename, conf[key]["size"], conf[key]["suffix"], extension + image, + filename, + conf[key]["size"], + conf[key]["suffix"], + settings.DEFAULT_IMG_FILE_TYPE if extension == "non" else extension, ) - except IOError: - raise Exception("The image file couldn't be identified") + except IOError as exc: + raise Exception("The image file couldn't be identified") from exc def resize_local_env(filename, extension): - if extension == "non": - extension = settings.DEFAULT_IMG_FILE_TYPE + """Resize images in a local environment.""" default_storage = get_storage_class()() path = default_storage.path(filename) image = Image.open(path) conf = settings.THUMB_CONF - [ - _save_thumbnails(image, path, conf[key]["size"], conf[key]["suffix"], extension) - for key in settings.THUMB_ORDER - ] + for key in settings.THUMB_ORDER: + _save_thumbnails( + image, + path, + conf[key]["size"], + conf[key]["suffix"], + settings.DEFAULT_IMG_FILE_TYPE if extension == "non" else extension, + ) def image_url(attachment, suffix): @@ -181,33 +206,33 @@ def image_url(attachment, suffix): if suffix == "original": return url - else: - default_storage = get_storage_class()() - fs = get_storage_class("django.core.files.storage.FileSystemStorage")() - - if suffix in settings.THUMB_CONF: - size = settings.THUMB_CONF[suffix]["suffix"] - filename = attachment.media_file.name - - if default_storage.exists(filename): - if ( - default_storage.exists(get_path(filename, size)) - and default_storage.size(get_path(filename, size)) > 0 - ): - file_path = get_path(filename, size) - url = ( - generate_media_url_with_sas(file_path) - if isinstance(default_storage, type(azure)) - else default_storage.url(file_path) - ) - else: - if default_storage.__class__ != fs.__class__: - resize(filename, extension=attachment.extension) - else: - resize_local_env(filename, extension=attachment.extension) - return image_url(attachment, suffix) + default_storage = get_storage_class()() + file_storage = get_storage_class("django.core.files.storage.FileSystemStorage")() + + if suffix in settings.THUMB_CONF: + size = settings.THUMB_CONF[suffix]["suffix"] + filename = attachment.media_file.name + + if default_storage.exists(filename): + if ( + default_storage.exists(get_path(filename, size)) + and default_storage.size(get_path(filename, size)) > 0 + ): + file_path = get_path(filename, size) + url = ( + generate_media_url_with_sas(file_path) + if isinstance(default_storage, type(azure)) + else default_storage.url(file_path) + ) else: - return None + if default_storage.__class__ != file_storage.__class__: + resize(filename, extension=attachment.extension) + else: + resize_local_env(filename, extension=attachment.extension) + + return image_url(attachment, suffix) + else: + return None return url diff --git a/onadata/libs/utils/log.py b/onadata/libs/utils/log.py index 8b2d470c19..0e35001209 100644 --- a/onadata/libs/utils/log.py +++ b/onadata/libs/utils/log.py @@ -1,12 +1,16 @@ +# -*- coding: utf-8 -*- +""" +Log utility functions and classes. +""" import logging from datetime import datetime -from django.utils.translation import gettext as _ - from onadata.libs.utils.viewer_tools import get_client_ip -class Enum(object): +class Enum: + """Enum class - dict-like class""" + __name__ = "Enum" def __init__(self, **enums): @@ -19,7 +23,7 @@ def __getitem__(self, item): return self.__getattr__(item) def __iter__(self): - return self.enums.itervalues() + return iter(self.enums.values()) Actions = Enum( @@ -60,43 +64,44 @@ def __iter__(self): class AuditLogHandler(logging.Handler): + """Audit logging handler class.""" def __init__(self, model=""): - super(AuditLogHandler, self).__init__() + super().__init__() self.model_name = model def _format(self, record): created_on = datetime.utcfromtimestamp(record.created).isoformat() created_on = created_on[:23] + created_on[26:] data = { - 'action': record.formhub_action, - 'user': record.request_username, - 'account': record.account_username, - 'audit': {}, - 'msg': record.msg, + "action": record.formhub_action, + "user": record.request_username, + "account": record.account_username, + "audit": {}, + "msg": record.msg, # save as python datetime object # to have mongo convert to ISO date and allow queries - 'created_on': created_on, - 'levelno': record.levelno, - 'levelname': record.levelname, - 'args': record.args, - 'funcName': record.funcName, - 'msecs': record.msecs, - 'relativeCreated': record.relativeCreated, - 'thread': record.thread, - 'name': record.name, - 'threadName': record.threadName, - 'exc_info': record.exc_info, - 'pathname': record.pathname, - 'exc_text': record.exc_text, - 'lineno': record.lineno, - 'process': record.process, - 'filename': record.filename, - 'module': record.module, - 'processName': record.processName + "created_on": created_on, + "levelno": record.levelno, + "levelname": record.levelname, + "args": record.args, + "funcName": record.funcName, + "msecs": record.msecs, + "relativeCreated": record.relativeCreated, + "thread": record.thread, + "name": record.name, + "threadName": record.threadName, + "exc_info": record.exc_info, + "pathname": record.pathname, + "exc_text": record.exc_text, + "lineno": record.lineno, + "process": record.process, + "filename": record.filename, + "module": record.module, + "processName": record.processName, } - if hasattr(record, 'audit') and isinstance(record.audit, dict): - data['audit'] = record.audit + if hasattr(record, "audit") and isinstance(record.audit, dict): + data["audit"] = record.audit return data @@ -105,21 +110,24 @@ def emit(self, record): # save to mongodb audit_log try: model = self.get_model(self.model_name) - except Exception as e: - logging.exception(_(u'Get model threw exception: %s' % str(e))) + except Exception as e: # pylint: disable=broad-except + logging.exception("Get model threw exception: %s", str(e)) else: log_entry = model(data) log_entry.save() def get_model(self, name): - names = name.split('.') - mod = __import__('.'.join(names[:-1]), fromlist=names[-1:]) + """Import and return the model under the given ``name``.""" + names = name.split(".") + mod = __import__(".".join(names[:-1]), fromlist=names[-1:]) return getattr(mod, names[-1]) -def audit_log(action, request_user, account_user, message, audit, request, - level=logging.DEBUG): +# pylint: disable=too-many-arguments +def audit_log( + action, request_user, account_user, message, audit, request, level=logging.DEBUG +): """ Create a log message based on these params @@ -135,12 +143,14 @@ def audit_log(action, request_user, account_user, message, audit, request, """ logger = logging.getLogger("audit_logger") extra = { - 'formhub_action': action, - 'request_username': - request_user.username if request_user.username else str(request_user), - 'account_username': - account_user.username if account_user.username else str(account_user), - 'client_ip': get_client_ip(request), - 'audit': audit + "formhub_action": action, + "request_username": request_user.username + if request_user.username + else str(request_user), + "account_username": account_user.username + if account_user.username + else str(account_user), + "client_ip": get_client_ip(request), + "audit": audit, } logger.log(level, message, extra=extra) diff --git a/onadata/libs/utils/logger_tools.py b/onadata/libs/utils/logger_tools.py index 928cd8cd12..0f0b5a10d3 100644 --- a/onadata/libs/utils/logger_tools.py +++ b/onadata/libs/utils/logger_tools.py @@ -78,7 +78,7 @@ from onadata.apps.viewer.models.data_dictionary import DataDictionary from onadata.apps.viewer.models.parsed_instance import ParsedInstance from onadata.apps.viewer.signals import process_submission -from onadata.libs.utils.analytics import track_object_event +from onadata.libs.utils.analytics import TrackObjectEvent from onadata.libs.utils.common_tags import METADATA_FIELDS from onadata.libs.utils.common_tools import get_uuid, report_exception from onadata.libs.utils.model_tools import set_uuid @@ -654,11 +654,11 @@ def safe_create_instance( # noqa C901 error = OpenRosaResponseBadRequest(e) except DjangoUnicodeDecodeError: error = OpenRosaResponseBadRequest( - _("File likely corrupted during " "transmission, please try later.") + _("File likely corrupted during transmission, please try later.") ) except NonUniqueFormIdError: error = OpenRosaResponseBadRequest( - _("Unable to submit because there are multiple forms with" " this formID.") + _("Unable to submit because there are multiple forms with this formID.") ) except DataError as e: error = OpenRosaResponseBadRequest((str(e))) @@ -755,7 +755,7 @@ def publish_form(callback): return { "type": "alert-error", "text": _( - ("An error occurred while publishing the form. " "Please try again.") + ("An error occurred while publishing the form. Please try again.") ), } except (AttributeError, DuplicateUUIDError, ValidationError) as e: @@ -763,7 +763,7 @@ def publish_form(callback): return {"type": "alert-error", "text": text(e)} -@track_object_event( +@TrackObjectEvent( user_field="user", properties={"created_by": "user", "xform_id": "pk", "xform_name": "title"}, additional_context={"from": "Publish XLS Form"}, @@ -788,7 +788,7 @@ def publish_xls_form(xls_file, user, project, id_string=None, created_by=None): return dd -@track_object_event( +@TrackObjectEvent( user_field="user", properties={"created_by": "user", "xform_id": "pk", "xform_name": "title"}, additional_context={"from": "Publish XML Form"}, diff --git a/onadata/libs/utils/middleware.py b/onadata/libs/utils/middleware.py index 25d6eaa7d5..5eb11cd789 100644 --- a/onadata/libs/utils/middleware.py +++ b/onadata/libs/utils/middleware.py @@ -1,18 +1,25 @@ +# -*- coding: utf-8 -*- +""" +Custom middleware classes. +""" import logging import traceback +from sys import stdout from django.conf import settings -from django.db import connection -from django.db import OperationalError +from django.db import OperationalError, connection from django.http import HttpResponseNotAllowed -from django.template import loader from django.middleware.locale import LocaleMiddleware +from django.template import loader from django.utils.translation import gettext as _ from django.utils.translation.trans_real import parse_accept_lang_header + from multidb.pinning import use_master class BaseMiddleware: + """BaseMiddleware - The base middleware class.""" + def __init__(self, get_response): self.get_response = get_response @@ -20,23 +27,28 @@ def __call__(self, request): return self.get_response(request) -class ExceptionLoggingMiddleware(object): +class ExceptionLoggingMiddleware: + """The exception logging middleware class - prints the exception traceback.""" + def __init__(self, get_response): self.get_response = get_response + # pylint: disable=unused-argument def process_exception(self, request, exception): + """Prints the exception traceback.""" print(traceback.format_exc()) -class HTTPResponseNotAllowedMiddleware(object): +class HTTPResponseNotAllowedMiddleware: + """The HTTP Not Allowed middleware class - renders the 405.html template.""" + def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) if isinstance(response, HttpResponseNotAllowed): - response.content = loader.render_to_string( - "405.html", request=request) + response.content = loader.render_to_string("405.html", request=request) return response @@ -48,31 +60,39 @@ class LocaleMiddlewareWithTweaks(LocaleMiddleware): """ def process_request(self, request): - accept = request.headers.get('Accept-Language', '') + accept = request.headers.get("Accept-Language", "") try: codes = [code for code, r in parse_accept_lang_header(accept)] - if 'km' in codes and 'km-kh' not in codes: - request.META['HTTP_ACCEPT_LANGUAGE'] = accept.replace('km', - 'km-kh') - except Exception as e: + if "km" in codes and "km-kh" not in codes: + request.META["HTTP_ACCEPT_LANGUAGE"] = accept.replace("km", "km-kh") + except Exception as e: # pylint: disable=broad-except # this might fail if i18n is disabled. - logging.exception(_(u'Settings request META HTTP accept language ' - 'threw exceptions: %s' % str(e))) + logging.exception( + _( + "Settings request META HTTP accept language " + f"threw exceptions: {str(e)}" + ) + ) - super(LocaleMiddlewareWithTweaks, self).process_request(request) + super().process_request(request) -class SqlLogging(object): +class SqlLogging: + """ + SQL logging middleware. + """ + def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) - from sys import stdout + if stdout.isatty(): for query in connection.queries: - print("\033[1;31m[%s]\033[0m \033[1m%s\033[0m" % ( - query['time'], " ".join(query['sql'].split()))) + time = query["time"] + sql = " ".join(query["sql"].split()) + print(f"\033[1;31m[{time}]\033[0m \033[1m{sql}\033[0m") return response @@ -82,7 +102,11 @@ class OperationalErrorMiddleware(BaseMiddleware): Captures requests returning 500 status code. Then retry it against master database. """ + def process_exception(self, request, exception): + """ + Handle retrying OperatuonalError exceptions. + """ # Filter out OperationalError Exceptions if isinstance(exception, OperationalError): already_raised = getattr(settings, "ALREADY_RAISED", False) @@ -94,3 +118,5 @@ def process_exception(self, request, exception): response = self.get_response(request) return response settings.ALREADY_RAISED = False + + return None diff --git a/onadata/libs/utils/model_tools.py b/onadata/libs/utils/model_tools.py index a3ade05174..5f08d9741d 100644 --- a/onadata/libs/utils/model_tools.py +++ b/onadata/libs/utils/model_tools.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Model utility functions. """ diff --git a/onadata/libs/utils/mongo.py b/onadata/libs/utils/mongo.py index 34da61045e..1b2d1d9cae 100644 --- a/onadata/libs/utils/mongo.py +++ b/onadata/libs/utils/mongo.py @@ -1,18 +1,32 @@ +# -*- coding: utf-8 -*- +""" +Utility functions for MongoDB operations. +""" from __future__ import unicode_literals import base64 import re from functools import reduce - -key_whitelist = ['$or', '$and', '$exists', '$in', '$gt', '$gte', - '$lt', '$lte', '$regex', '$options', '$all'] -b64dollar = base64.b64encode(b'$').decode('utf-8') -b64dot = base64.b64encode(b'.').decode('utf-8') -re_b64dollar = re.compile(r'^%s' % b64dollar) -re_b64dot = re.compile(r'%s' % b64dot) -re_dollar = re.compile(r'^\$') -re_dot = re.compile(r'\.') +key_whitelist = [ + "$or", + "$and", + "$exists", + "$in", + "$gt", + "$gte", + "$lt", + "$lte", + "$regex", + "$options", + "$all", +] +b64dollar = base64.b64encode(b"$").decode("utf-8") +b64dot = base64.b64encode(b".").decode("utf-8") +re_b64dollar = re.compile(rf"^{b64dollar}") +re_b64dot = re.compile(rf"{b64dot}") +re_dollar = re.compile(r"^\$") +re_dot = re.compile(r"\.") def _pattern_transform(key, transform_list): @@ -20,7 +34,7 @@ def _pattern_transform(key, transform_list): def _decode_from_mongo(key): - return _pattern_transform(key, [(re_b64dollar, '$'), (re_b64dot, '.')]) + return _pattern_transform(key, [(re_b64dollar, "$"), (re_b64dot, ".")]) def _encode_for_mongo(key): @@ -28,5 +42,4 @@ def _encode_for_mongo(key): def _is_invalid_for_mongo(key): - return key not in\ - key_whitelist and (key.startswith('$') or key.count('.') > 0) + return key not in key_whitelist and (key.startswith("$") or key.count(".") > 0) diff --git a/onadata/libs/utils/numeric.py b/onadata/libs/utils/numeric.py index 9ba9a6635a..c376e43020 100644 --- a/onadata/libs/utils/numeric.py +++ b/onadata/libs/utils/numeric.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +The int_or_parse_error utility function. +""" from rest_framework.exceptions import ParseError @@ -8,5 +12,5 @@ def int_or_parse_error(value, error_string): """ try: int(value) - except ValueError: - raise ParseError(error_string) + except ValueError as exc: + raise ParseError(error_string) from exc diff --git a/onadata/libs/utils/openid_connect_tools.py b/onadata/libs/utils/openid_connect_tools.py index 73c8f48046..1ecdb6e729 100644 --- a/onadata/libs/utils/openid_connect_tools.py +++ b/onadata/libs/utils/openid_connect_tools.py @@ -1,21 +1,22 @@ +# -*- coding: utf-8 -*- """ OpenID Connect Tools """ import json -from django.http import HttpResponseRedirect, Http404 from django.core.cache import cache +from django.http import Http404, HttpResponseRedirect from django.utils.translation import gettext as _ import jwt import requests from jwt.algorithms import RSAAlgorithm -EMAIL = 'email' -NAME = 'name' -FIRST_NAME = 'given_name' -LAST_NAME = 'family_name' -NONCE = 'nonce' +EMAIL = "email" +NAME = "name" +FIRST_NAME = "given_name" +LAST_NAME = "family_name" +NONCE = "nonce" class OpenIDHandler: @@ -26,60 +27,54 @@ class OpenIDHandler: 'code' or 'id_token' authorization flow """ - def __init__( - self, - provider_configuration: dict - ): + def __init__(self, provider_configuration: dict): """ Initializes a OpenIDHandler Object to handle all OpenID Connect grant flows """ self.provider_configuration = provider_configuration - self.client_id = provider_configuration.get('client_id') - self.client_secret = provider_configuration.get('client_secret') + self.client_id = provider_configuration.get("client_id") + self.client_secret = provider_configuration.get("client_secret") def make_login_request(self, nonce: int, state=None): """ Makes a login request to the "authorization_endpoint" listed in the provider_configuration """ - if 'authorization_endpoint' in self.provider_configuration: - url = self.provider_configuration['authorization_endpoint'] - url += f'?nonce={nonce}' + if "authorization_endpoint" in self.provider_configuration: + url = self.provider_configuration["authorization_endpoint"] + url += f"?nonce={nonce}" if state: - url += f'&state={state}' + url += f"&state={state}" else: raise ValueError( - 'authorization_endpoint not found in provider configuration') + "authorization_endpoint not found in provider configuration" + ) - if 'client_id' in self.provider_configuration: - url += '&client_id=' + self.provider_configuration['client_id'] + if "client_id" in self.provider_configuration: + url += "&client_id=" + self.provider_configuration["client_id"] else: - raise ValueError('client_id not found in provider configuration') + raise ValueError("client_id not found in provider configuration") - if 'callback_uri' in self.provider_configuration: - url += '&redirect_uri=' + self.provider_configuration[ - 'callback_uri'] + if "callback_uri" in self.provider_configuration: + url += "&redirect_uri=" + self.provider_configuration["callback_uri"] else: - raise ValueError('client_id not found in provider configuration') + raise ValueError("client_id not found in provider configuration") - if 'scope' in self.provider_configuration: - url += '&scope=' + self.provider_configuration['scope'] + if "scope" in self.provider_configuration: + url += "&scope=" + self.provider_configuration["scope"] else: - raise ValueError('scope not found in provider configuration') + raise ValueError("scope not found in provider configuration") - if 'response_type' in self.provider_configuration: - url += '&response_type=' + self.provider_configuration[ - 'response_type'] + if "response_type" in self.provider_configuration: + url += "&response_type=" + self.provider_configuration["response_type"] else: - raise ValueError( - 'response_type not found in provider configuration') + raise ValueError("response_type not found in provider configuration") - if 'response_mode' in self.provider_configuration: - url += '&response_mode=' + self.provider_configuration[ - 'response_mode'] + if "response_mode" in self.provider_configuration: + url += "&response_mode=" + self.provider_configuration["response_mode"] return HttpResponseRedirect(url) @@ -94,7 +89,7 @@ def get_claim_values(self, claim_list: list, decoded_token: dict): decoded_token: A dict containing the decoded values of an ID Token """ claim_values = {} - claim_names = self.provider_configuration.get('claims') + claim_names = self.provider_configuration.get("claims") for claim in claim_list: claim_name = claim @@ -113,51 +108,54 @@ def _retrieve_jwk_related_to_kid(self, kid): from the JSON Web Key Set Endpoint """ if "jwks_endpoint" not in self.provider_configuration: - raise ValueError( - "jwks_endpoint not found in provider configuration") + raise ValueError("jwks_endpoint not found in provider configuration") - response = requests.get(self.provider_configuration['jwks_endpoint']) + response = requests.get(self.provider_configuration["jwks_endpoint"]) if response.status_code == 200: jwks = response.json() - for jwk in jwks.get('keys'): - if jwk.get('kid') == kid: + for jwk in jwks.get("keys"): + if jwk.get("kid") == kid: return jwk - def obtain_id_token_from_code(self, code: str, openid_provider: str = ''): + return None + + def obtain_id_token_from_code(self, code: str, openid_provider: str = ""): """ Obtain an ID Token using the Authorization Code flow """ - headers = {'Content-Type': 'application/x-www-form-urlencoded'} + headers = {"Content-Type": "application/x-www-form-urlencoded"} payload = { - 'grant_type': 'authorization_code', - 'code': code, - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'redirect_uri': self.provider_configuration.get('callback_uri') + "grant_type": "authorization_code", + "code": code, + "client_id": self.client_id, + "client_secret": self.client_secret, + "redirect_uri": self.provider_configuration.get("callback_uri"), } if "token_endpoint" not in self.provider_configuration: raise ValueError("token_endpoint not in provider configuration") response = requests.post( - self.provider_configuration['token_endpoint'], + self.provider_configuration["token_endpoint"], params=payload, - headers=headers) + headers=headers, + ) if response.status_code == 200: - id_token = response.json().get('id_token') + id_token = response.json().get("id_token") return id_token - else: - retry_message = 'Failed to retrieve ID Token, ' + \ - f'retry' + \ - 'the authentication process' - raise Http404(_(retry_message)) + + retry_message = ( + "Failed to retrieve ID Token, " + + f'retry' + + "the authentication process" + ) + raise Http404(_(retry_message)) def verify_and_decode_id_token( - self, id_token: str, - cached_nonce: bool = False, - openid_provider: str = ''): + self, id_token: str, cached_nonce: bool = False, openid_provider: str = "" + ): """ Verifies that the ID Token passed was signed and sent by the Open ID Connect Provider and that the client is one of the audiences then @@ -166,46 +164,44 @@ def verify_and_decode_id_token( unverified_header = jwt.get_unverified_header(id_token) # Get public key thumbprint - kid = unverified_header.get('kid') + kid = unverified_header.get("kid") jwk = self._retrieve_jwk_related_to_kid(kid) if jwk: - alg = unverified_header.get('alg') + alg = unverified_header.get("alg") public_key = RSAAlgorithm.from_jwk(json.dumps(jwk)) - try: - decoded_token = jwt.decode( - id_token, - public_key, - audience=[self.client_id], - algorithms=alg) - - if cached_nonce: - # Verify that the cached nonce is present and that - # the provider the nonce was initiated for, is the same - # provider returning it - provider_initiated_for = cache.get( - decoded_token.get(NONCE)) - - if provider_initiated_for != openid_provider: - raise Exception('Incorrect nonce value returned') - return decoded_token - except Exception as e: - raise e + decoded_token = jwt.decode( + id_token, public_key, audience=[self.client_id], algorithms=alg + ) + + if cached_nonce: + # Verify that the cached nonce is present and that + # the provider the nonce was initiated for, is the same + # provider returning it + provider_initiated_for = cache.get(decoded_token.get(NONCE)) + + if provider_initiated_for != openid_provider: + raise Exception("Incorrect nonce value returned") + return decoded_token + + return None def end_openid_provider_session(self): """ Clears the SSO cookie set at authentication and redirects the User to the end_session endpoint provided by the provider configuration """ - end_session_endpoint = self.provider_configuration.get( - 'end_session_endpoint') + end_session_endpoint = self.provider_configuration.get("end_session_endpoint") target_url_after_logout = self.provider_configuration.get( - 'target_url_after_logout') + "target_url_after_logout" + ) response = HttpResponseRedirect( - end_session_endpoint + - '?post_logout_redirect_uri=' + target_url_after_logout) - response.delete_cookie('SSO') + end_session_endpoint + + "?post_logout_redirect_uri=" + + target_url_after_logout + ) + response.delete_cookie("SSO") return response diff --git a/onadata/libs/utils/osm.py b/onadata/libs/utils/osm.py index cfad61280e..fc721fb9b5 100644 --- a/onadata/libs/utils/osm.py +++ b/onadata/libs/utils/osm.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ OSM utility module. """ @@ -17,7 +17,7 @@ from onadata.apps.logger.models.instance import Instance from onadata.apps.logger.models.osmdata import OsmData from onadata.apps.restservice.signals import trigger_webhook -from onadata.celery import app +from onadata.celeryapp import app def _get_xml_obj(xml): @@ -25,8 +25,8 @@ def _get_xml_obj(xml): xml = xml.strip().encode() try: return fromstring(xml) - except _etree.XMLSyntaxError as e: # pylint: disable=no-member - if "Attribute action redefined" in e.msg: + except _etree.XMLSyntaxError as e: # pylint: disable=c-extension-no-member + if "Attribute action redefined" in str(e): xml = xml.replace(b'action="modify" ', b"") return _get_xml_obj(xml) diff --git a/onadata/libs/utils/profiler.py b/onadata/libs/utils/profiler.py deleted file mode 100644 index 296f7c3667..0000000000 --- a/onadata/libs/utils/profiler.py +++ /dev/null @@ -1,49 +0,0 @@ -import hotshot -import os -import time -from django.conf import settings -import tempfile - -if hasattr(settings, 'PROFILE_LOG_BASE'): - PROFILE_LOG_BASE = settings.PROFILE_LOG_BASE -else: - PROFILE_LOG_BASE = tempfile.gettempdir() - - -def profile(log_file): - """Profile some callable. - - This decorator uses the hotshot profiler to profile some callable (like - a view function or method) and dumps the profile data somewhere sensible - for later processing and examination. - - It takes one argument, the profile log name. If it's a relative path, - it places it under the PROFILE_LOG_BASE. It also inserts a time stamp - into the file name, such that 'my_view.prof' become - 'my_view-20100211T170321.prof', where the time stamp is in UTC. This - makes it easy to run and compare multiple trials. - """ - - if not os.path.isabs(log_file): - log_file = os.path.join(PROFILE_LOG_BASE, log_file) - - def _outer(f): - if not settings.PROFILE_API_ACTION_FUNCTION: - return f - - def _inner(*args, **kwargs): - # Add a timestamp to the profile output when the callable - # is actually called. - (base, ext) = os.path.splitext(log_file) - base = base + "-" + time.strftime("%Y%m%dT%H%M%S", time.gmtime()) - final_log_file = base + ext - - prof = hotshot.Profile(final_log_file) - try: - ret = prof.runcall(f, *args, **kwargs) - finally: - prof.close() - return ret - - return _inner - return _outer diff --git a/onadata/libs/utils/project_utils.py b/onadata/libs/utils/project_utils.py index 234a53f360..60c3e3d1dc 100644 --- a/onadata/libs/utils/project_utils.py +++ b/onadata/libs/utils/project_utils.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ project_utils module - apply project permissions to a form. """ @@ -7,16 +7,24 @@ from django.conf import settings from django.db import IntegrityError -from onadata.apps.logger.models import Project, XForm -from onadata.celery import app +from multidb.pinning import use_master + +from onadata.apps.logger.models.project import Project +from onadata.apps.logger.models.xform import XForm +from onadata.celeryapp import app from onadata.libs.permissions import ( - ROLES, OwnerRole, get_object_users_with_permissions, - is_organization, get_role) + ROLES, + OwnerRole, + get_object_users_with_permissions, + get_role, + is_organization, +) from onadata.libs.utils.common_tags import OWNER_TEAM_NAME from onadata.libs.utils.common_tools import report_exception def get_project_users(project): + """Return project users with the role assigned to them.""" ret = {} for perm in project.projectuserobjectpermission_set.all(): @@ -24,17 +32,17 @@ def get_project_users(project): user = perm.user ret[user.username] = { - 'permissions': [], - 'is_org': is_organization(user.profile), - 'first_name': user.first_name, - 'last_name': user.last_name, + "permissions": [], + "is_org": is_organization(user.profile), + "first_name": user.first_name, + "last_name": user.last_name, } - ret[perm.user.username]['permissions'].append(perm.permission.codename) + ret[perm.user.username]["permissions"].append(perm.permission.codename) - for user in ret.keys(): - ret[user]['role'] = get_role(ret[user]['permissions'], project) - del ret[user]['permissions'] + for user, val in ret.items(): + val["role"] = get_role(val["permissions"], project) + del val["permissions"] return ret @@ -53,26 +61,24 @@ def set_project_perms_to_xform(xform, project): xform.save() # clear existing permissions - for perm in get_object_users_with_permissions( - xform, with_group_users=True): - user = perm['user'] - role_name = perm['role'] + for perm in get_object_users_with_permissions(xform, with_group_users=True): + user = perm["user"] + role_name = perm["role"] role = ROLES.get(role_name) - if role and (user != xform.user and project.user != user and - project.created_by != user): + if role and (user not in (xform.user, project.user, project.created_by)): role.remove_obj_permissions(user, xform) owners = project.organization.team_set.filter( - name="{}#{}".format(project.organization.username, OWNER_TEAM_NAME), - organization=project.organization) + name=f"{project.organization.username}#{OWNER_TEAM_NAME}", + organization=project.organization, + ) if owners: OwnerRole.add(owners[0], xform) - for perm in get_object_users_with_permissions( - project, with_group_users=True): - user = perm['user'] - role_name = perm['role'] + for perm in get_object_users_with_permissions(project, with_group_users=True): + user = perm["user"] + role_name = perm["role"] role = ROLES.get(role_name) if user == xform.created_by: @@ -88,13 +94,16 @@ def set_project_perms_to_xform_async(self, xform_id, project_id): """ Apply project permissions for ``project_id`` to a form ``xform_id`` task. """ + def _set_project_perms(): try: xform = XForm.objects.get(id=xform_id) project = Project.objects.get(id=project_id) except (Project.DoesNotExist, XForm.DoesNotExist) as e: - msg = '%s: Setting project %d permissions to form %d failed.' % ( - type(e), project_id, xform_id) + msg = ( + f"{type(e)}: Setting project {project_id} permissions to " + f"form {xform_id} failed." + ) # make a report only on the 3rd try. if self.request.retries > 2: report_exception(msg, e, sys.exc_info()) @@ -103,8 +112,8 @@ def _set_project_perms(): set_project_perms_to_xform(xform, project) try: - if getattr(settings, 'SLAVE_DATABASES', []): - from multidb.pinning import use_master + if getattr(settings, "SLAVE_DATABASES", []): + with use_master: _set_project_perms() else: @@ -112,8 +121,10 @@ def _set_project_perms(): except (Project.DoesNotExist, XForm.DoesNotExist) as e: # make a report only on the 3rd try. if self.request.retries > 2: - msg = '%s: Setting project %d permissions to form %d failed.' % ( - type(e), project_id, xform_id) + msg = ( + f"{type(e)}: Setting project {project_id} permissions to " + f"form {xform_id} failed." + ) report_exception(msg, e, sys.exc_info()) # let's retry if the record may still not be available in read replica. self.retry(countdown=60 * self.request.retries) @@ -122,6 +133,8 @@ def _set_project_perms(): # already. pass except Exception as e: # pylint: disable=broad-except - msg = '%s: Setting project %d permissions to form %d failed.' % ( - type(e), project_id, xform_id) + msg = ( + f"{type(e)}: Setting project {project_id} permissions to " + f"form {xform_id} failed." + ) report_exception(msg, e, sys.exc_info()) diff --git a/onadata/libs/utils/quick_converter.py b/onadata/libs/utils/quick_converter.py index 8923c3be36..d57b682d32 100644 --- a/onadata/libs/utils/quick_converter.py +++ b/onadata/libs/utils/quick_converter.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +The QuickConverter form class - publishes XLSForms. +""" from django import forms from django.utils.translation import gettext_lazy @@ -5,10 +9,17 @@ class QuickConverter(forms.Form): + """ + The QuickConverter form - publishes XLSForms. + """ + xls_file = forms.FileField(label=gettext_lazy("XLS File")) def publish(self, user): + """Create and return a DataDictionary object.""" if self.is_valid(): return DataDictionary.objects.create( - user=user, - xls=self.cleaned_data['xls_file']) + user=user, xls=self.cleaned_data["xls_file"] + ) + + return None diff --git a/onadata/libs/utils/timing.py b/onadata/libs/utils/timing.py index 88143e7b75..c2e4209bc8 100644 --- a/onadata/libs/utils/timing.py +++ b/onadata/libs/utils/timing.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Custom date utility functions. +""" import datetime import six @@ -5,11 +9,16 @@ def get_header_date_format(date_modified): - format = "%a, %d %b %Y %H:%M:%S GMT" - return date_modified.strftime(format) + """ + Returns ``date_modified`` as a string with the format %a, %d %b %Y %H:%M:%S GMT + """ + return date_modified.strftime("%a, %d %b %Y %H:%M:%S GMT") def get_date(_object=None): + """ + Returns a date formatted string of the date_modified of the given ``_object``. + """ if hasattr(_object, "date_modified"): _date = _object.date_modified elif hasattr(_object, "instance"): @@ -20,9 +29,9 @@ def get_date(_object=None): _date = _object.profile.date_modified elif isinstance(_object, dict): # most likely an instance json, use _submission_time - _date = _object.get('_submission_time') + _date = _object.get("_submission_time") if isinstance(_date, six.string_types): - _date = datetime.datetime.strptime(_date[:19], '%Y-%m-%dT%H:%M:%S') + _date = datetime.datetime.strptime(_date[:19], "%Y-%m-%dT%H:%M:%S") else: # default value to avoid the UnboundLocalError _date = timezone.now() @@ -31,7 +40,8 @@ def get_date(_object=None): def last_modified_header(last_modified_date): - return {'Last-Modified': last_modified_date} + """Returns a dictionary with the 'Last-Modified' key and value.""" + return {"Last-Modified": last_modified_date} def calculate_duration(start_time, end_time): @@ -45,7 +55,7 @@ def calculate_duration(start_time, end_time): _start = datetime.datetime.strptime(start_time[:19], _format) _end = datetime.datetime.strptime(end_time[:19], _format) except (TypeError, ValueError): - return '' + return "" duration = (_end - _start).total_seconds() diff --git a/onadata/libs/utils/user_auth.py b/onadata/libs/utils/user_auth.py index 425ac4fe1d..d7b55dc428 100644 --- a/onadata/libs/utils/user_auth.py +++ b/onadata/libs/utils/user_auth.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ User authentication utility functions. """ @@ -6,6 +6,7 @@ import re from functools import wraps +from django.apps import apps from django.contrib.auth import authenticate, get_user_model from django.contrib.sites.models import Site from django.http import HttpResponse @@ -14,13 +15,18 @@ from guardian.shortcuts import assign_perm, get_perms_for_model from rest_framework.authtoken.models import Token -from onadata.apps.api.models import OrganizationProfile, Team, TempToken -from onadata.apps.logger.models import MergedXForm, Note, Project, XForm -from onadata.apps.main.models import UserProfile +from onadata.apps.api.models.team import Team +from onadata.apps.api.models.temp_token import TempToken +from onadata.apps.logger.models.note import Note +from onadata.apps.logger.models.project import Project +from onadata.apps.logger.models.xform import XForm from onadata.libs.utils.viewer_tools import get_form # pylint: disable=invalid-name User = get_user_model() +UserProfile = apps.get_model("main", "UserProfile") +OrganizationProfile = apps.get_model("api", "OrganizationProfile") +MergedXForm = apps.get_model("logger", "MergedXForm") class HttpResponseNotAuthorized(HttpResponse): diff --git a/onadata/settings/common.py b/onadata/settings/common.py index 83ef6581c3..2a1e2f1773 100644 --- a/onadata/settings/common.py +++ b/onadata/settings/common.py @@ -242,9 +242,10 @@ "USE_RAPIDPRO_VIEWSET": False, } +MSFT_OAUTH_ENDPOINT = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" OPENID_CONNECT_AUTH_SERVERS = { "microsoft": { - "AUTHORIZATION_ENDPOINT": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + "AUTHORIZATION_ENDPOINT": MSFT_OAUTH_ENDPOINT, "CLIENT_ID": "client_id", "JWKS_ENDPOINT": "https://login.microsoftonline.com/common/discovery/v2.0/keys", "SCOPE": "openid profile", @@ -257,10 +258,11 @@ } } +DEFAULT_MODEL_SERIALIZER_CLASS = "rest_framework.serializers.HyperlinkedModelSerializer" REST_FRAMEWORK = { # Use hyperlinked styles by default. # Only used if the `serializer_class` attribute is not set on a view. - "DEFAULT_MODEL_SERIALIZER_CLASS": "rest_framework.serializers.HyperlinkedModelSerializer", + "DEFAULT_MODEL_SERIALIZER_CLASS": DEFAULT_MODEL_SERIALIZER_CLASS, # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. "DEFAULT_PERMISSION_CLASSES": [ @@ -439,7 +441,7 @@ def configure_logging(logger, **kwargs): GOOGLE_STEP2_URI = "http://ona.io/gwelcome" GOOGLE_OAUTH2_CLIENT_ID = "REPLACE ME" -GOOGLE_OAUTH2_CLIENT_SECRET = "REPLACE ME" +GOOGLE_OAUTH2_CLIENT_SECRET = "REPLACE ME" # noqa THUMB_CONF = { "large": {"size": 1280, "suffix": "-large"}, @@ -578,7 +580,7 @@ def configure_logging(logger, **kwargs): # Prevents "The number of GET/POST parameters exceeded" exception DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000000 -SECRET_KEY = "mlfs33^s1l4xf6a36$0#j%dd*sisfoi&)&4s-v=91#^l01v)*j" +SECRET_KEY = "mlfs33^s1l4xf6a36$0#j%dd*sisfoi&)&4s-v=91#^l01v)*j" # noqa # Time in minutes to lock out user from account LOCKOUT_TIME = 30 * 60 diff --git a/onadata/settings/default_settings.py b/onadata/settings/default_settings.py index 16669f7741..409ff48a47 100644 --- a/onadata/settings/default_settings.py +++ b/onadata/settings/default_settings.py @@ -31,4 +31,4 @@ SLAVE_DATABASES = [] # Make a unique unique key just for testing, and don't share it with anybody. -SECRET_KEY = "mlfs33^s1l4xf6a36$0#j%dd*sisfoi&)&4s-v=91#^l01v)*j" +SECRET_KEY = "mlfs33^s1l4xf6a36$0#j%dd*sisfoi&)&4s-v=91#^l01v)*j" # noqa diff --git a/onadata/settings/docker.py b/onadata/settings/docker.py index 558b73635a..b53ea61909 100644 --- a/onadata/settings/docker.py +++ b/onadata/settings/docker.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Example local_settings.py used by the Dockerfile. """ @@ -33,7 +33,7 @@ SLAVE_DATABASES = [] # Make a unique unique key just for testing, and don't share it with anybody. -SECRET_KEY = "~&nN9d`bxmJL2[$HhYE9qAk=+4P:cf3b" +SECRET_KEY = "~&nN9d`bxmJL2[$HhYE9qAk=+4P:cf3b" # noqa ALLOWED_HOSTS = ["127.0.0.1", "localhost"] diff --git a/onadata/settings/drone_test.py b/onadata/settings/drone_test.py index b611bab2cd..31d3bb96bd 100644 --- a/onadata/settings/drone_test.py +++ b/onadata/settings/drone_test.py @@ -1,4 +1,4 @@ -# -*- coding:utf-8 -*- +# -*- coding: utf-8 -*- """ Example local_settings.py for use with DroneCI. """ diff --git a/onadata/settings/production_example.py b/onadata/settings/production_example.py index ea57dcf820..aed2573692 100644 --- a/onadata/settings/production_example.py +++ b/onadata/settings/production_example.py @@ -1,4 +1,4 @@ -# -*- coding=utf-8 -*- +# -*- coding: utf-8 -*- """ Example local_settings.py used by the Dockerfile. """ diff --git a/onadata/settings/staging_example.py b/onadata/settings/staging_example.py index 9436355644..d46fd4d8bd 100644 --- a/onadata/settings/staging_example.py +++ b/onadata/settings/staging_example.py @@ -27,7 +27,7 @@ # TIME_ZONE = 'UTC' -SECRET_KEY = "please replace this text" +SECRET_KEY = "please replace this text" # noqa # This trick works only when we run tests from the command line. TESTING_MODE = len(sys.argv) >= 2 and (