From edfac975e41b6c19128b8d74b7a4796d6580dae6 Mon Sep 17 00:00:00 2001 From: Daniel Ostkamp <4895210+Iso5786@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:31:09 +0100 Subject: [PATCH] Added django-prometheus dependency and adjusted settings.py --- .../providers/surf_conext/views.py | 102 ++++----- apps/badgrsocialauth/redirect_urls.py | 1 - apps/directaward/admin.py | 31 ++- apps/lti13/models.py | 116 ++++++---- apps/mainsite/drf_fields.py | 33 +-- apps/mainsite/oidc_authentication.py | 32 ++- apps/mainsite/settings.py | 7 +- apps/mainsite/urls.py | 1 + apps/mainsite/utils.py | 201 +++++++++--------- apps/public/public_api.py | 145 ++++++------- requirements.txt | 1 + 11 files changed, 351 insertions(+), 319 deletions(-) diff --git a/apps/badgrsocialauth/providers/surf_conext/views.py b/apps/badgrsocialauth/providers/surf_conext/views.py index e887b726..0eb28456 100755 --- a/apps/badgrsocialauth/providers/surf_conext/views.py +++ b/apps/badgrsocialauth/providers/surf_conext/views.py @@ -30,7 +30,7 @@ from mainsite.models import BadgrApp from .provider import SurfConextProvider -logger = logging.getLogger("Badgr.Debug") +logger = logging.getLogger('Badgr.Debug') def login(request): @@ -42,15 +42,15 @@ def login(request): # state contains the data required for the redirect after the login via SURFconext, # it contains the user token, type of process and which badge_app - referer = request.headers["referer"].split("/")[3] - badgr_app_pk = request.session.get("badgr_app_pk", None) + referer = request.headers['referer'].split('/')[3] + badgr_app_pk = request.session.get('badgr_app_pk', None) try: badgr_app_pk = int(badgr_app_pk) except: badgr_app_pk = settings.BADGR_APP_ID state = json.dumps( [ - request.GET.get("process", "login"), + request.GET.get('process', 'login'), get_session_authcode(request), badgr_app_pk, referer, @@ -58,19 +58,19 @@ def login(request): ) params = { - "state": state, - "client_id": settings.SURF_CONEXT_CLIENT, - "response_type": "code", - "scope": "openid", - "redirect_uri": f"{settings.HTTP_ORIGIN}/account/openid/login/callback/", - "claims": '{"id_token":{"preferred_username":null,"given_name":null,' + 'state': state, + 'client_id': settings.SURF_CONEXT_CLIENT, + 'response_type': 'code', + 'scope': 'openid', + 'redirect_uri': f'{settings.HTTP_ORIGIN}/account/openid/login/callback/', + 'claims': '{"id_token":{"preferred_username":null,"given_name":null,' '"family_name":null,"email":null,"schac_home_organization":null}}', } - if request.GET.get("force_login") == "True": - params["prompt"] = "login" + if request.GET.get('force_login') == 'True': + params['prompt'] = 'login' args = urllib.parse.urlencode(params) # Redirect to eduID and enforce a linked SURFconext user with validated names - login_url = f"{settings.SURFCONEXT_DOMAIN_URL}/authorize?{args}" + login_url = f'{settings.SURFCONEXT_DOMAIN_URL}/authorize?{args}' return redirect(login_url) @@ -95,57 +95,52 @@ def callback(request): """ # extract the state of the redirect if request.user.is_authenticated: - get_account_adapter(request).logout( - request - ) # logging in while being authenticated breaks the login procedure + get_account_adapter(request).logout(request) # logging in while being authenticated breaks the login procedure - process, auth_token, badgr_app_pk, referer = json.loads(request.GET.get("state")) + process, auth_token, badgr_app_pk, referer = json.loads(request.GET.get('state')) - code = request.GET.get("code", None) + code = request.GET.get('code', None) if code is None: - error = "Server error: No userToken found in callback" + error = 'Server error: No userToken found in callback' return render_authentication_error(request, SurfConextProvider.id, error=error) # 1. Exchange callback Token for id token payload = { - "grant_type": "authorization_code", - "redirect_uri": f"{settings.HTTP_ORIGIN}/account/openid/login/callback/", - "code": code, - "scope": "openid", - "client_id": settings.SURF_CONEXT_CLIENT, - "client_secret": settings.SURF_CONEXT_SECRET, + 'grant_type': 'authorization_code', + 'redirect_uri': f'{settings.HTTP_ORIGIN}/account/openid/login/callback/', + 'code': code, + 'scope': 'openid', + 'client_id': settings.SURF_CONEXT_CLIENT, + 'client_secret': settings.SURF_CONEXT_SECRET, } headers = { - "Content-Type": "application/x-www-form-urlencoded", - "Cache-Control": "no-cache", + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cache-Control': 'no-cache', } response = requests.post( - f"{settings.SURFCONEXT_DOMAIN_URL}/token", + f'{settings.SURFCONEXT_DOMAIN_URL}/token', data=urllib.parse.urlencode(payload), headers=headers, ) if response.status_code != 200: - error = ( - "Server error: Token endpoint error (http %s) try alternative login methods" - % response.status_code - ) + error = 'Server error: Token endpoint error (http %s) try alternative login methods' % response.status_code return render_authentication_error(request, SurfConextProvider.id, error=error) data = response.json() - id_token = data.get("id_token", None) + id_token = data.get('id_token', None) if id_token is None: - error = "Server error: No id_token token, try alternative login methods." + error = 'Server error: No id_token token, try alternative login methods.' return render_authentication_error(request, SurfConextProvider.id, error=error) badgr_app = BadgrApp.objects.get(pk=badgr_app_pk) set_session_badgr_app(request, BadgrApp.objects.get(pk=badgr_app.pk)) payload = jwt.get_unverified_claims(id_token) - for attr in ["sub", "email", "schac_home_organization"]: + for attr in ['sub', 'email', 'schac_home_organization']: if attr not in payload: - error = f"Sorry, your account does not have a {attr} attribute. Login with SURFconext and then try again" + error = f'Sorry, your account does not have a {attr} attribute. Login with SURFconext and then try again' logger.error(error) return render_authentication_error(request, SurfConextProvider.id, error) @@ -153,16 +148,16 @@ def callback(request): provider = SurfConextProvider(request) login = provider.sociallogin_from_response(request, payload) # connect process in which OpenID connects with, either login or connect, if you connect then login user with token - login.state = {"process": process} + login.state = {'process': process} # login for connect because socialLogin can only connect to request.user - if process == "connect" and request.user.is_anonymous and auth_token: + if process == 'connect' and request.user.is_anonymous and auth_token: request.user = get_verified_user(auth_token=auth_token) ret = complete_social_login(request, login) - new_url = ret.url + "&role=teacher" + new_url = ret.url + '&role=teacher' ret = HttpResponseRedirect(new_url) if not request.user.is_anonymous: # the social login succeeded - institution_identifier = payload["schac_home_organization"] + institution_identifier = payload['schac_home_organization'] try: institution = Institution.objects.get(identifier=institution_identifier) # the institution exists @@ -188,20 +183,17 @@ def callback(request): ): # there is no provisionment extra_context = {} if institution.email: - extra_context["admin_email"] = institution.email + extra_context['admin_email'] = institution.email elif institution and institution.cached_staff(): cached_staff = institution.cached_staff() - admins = list( - filter(lambda u: u.may_administrate_users, cached_staff) - ) + admins = list(filter(lambda u: u.may_administrate_users, cached_staff)) if len(admins) > 0: - extra_context["admin_email"] = admins[0].user.email + extra_context['admin_email'] = admins[0].user.email - error = "Sorry, you can not register without an invite." - extra_context["code"] = AuthErrorCode.REGISTER_WITHOUT_INVITE + error = 'Sorry, you can not register without an invite.' + extra_context['code'] = AuthErrorCode.REGISTER_WITHOUT_INVITE if ( - request.user.date_joined.today().date() - == datetime.datetime.today().date() + request.user.date_joined.today().date() == datetime.datetime.today().date() ): # extra protection before deletion request.user.delete() return render_authentication_error( @@ -211,20 +203,18 @@ def callback(request): extra_context=extra_context, ) - except ( - Institution.DoesNotExist - ): # no institution yet, and therefore also first login ever - error = "Sorry, your institution has not been created yet." + except Institution.DoesNotExist: # no institution yet, and therefore also first login ever + error = 'Sorry, your institution has not been created yet.' return render_authentication_error( request, SurfConextProvider.id, error, - extra_context={"code": AuthErrorCode.REGISTER_WITHOUT_INVITE}, + extra_context={'code': AuthErrorCode.REGISTER_WITHOUT_INVITE}, ) request.user.accept_general_terms() # override the response with a redirect to staff dashboard if the login came from there - if referer == "staff": - return HttpResponseRedirect(reverse("admin:index")) + if referer == 'staff': + return HttpResponseRedirect(reverse('admin:index')) return ret diff --git a/apps/badgrsocialauth/redirect_urls.py b/apps/badgrsocialauth/redirect_urls.py index ed496b46..cccbdf8a 100644 --- a/apps/badgrsocialauth/redirect_urls.py +++ b/apps/badgrsocialauth/redirect_urls.py @@ -27,7 +27,6 @@ path('impersonate/', ImpersonateUser.as_view(), name='impersonate_user'), ] - for provider in providers.registry.get_list(): try: prov_mod = importlib.import_module(provider.get_package() + '.urls') diff --git a/apps/directaward/admin.py b/apps/directaward/admin.py index 3d7a8360..458d8eaa 100644 --- a/apps/directaward/admin.py +++ b/apps/directaward/admin.py @@ -7,11 +7,22 @@ class DirectAwardAdmin(admin.ModelAdmin): - list_display = ('id', 'recipient_email', 'eppn', 'created_at', admin_list_linkify('badgeclass', 'name'), - 'institution_identifier', 'status') + list_display = ( + 'id', + 'recipient_email', + 'eppn', + 'created_at', + admin_list_linkify('badgeclass', 'name'), + 'institution_identifier', + 'status', + ) list_display_links = ('recipient_email', 'eppn') list_filter = ('created_at', 'badgeclass__name', 'badgeclass__issuer__faculty__institution__identifier') - search_fields = ('recipient_email', 'eppn', 'badgeclass__name',) + search_fields = ( + 'recipient_email', + 'eppn', + 'badgeclass__name', + ) @admin.display( description='Institution', @@ -21,12 +32,17 @@ def institution_identifier(self, obj): return obj.badgeclass.issuer.faculty.institution.identifier - class DirectAwardBundleAdmin(admin.ModelAdmin): list_display = ( - 'assertion_count', 'direct_award_count', 'direct_award_rejected_count', - 'direct_award_scheduled_count','direct_award_revoked_count', - 'created_at', admin_list_linkify('badgeclass', 'name'), 'institution_identifier') + 'assertion_count', + 'direct_award_count', + 'direct_award_rejected_count', + 'direct_award_scheduled_count', + 'direct_award_revoked_count', + 'created_at', + admin_list_linkify('badgeclass', 'name'), + 'institution_identifier', + ) list_filter = ('created_at', 'badgeclass__issuer__faculty__institution__identifier') search_fields = ('badgeclass__name',) @@ -39,6 +55,5 @@ def institution_identifier(self, obj): return obj.badgeclass.issuer.faculty.institution.identifier - badgr_admin.register(DirectAward, DirectAwardAdmin) badgr_admin.register(DirectAwardBundle, DirectAwardBundleAdmin) diff --git a/apps/lti13/models.py b/apps/lti13/models.py index 6f1b5c35..9a7e239d 100644 --- a/apps/lti13/models.py +++ b/apps/lti13/models.py @@ -14,12 +14,14 @@ class LtiToolKey(models.Model): - name = models.CharField(max_length=255, null=False, blank=False, unique=True, help_text=_("Key name")) - private_key = models.TextField(null=False, blank=False, help_text=_("Tool's generated Private key. " - "Keep this value in secret")) + name = models.CharField(max_length=255, null=False, blank=False, unique=True, help_text=_('Key name')) + private_key = models.TextField( + null=False, blank=False, help_text=_("Tool's generated Private key. Keep this value in secret") + ) public_key = models.TextField(null=True, blank=True, help_text=_("Tool's generated Public key")) - public_jwk = models.TextField(null=True, blank=True, help_text=_("Tool's generated Public key (from the field" - " above) presented as JWK.")) + public_jwk = models.TextField( + null=True, blank=True, help_text=_("Tool's generated Public key (from the field above) presented as JWK.") + ) def save(self, *args, **kwargs): # pylint: disable=arguments-differ,signature-differs if self.public_key: @@ -34,43 +36,63 @@ def __str__(self): return '' % (self.id, self.name) class Meta(object): - db_table = "lti1p3_tool_key" - verbose_name = "lti 1.3 tool key" - verbose_name_plural = "lti 1.3 tool keys" + db_table = 'lti1p3_tool_key' + verbose_name = 'lti 1.3 tool key' + verbose_name_plural = 'lti 1.3 tool keys' class LtiTool(models.Model): title = models.CharField(max_length=255) description = models.TextField(blank=True, null=True, default=None) is_active = models.BooleanField(default=True) - issuer = models.CharField(max_length=255, - help_text=_("This will usually look something like 'http://example.com'. " - "Value provided by LTI 1.3 Platform")) + issuer = models.CharField( + max_length=255, + help_text=_("This will usually look something like 'http://example.com'. Value provided by LTI 1.3 Platform"), + ) institution = models.ForeignKey(Institution, on_delete=models.CASCADE, blank=False, null=False) - client_id = models.CharField(max_length=255, null=False, blank=False, - help_text=_("Value provided by LTI 1.3 Platform")) - use_by_default = models.BooleanField(default=False, help_text=_("This iss config will be used in case " - "if client-id was not passed")) - auth_login_url = models.CharField(max_length=1024, null=False, blank=False, - help_text=_("The platform's OIDC login endpoint. " - "Value provided by LTI 1.3 Platform"), - validators=[URLValidator()]) - auth_token_url = models.CharField(max_length=1024, null=False, blank=False, - help_text=_("The platform's service authorization " - "endpoint. Value provided by " - "LTI 1.3 Platform"), - validators=[URLValidator()]) - auth_audience = models.CharField(max_length=1024, null=True, blank=True, - help_text=_("The platform's OAuth2 Audience (aud). " - "Usually could be skipped")) - key_set_url = models.CharField(max_length=1024, null=True, blank=True, - help_text=_("The platform's JWKS endpoint. " - "Value provided by LTI 1.3 Platform"), - validators=[URLValidator()]) - key_set = models.TextField(null=True, blank=True, help_text=_("In case if platform's JWKS endpoint somehow " - "unavailable you may paste JWKS here. " - "Value provided by LTI 1.3 Platform")) - tool_key = models.ForeignKey(LtiToolKey, on_delete=models.PROTECT, related_name="lti_tools") + client_id = models.CharField( + max_length=255, null=False, blank=False, help_text=_('Value provided by LTI 1.3 Platform') + ) + use_by_default = models.BooleanField( + default=False, help_text=_('This iss config will be used in case if client-id was not passed') + ) + auth_login_url = models.CharField( + max_length=1024, + null=False, + blank=False, + help_text=_("The platform's OIDC login endpoint. Value provided by LTI 1.3 Platform"), + validators=[URLValidator()], + ) + auth_token_url = models.CharField( + max_length=1024, + null=False, + blank=False, + help_text=_("The platform's service authorization endpoint. Value provided by LTI 1.3 Platform"), + validators=[URLValidator()], + ) + auth_audience = models.CharField( + max_length=1024, + null=True, + blank=True, + help_text=_("The platform's OAuth2 Audience (aud). Usually could be skipped"), + ) + key_set_url = models.CharField( + max_length=1024, + null=True, + blank=True, + help_text=_("The platform's JWKS endpoint. Value provided by LTI 1.3 Platform"), + validators=[URLValidator()], + ) + key_set = models.TextField( + null=True, + blank=True, + help_text=_( + "In case if platform's JWKS endpoint somehow " + 'unavailable you may paste JWKS here. ' + 'Value provided by LTI 1.3 Platform' + ), + ) + tool_key = models.ForeignKey(LtiToolKey, on_delete=models.PROTECT, related_name='lti_tools') def clean(self): if not self.key_set_url and not self.key_set: @@ -89,14 +111,14 @@ def clean(self): def to_dict(self): data = { - "issuer": self.issuer, - "client_id": self.client_id, - "auth_login_url": self.auth_login_url, - "auth_token_url": self.auth_token_url, - "auth_audience": self.auth_audience, - "key_set_url": self.key_set_url, - "key_set": json.loads(self.key_set) if self.key_set else None, - "institution": self.institution.identifier + 'issuer': self.issuer, + 'client_id': self.client_id, + 'auth_login_url': self.auth_login_url, + 'auth_token_url': self.auth_token_url, + 'auth_audience': self.auth_audience, + 'key_set_url': self.key_set_url, + 'key_set': json.loads(self.key_set) if self.key_set else None, + 'institution': self.institution.identifier, } return data @@ -113,7 +135,7 @@ class LtiCourse(BaseAuditedModel, BaseVersionedEntity): identifier = models.CharField(max_length=255) title = models.CharField(max_length=255, null=False, blank=False) label = models.CharField(max_length=255, null=False, blank=False) - badgeclass = models.OneToOneField(BadgeClass, blank=False, null=False, on_delete=models.CASCADE, - related_name='lti_course') - tool = models.ForeignKey(LtiTool, blank=False, null=False, on_delete=models.CASCADE, - related_name='courses') + badgeclass = models.OneToOneField( + BadgeClass, blank=False, null=False, on_delete=models.CASCADE, related_name='lti_course' + ) + tool = models.ForeignKey(LtiTool, blank=False, null=False, on_delete=models.CASCADE, related_name='courses') diff --git a/apps/mainsite/drf_fields.py b/apps/mainsite/drf_fields.py index 59e6e64b..33d0f818 100644 --- a/apps/mainsite/drf_fields.py +++ b/apps/mainsite/drf_fields.py @@ -17,10 +17,7 @@ class Base64FileField(FileField): # mimetypes.guess_extension() may return different values for same mimetype, but we need one extension for one mime - _MIME_MAPPING = { - 'image/jpeg': '.jpg', - 'audio/wav': '.wav' - } + _MIME_MAPPING = {'image/jpeg': '.jpg', 'audio/wav': '.wav'} _ERROR_MESSAGE = _('Base64 string is incorrect') def to_internal_value(self, data): @@ -29,14 +26,18 @@ def to_internal_value(self, data): try: mime, encoded_data = data.replace('data:', '', 1).split(';base64,') - extension = self._MIME_MAPPING[mime] if mime in list( - self._MIME_MAPPING.keys()) else mimetypes.guess_extension(mime) - ret = ContentFile(base64.b64decode(encoded_data), name='{name}{extension}'.format(name=str(uuid.uuid4()), - extension=extension)) + extension = ( + self._MIME_MAPPING[mime] if mime in list(self._MIME_MAPPING.keys()) else mimetypes.guess_extension(mime) + ) + ret = ContentFile( + base64.b64decode(encoded_data), + name='{name}{extension}'.format(name=str(uuid.uuid4()), extension=extension), + ) filesize = sys.getsizeof(ret.file) if filesize > settings.MAX_IMAGE_UPLOAD_SIZE: raise ValidationError( - 'Image too large, max file size is {}'.format(settings.MAX_IMAGE_UPLOAD_SIZE_LABEL), 999) + 'Image too large, max file size is {}'.format(settings.MAX_IMAGE_UPLOAD_SIZE_LABEL), 999 + ) return ret except (ValueError, binascii.Error): return super(Base64FileField, self).to_internal_value(data) @@ -47,16 +48,18 @@ class ValidImageField(Base64FileField): def __init__(self, skip_http=True, allow_empty_file=False, use_url=True, allow_null=True, **kwargs): self.skip_http = skip_http - super(ValidImageField, self).__init__(allow_empty_file=allow_empty_file, - use_url=use_url, - allow_null=allow_null, - **kwargs) + super(ValidImageField, self).__init__( + allow_empty_file=allow_empty_file, use_url=use_url, allow_null=allow_null, **kwargs + ) def to_internal_value(self, data): # Skip http/https urls to avoid overwriting valid data when, for example, a client GETs and subsequently PUTs an # entity containing an image URL. - if self.skip_http and not isinstance(data, UploadedFile) and urllib.parse.urlparse(data).scheme in ( - 'http', 'https'): + if ( + self.skip_http + and not isinstance(data, UploadedFile) + and urllib.parse.urlparse(data).scheme in ('http', 'https') + ): raise SkipField() return super(ValidImageField, self).to_internal_value(data) diff --git a/apps/mainsite/oidc_authentication.py b/apps/mainsite/oidc_authentication.py index e76bb616..b2101833 100644 --- a/apps/mainsite/oidc_authentication.py +++ b/apps/mainsite/oidc_authentication.py @@ -13,43 +13,39 @@ class OIDCAuthentication(BaseAuthentication): - def authenticate(self, request): """ Returns two-tuple of (user, token) if authentication succeeds, or None otherwise. """ logger = logging.getLogger('Badgr.Debug') - x_requested_with = request.headers.get("x-requested-with") - if x_requested_with and x_requested_with.lower() == "client": - logger.info("Skipping OIDCAuthentication as HTTP_X_REQUESTED_WITH = client") + x_requested_with = request.headers.get('x-requested-with') + if x_requested_with and x_requested_with.lower() == 'client': + logger.info('Skipping OIDCAuthentication as HTTP_X_REQUESTED_WITH = client') return None - logger.info(f"OIDCAuthentication {request.META}") + logger.info(f'OIDCAuthentication {request.META}') authorization = request.environ.get('HTTP_AUTHORIZATION') if not authorization: - logger.info("OIDCAuthentication no authorization") + logger.info('OIDCAuthentication no authorization') return None - bearer_token = authorization[len('bearer '):] + bearer_token = authorization[len('bearer ') :] if not bearer_token: - logger.info("OIDCAuthentication no bearer_token") + logger.info('OIDCAuthentication no bearer_token') return None payload = {'token': bearer_token} headers = {'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded'} - url = f"{settings.EDUID_PROVIDER_URL}/introspect" + url = f'{settings.EDUID_PROVIDER_URL}/introspect' auth = (settings.OIDC_RS_ENTITY_ID, settings.OIDC_RS_SECRET) - response = requests.post(url, - data=urllib.parse.urlencode(payload), - auth=auth, - headers=headers) + response = requests.post(url, data=urllib.parse.urlencode(payload), auth=auth, headers=headers) if response.status_code != 200: - logger.info(f"OIDCAuthentication bad response {response.status_code} {response.json()}") + logger.info(f'OIDCAuthentication bad response {response.status_code} {response.json()}') return None introspect_json = response.json() - logger.info(f"OIDCAuthentication introspect {introspect_json}") + logger.info(f'OIDCAuthentication introspect {introspect_json}') if not introspect_json['active']: return None @@ -61,14 +57,14 @@ def authenticate(self, request): try: institution = Institution.objects.get(manage_client_id=client_id) except Institution.DoesNotExist: - raise BadRequest(f"Institution with manage_client_id {client_id} does not exists") + raise BadRequest(f'Institution with manage_client_id {client_id} does not exists') - logger.info(f"OIDCAuthentication institution {institution} client_id {client_id}") + logger.info(f'OIDCAuthentication institution {institution} client_id {client_id}') if institution and institution.sis_integration_enabled: user = institution.sis_default_user else: - raise BadRequest(f"Institution {institution.identifier} is not sis_integration_enabled") + raise BadRequest(f'Institution {institution.identifier} is not sis_integration_enabled') if user: request.sis_api_call = True diff --git a/apps/mainsite/settings.py b/apps/mainsite/settings.py index 73a678f9..d5e82d9a 100644 --- a/apps/mainsite/settings.py +++ b/apps/mainsite/settings.py @@ -1,4 +1,3 @@ -import json import os from mainsite import TOP_DIR @@ -83,6 +82,7 @@ def legacy_boolean_parsing(env_key, default_value): ] MIDDLEWARE = [ + 'django_prometheus.middleware.PrometheusBeforeMiddleware', 'corsheaders.middleware.CorsMiddleware', 'lti13.middleware.SameSiteMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -99,6 +99,7 @@ def legacy_boolean_parsing(env_key, default_value): # 'mainsite.middleware.TrailingSlashMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', + 'django_prometheus.middleware.PrometheusAfterMiddleware', ] ROOT_URLCONF = 'mainsite.urls' @@ -358,7 +359,7 @@ def legacy_boolean_parsing(env_key, default_value): CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', + 'BACKEND': 'django_prometheus.cache.backends.memcached.PyMemcacheCache', 'LOCATION': os.environ.get('MEMCACHED', '0.0.0.0:11211'), } } @@ -536,7 +537,7 @@ def legacy_boolean_parsing(env_key, default_value): # Database DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.mysql', + 'ENGINE': 'django_prometheus.db.backends.mysql', 'NAME': os.environ['BADGR_DB_NAME'], 'USER': os.environ['BADGR_DB_USER'], 'PASSWORD': os.environ['BADGR_DB_PASSWORD'], diff --git a/apps/mainsite/urls.py b/apps/mainsite/urls.py index eb1251a7..f2cca3aa 100644 --- a/apps/mainsite/urls.py +++ b/apps/mainsite/urls.py @@ -95,6 +95,7 @@ path('signing/', include(('signing.api_urls', 'signing'), namespace='signing_apis')), # include staff endpoints path('staff-membership/', include('staff.api_urls')), + path('', include('django_prometheus.urls')), ] # Test URLs to allow you to see these pages while DEBUG is True diff --git a/apps/mainsite/utils.py b/apps/mainsite/utils.py index 65fec240..d9d73b75 100644 --- a/apps/mainsite/utils.py +++ b/apps/mainsite/utils.py @@ -32,26 +32,24 @@ from premailer import transform from resizeimage.resizeimage import resize_contain -slugify_function_path = \ - getattr(settings, 'AUTOSLUG_SLUGIFY_FUNCTION', 'autoslug.utils.slugify') +slugify_function_path = getattr(settings, 'AUTOSLUG_SLUGIFY_FUNCTION', 'autoslug.utils.slugify') slugify = get_callable(slugify_function_path) def client_ip_from_request(request): - """Returns the IP of the request, accounting for the possibility of being behind a proxy. - """ - ip = request.headers.get("x-forwarded-for", None) + """Returns the IP of the request, accounting for the possibility of being behind a proxy.""" + ip = request.headers.get('x-forwarded-for', None) if ip: # X_FORWARDED_FOR returns client1, proxy1, proxy2,... - ip = ip.split(", ")[0] + ip = ip.split(', ')[0] else: - ip = request.META.get("REMOTE_ADDR", "") + ip = request.META.get('REMOTE_ADDR', '') return ip class OriginSettingsObject(object): - DefaultOrigin = "http://localhost:8000" + DefaultOrigin = 'http://localhost:8000' @property def DEFAULT_HTTP_PROTOCOL(self): @@ -103,9 +101,8 @@ def fetch_remote_file_to_storage(remote_url, upload_to=''): if r.status_code == 200: name, ext = os.path.splitext(urllib.parse.urlparse(r.url).path) storage_name = '{upload_to}/cached/{filename}{ext}'.format( - upload_to=upload_to, - filename=hashlib.md5(remote_url.encode()).hexdigest(), - ext=ext) + upload_to=upload_to, filename=hashlib.md5(remote_url.encode()).hexdigest(), ext=ext + ) if not store.exists(storage_name): buf = io.BytesIO(r.content) store.save(storage_name, buf) @@ -140,15 +137,14 @@ def list_of(value): def open_mail_in_browser(html): tmp = tempfile.NamedTemporaryFile(delete=False) - path = tmp.name + ".html" - f = open(path, "w") + path = tmp.name + '.html' + f = open(path, 'w') f.write(html) f.close() - webbrowser.open("file://" + path) + webbrowser.open('file://' + path) class EmailMessageMaker: - @staticmethod def _create_example_image(badgeclass): path = badgeclass.image.path @@ -158,9 +154,9 @@ def _create_example_image(badgeclass): encoded = base64.b64encode(svg).decode() return 'data:image/svg+xml;base64,{}'.format(encoded) else: - background = Image.open(badgeclass.image.path).convert("RGBA") + background = Image.open(badgeclass.image.path).convert('RGBA') - overlay = Image.open(finders.find('images/example_overlay.png')).convert("RGBA") + overlay = Image.open(finders.find('images/example_overlay.png')).convert('RGBA') if overlay.width != background.width: width_ratio = background.width / overlay.width new_background_height = background.height * width_ratio @@ -169,46 +165,54 @@ def _create_example_image(badgeclass): position = (0, background.height // 4) background.paste(overlay, position, overlay) buffered = BytesIO() - background.save(buffered, format="PNG") + background.save(buffered, format='PNG') encoded_string = base64.b64encode(buffered.getvalue()).decode() return 'data:image/png;base64,{}'.format(encoded_string) @staticmethod def create_enrollment_denied_email(enrollment): template = 'email/enrollment_denied.html' - email_vars = {'public_badge_url': enrollment.badge_class.public_url, - 'badgeclass_name': enrollment.badge_class.name, - 'recipient_name': enrollment.user.full_name, - 'deny_reason': enrollment.deny_reason} + email_vars = { + 'public_badge_url': enrollment.badge_class.public_url, + 'badgeclass_name': enrollment.badge_class.name, + 'recipient_name': enrollment.user.full_name, + 'deny_reason': enrollment.deny_reason, + } return render_to_string(template, email_vars) @staticmethod def create_assertion_revoked_email(badge_instance): template = 'email/assertion_revoked.html' - email_vars = {'public_badge_url': badge_instance.badgeclass.public_url, - 'badgeclass_name': badge_instance.badgeclass.name, - 'recipient_name': badge_instance.user.full_name, - 'ui_url': urllib.parse.urljoin(settings.UI_URL, 'archived'), - 'revocation_reason': badge_instance.revocation_reason} + email_vars = { + 'public_badge_url': badge_instance.badgeclass.public_url, + 'badgeclass_name': badge_instance.badgeclass.name, + 'recipient_name': badge_instance.user.full_name, + 'ui_url': urllib.parse.urljoin(settings.UI_URL, 'archived'), + 'revocation_reason': badge_instance.revocation_reason, + } return render_to_string(template, email_vars) @staticmethod def create_direct_award_deleted_email(direct_award): template = 'email/awarded_badge_deleted.html' - email_vars = {'public_badge_url': direct_award.badgeclass.public_url, - 'badgeclass_name': direct_award.badgeclass.name, - 'recipient_name': direct_award.recipient_email, - 'ui_url': urllib.parse.urljoin(settings.UI_URL, 'archived'), - 'revocation_reason': direct_award.revocation_reason} + email_vars = { + 'public_badge_url': direct_award.badgeclass.public_url, + 'badgeclass_name': direct_award.badgeclass.name, + 'recipient_name': direct_award.recipient_email, + 'ui_url': urllib.parse.urljoin(settings.UI_URL, 'archived'), + 'revocation_reason': direct_award.revocation_reason, + } return render_to_string(template, email_vars) @staticmethod def create_staff_rights_changed_email(staff_membership): template = 'email/staff_rights_changed.html' - email_vars = {'recipient_name': staff_membership.user.full_name, - 'entity_type': staff_membership.object.__class__.__name__.lower(), - 'entity_type_dutch': staff_membership.object.DUTCH_NAME.lower(), - 'entity_name': staff_membership.object.name} + email_vars = { + 'recipient_name': staff_membership.user.full_name, + 'entity_type': staff_membership.object.__class__.__name__.lower(), + 'entity_type_dutch': staff_membership.object.DUTCH_NAME.lower(), + 'entity_name': staff_membership.object.name, + } return render_to_string(template, email_vars) @staticmethod @@ -219,44 +223,52 @@ def create_user_invited_email(provisionment, login_link): if provisionment.user: invitee_name = provisionment.user.full_name invitee_name_dutch = provisionment.user.full_name - email_vars = {'login_link': login_link, - 'invited_by_name': provisionment.created_by.full_name, - 'invitee_name': invitee_name, - 'invitee_name_dutch': invitee_name_dutch, - 'entity_type': provisionment.entity.__class__.__name__.lower(), - 'entity_type_dutch': provisionment.entity.DUTCH_NAME.lower(), - 'entity_name': provisionment.entity.name, - 'support_email_address': 'support@edubadges.nl'} + email_vars = { + 'login_link': login_link, + 'invited_by_name': provisionment.created_by.full_name, + 'invitee_name': invitee_name, + 'invitee_name_dutch': invitee_name_dutch, + 'entity_type': provisionment.entity.__class__.__name__.lower(), + 'entity_type_dutch': provisionment.entity.DUTCH_NAME.lower(), + 'entity_name': provisionment.entity.name, + 'support_email_address': 'support@edubadges.nl', + } return render_to_string(template, email_vars) @staticmethod def create_student_badge_request_email(user, badge_class): template = 'email/requested_badge.html' - email_vars = {'public_badge_url': badge_class.public_url, - 'badge_name': badge_class.name, - 'user_name': user.get_full_name()} + email_vars = { + 'public_badge_url': badge_class.public_url, + 'badge_name': badge_class.name, + 'user_name': user.get_full_name(), + } return render_to_string(template, email_vars) @staticmethod def create_endorsement_requested_mail(current_user, user, endorsement): template = 'email/notification_endorsement.html' - email_vars = {'endorsement': endorsement, - 'ui_url': urllib.parse.urljoin(settings.UI_URL, - f"badgeclass/{endorsement.endorser.entity_id}/endorsed"), - 'ui_url_notifications': urllib.parse.urljoin(settings.UI_URL, 'notifications'), - 'user': user, - 'current_user': current_user} + email_vars = { + 'endorsement': endorsement, + 'ui_url': urllib.parse.urljoin(settings.UI_URL, f'badgeclass/{endorsement.endorser.entity_id}/endorsed'), + 'ui_url_notifications': urllib.parse.urljoin(settings.UI_URL, 'notifications'), + 'user': user, + 'current_user': current_user, + } return render_to_string(template, email_vars) @staticmethod def reject_approve_endorsement_mail(user, endorsement, accepted): template = 'email/accepted_rejected_endorsement.html' - email_vars = {'endorsement': endorsement, - 'action_en': 'accepted' if accepted else 'rejected', - 'action_nl': 'geaccepteerd' if accepted else 'afgewezen', - 'ui_url': urllib.parse.urljoin(settings.UI_URL, - f"badgeclass/{endorsement.endorsee.entity_id}/endorsements"), - 'user': user} + email_vars = { + 'endorsement': endorsement, + 'action_en': 'accepted' if accepted else 'rejected', + 'action_nl': 'geaccepteerd' if accepted else 'afgewezen', + 'ui_url': urllib.parse.urljoin( + settings.UI_URL, f'badgeclass/{endorsement.endorsee.entity_id}/endorsements' + ), + 'user': user, + } return render_to_string(template, email_vars) @staticmethod @@ -266,11 +278,13 @@ def create_staff_member_addition_email(new_staff_membership): entity_type = entity.__class__.__name__.lower() entity_type_dutch = entity.DUTCH_NAME.lower() determiner = 'an' if entity_type[0] in 'aeiou' else 'a' - email_vars = {'staff_page_url': new_staff_membership.staff_page_url, - 'entity_type': entity_type, - 'entity_type_dutch': entity_type_dutch, - 'entity_name': entity.name, - 'determiner': determiner} + email_vars = { + 'staff_page_url': new_staff_membership.staff_page_url, + 'entity_type': entity_type, + 'entity_type_dutch': entity_type_dutch, + 'entity_name': entity.name, + 'determiner': determiner, + } return render_to_string(template, email_vars) @staticmethod @@ -294,11 +308,11 @@ def create_enrolment_notification_mail(badge_class, student, created_enrollment) template = 'email/notification_badge.html' email_vars = { 'user_email': student.email, - 'ui_url': urllib.parse.urljoin(settings.UI_URL, f"badgeclass/{badge_class.entity_id}/enrollments"), + 'ui_url': urllib.parse.urljoin(settings.UI_URL, f'badgeclass/{badge_class.entity_id}/enrollments'), 'ui_url_notifications': urllib.parse.urljoin(settings.UI_URL, 'notifications'), 'badgeclass_name': badge_class.name, 'enrollment_evidence_description': created_enrollment.narrative, - 'enrollment_evidence_url': created_enrollment.evidence_url + 'enrollment_evidence_url': created_enrollment.evidence_url, } return render_to_string(template, email_vars) @@ -369,7 +383,7 @@ def create_scheduled_direct_award_bundle_mail(direct_award_bundle): email_vars = { 'direct_award_count': direct_award_bundle.direct_award_scheduled_count, 'direct_award_bundle_url': direct_award_bundle.url, - 'scheduled_at': direct_award_bundle.scheduled_at.strftime("%Y-%m-%d %H:%M"), + 'scheduled_at': direct_award_bundle.scheduled_at.strftime('%Y-%m-%d %H:%M'), 'badgeclass_description': badgeclass.description, 'badgeclass_name': badgeclass.name, } @@ -382,17 +396,14 @@ def create_feedback_mail(current_user, message): 'current_user': current_user, 'date': datetime.now(), 'environment': settings.DOMAIN, - 'message': message + 'message': message, } return render_to_string(template, email_vars) @staticmethod def create_email_validation_mail(code, user, badge, issuer): template = 'email/email_validation.html' - email_vars = {'code': code, - 'user': user, - 'badge': badge, - 'issuer': issuer} + email_vars = {'code': code, 'user': user, 'badge': badge, 'issuer': issuer} return render_to_string(template, email_vars) @@ -402,7 +413,7 @@ def send_mail(subject, message, recipient_list=None, html_message=None, bcc=None if html_message: html_with_inline_css = transform(html_message) msg = mail.EmailMessage(subject=subject, body=html_with_inline_css, from_email=None, to=recipient_list, bcc=bcc) - msg.content_subtype = "html" + msg.content_subtype = 'html' msg.send() else: mail.send_mail(subject, message, from_email=None, recipient_list=recipient_list, html_message=html_message) @@ -446,11 +457,11 @@ def _decompression_bomb_check(image, max_pixels=Image.MAX_IMAGE_PIXELS): def add_watermark(uploaded_image, is_svg): - text = "DEMO" + text = 'DEMO' angle = 45 opacity = 0.85 absolute = pathlib.Path().absolute() - font = f"{absolute}/apps/mainsite/arial.ttf" + font = f'{absolute}/apps/mainsite/arial.ttf' if is_svg: svg_buf = io.BytesIO() @@ -469,9 +480,9 @@ def add_watermark(uploaded_image, is_svg): n_width = right - left n_height = bottom - top draw = ImageDraw.Draw(watermark, 'RGBA') - draw.text(((watermark.size[0] - n_width) / 2, - (watermark.size[1] - n_height) / 2), - text, font=n_font, fill=(220, 12, 12)) + draw.text( + ((watermark.size[0] - n_width) / 2, (watermark.size[1] - n_height) / 2), text, font=n_font, fill=(220, 12, 12) + ) watermark = watermark.rotate(angle, Image.BICUBIC) alpha = watermark.split()[3] alpha = ImageEnhance.Brightness(alpha).enhance(opacity) @@ -482,11 +493,9 @@ def add_watermark(uploaded_image, is_svg): new_image.save(byte_string, 'PNG') image_name = uploaded_image.name if is_svg: - pattern = re.compile(r"\.svg", re.IGNORECASE) + pattern = re.compile(r'\.svg', re.IGNORECASE) image_name = pattern.sub('.png', image_name) - return InMemoryUploadedFile(byte_string, None, - image_name, 'image/png', - byte_string.getvalue().__len__(), None) + return InMemoryUploadedFile(byte_string, None, image_name, 'image/png', byte_string.getvalue().__len__(), None) def resize_image(uploaded_image): @@ -494,34 +503,28 @@ def resize_image(uploaded_image): f = open(uploaded_image.name, 'rb') image = Image.open(f) if _decompression_bomb_check(image): - raise ValidationError("Invalid image") - except IOError as e: + raise ValidationError('Invalid image') + except IOError: return uploaded_image if image.format == 'PNG': max_square = getattr(settings, 'IMAGE_FIELD_MAX_PX', 400) - smaller_than_canvas = (image.width < max_square and image.height < max_square) + smaller_than_canvas = image.width < max_square and image.height < max_square if smaller_than_canvas: - max_square = (image.width - if image.width > image.height - else image.height) + max_square = image.width if image.width > image.height else image.height new_image = resize_contain(image, (max_square, max_square)) byte_string = io.BytesIO() new_image.save(byte_string, 'PNG') - return InMemoryUploadedFile(byte_string, None, - uploaded_image.name, 'image/png', - byte_string.getvalue().__len__(), None) + return InMemoryUploadedFile( + byte_string, None, uploaded_image.name, 'image/png', byte_string.getvalue().__len__(), None + ) def scrub_svg_image(uploaded_image): - MALICIOUS_SVG_TAGS = [ - "script" - ] - MALICIOUS_SVG_ATTRIBUTES = [ - "onload" - ] - SVG_NAMESPACE = "http://www.w3.org/2000/svg" + MALICIOUS_SVG_TAGS = ['script'] + MALICIOUS_SVG_ATTRIBUTES = ['onload'] + SVG_NAMESPACE = 'http://www.w3.org/2000/svg' uploaded_image.file.seek(0) - ET.register_namespace("", SVG_NAMESPACE) + ET.register_namespace('', SVG_NAMESPACE) tree = ET.parse(uploaded_image.file) root = tree.getroot() diff --git a/apps/public/public_api.py b/apps/public/public_api.py index 9c5fd8d0..91eafdaf 100644 --- a/apps/public/public_api.py +++ b/apps/public/public_api.py @@ -35,6 +35,7 @@ class AssertionValidate(BaseEntityDetailView): """ Endpoint for validating a badge (GET) """ + model = BadgeInstance permission_classes = (permissions.AllowAny,) http_method_names = ['get'] @@ -72,7 +73,7 @@ def get_slug_to_entity_id_redirect(self, slug): if redirect_url is not None: query = self.request.META.get('QUERY_STRING', '') if query: - redirect_url = "{}?{}".format(redirect_url, query) + redirect_url = '{}?{}'.format(redirect_url, query) return redirect(redirect_url, permanent=True) else: raise Http404 @@ -82,6 +83,7 @@ class JSONComponentView(VersionedObjectMixin, APIView, SlugToEntityIdRedirectMix """ Abstract Component Class """ + permission_classes = (permissions.AllowAny,) authentication_classes = () html_renderer_class = None @@ -171,9 +173,8 @@ def get_badgrapp_redirect(self): stripped_path = re.sub(r'^/public/', '', path) query_string = self.request.META.get('QUERY_STRING', None) ret = '{redirect}{path}{query}'.format( - redirect=redirect, - path=stripped_path, - query='?' + query_string if query_string else '') + redirect=redirect, path=stripped_path, query='?' + query_string if query_string else '' + ) return ret @staticmethod @@ -193,7 +194,6 @@ def get_object(self, entity_id): return current_object def get(self, request, **kwargs): - entity_id = kwargs.get('entity_id') current_object = self.get_object(entity_id) if current_object is None and self.slugToEntityIdRedirect and getattr(request, 'version', 'v1') == 'v2': @@ -204,24 +204,21 @@ def get(self, request, **kwargs): image_prop = getattr(current_object, self.prop) if not bool(image_prop): return Response(status=status.HTTP_404_NOT_FOUND) - lang = request.query_params.get("lang") + lang = request.query_params.get('lang') if lang: - if lang == "en" and hasattr(current_object, self.prop_en): + if lang == 'en' and hasattr(current_object, self.prop_en): image_prop = getattr(current_object, self.prop_en) - if lang == "nl" and hasattr(current_object, self.prop_nl): + if lang == 'nl' and hasattr(current_object, self.prop_nl): image_prop = getattr(current_object, self.prop_nl) image_type = request.query_params.get('type', 'original') if image_type not in ['original', 'png']: - raise ValidationError("invalid image type: {}".format(image_type)) + raise ValidationError('invalid image type: {}'.format(image_type)) - supported_fmts = { - 'square': (1, 1), - 'wide': (1.91, 1) - } + supported_fmts = {'square': (1, 1), 'wide': (1.91, 1)} image_fmt = request.query_params.get('fmt', 'square').lower() if image_fmt not in list(supported_fmts.keys()): - raise ValidationError("invalid image format: {}".format(image_fmt)) + raise ValidationError('invalid image format: {}'.format(image_fmt)) image_url = image_prop.url filename, ext = os.path.splitext(image_prop.name) @@ -232,14 +229,14 @@ def get(self, request, **kwargs): dirname=dirname, basename=basename, version=version_suffix, - fmt_suffix="-{}".format(image_fmt) if image_fmt != 'square' else "" + fmt_suffix='-{}'.format(image_fmt) if image_fmt != 'square' else '', ) storage = DefaultStorage() def _fit_to_height(img, ar, height=400): img.thumbnail((height, height)) new_size = (int(ar[0] * height), int(ar[1] * height)) - new_img = Image.new("RGBA", new_size) + new_img = Image.new('RGBA', new_size) new_img.paste(img, ((new_size[0] - height) / 2, (new_size[1] - height) / 2)) new_img.show() return new_img @@ -279,12 +276,11 @@ class InstitutionJson(JSONComponentView): model = Institution def get_context_data(self, **kwargs): - image_url = "{}{}?type=png".format( - OriginSetting.HTTP, - reverse('institution_image', kwargs={'entity_id': self.current_object.entity_id}) + image_url = '{}{}?type=png'.format( + OriginSetting.HTTP, reverse('institution_image', kwargs={'entity_id': self.current_object.entity_id}) ) if self.is_wide_bot(): - image_url = "{}&fmt=wide".format(image_url) + image_url = '{}&fmt=wide'.format(image_url) return dict( title=self.current_object.name, @@ -325,29 +321,29 @@ def get_json(self, request): obi_version = self._get_request_obi_version(request) if 'institution' in expands: - json['faculty'] = {'name': self.current_object.faculty.name, - 'institution': self.current_object.faculty.institution.get_json(obi_version=obi_version)} + json['faculty'] = { + 'name': self.current_object.faculty.name, + 'institution': self.current_object.faculty.institution.get_json(obi_version=obi_version), + } return json def get_context_data(self, **kwargs): - image_url = "{}{}?type=png".format( - OriginSetting.HTTP, - reverse('issuer_image', kwargs={'entity_id': self.current_object.entity_id}) + image_url = '{}{}?type=png'.format( + OriginSetting.HTTP, reverse('issuer_image', kwargs={'entity_id': self.current_object.entity_id}) ) if self.is_wide_bot(): - image_url = "{}&fmt=wide".format(image_url) + image_url = '{}&fmt=wide'.format(image_url) return dict( title=self.current_object.name, description=self.current_object.description, public_url=self.current_object.public_url, - image_url=image_url + image_url=image_url, ) class IssuerPublicKeyJson(IssuerJson): - def get(self, request, **kwargs): self.current_object = self.get_object(request, **kwargs) self.log(self.current_object) @@ -357,8 +353,9 @@ def get(self, request, **kwargs): return render(request, self.template_name, context=self.get_context_data()) pubkey_issuer = PublicKeyIssuer.objects.get(entity_id=kwargs.get('public_key_id')) - issuer_json = self.get_json(request=request, signed=True, public_key_issuer=pubkey_issuer, - expand_public_key=False) + issuer_json = self.get_json( + request=request, signed=True, public_key_issuer=pubkey_issuer, expand_public_key=False + ) return Response(issuer_json) @@ -403,16 +400,15 @@ def get_json(self, request): expand_awards = 'awards' in expands if expand_awards: - json['award_allowed_institutions'] = [inst.name for inst in - badge_class.award_allowed_institutions.all()] + json['award_allowed_institutions'] = [inst.name for inst in badge_class.award_allowed_institutions.all()] json['formal'] = badge_class.formal json['archived'] = badge_class.archived json['self_enrollment_disabled'] = badge_class.self_enrollment_disabled json['awardNonValidatedNameAllowed'] = badge_class.award_non_validated_name_allowed if 'issuer' in expands: - json['issuer'] = badge_class.cached_issuer.get_json(obi_version=obi_version, - expand_institution=True, - expand_awards=expand_awards) + json['issuer'] = badge_class.cached_issuer.get_json( + obi_version=obi_version, expand_institution=True, expand_awards=expand_awards + ) if 'endorsements' in expands: json['endorsements'] = [self.endorsement_to_json(bc) for bc in badge_class.cached_endorsements()] json['endorsed'] = [endorsement.endorsee.entity_id for endorsement in badge_class.cached_endorsed()] @@ -432,13 +428,13 @@ def get_json(self, request): @staticmethod def _image_urls(obj, name, container): - image_url = OriginSetting.HTTP + reverse(f"{name}_image", kwargs={'entity_id': obj.entity_id}) + image_url = OriginSetting.HTTP + reverse(f'{name}_image', kwargs={'entity_id': obj.entity_id}) if hasattr(obj, 'image'): container['image'] = image_url if hasattr(obj, 'image_english') and obj.image_english: - container['imageEnglish'] = f"{image_url}?lang=en" + container['imageEnglish'] = f'{image_url}?lang=en' if hasattr(obj, 'imageDutch') and obj.image_dutch: - container['image_dutch'] = f"{image_url}?lang=nl" + container['image_dutch'] = f'{image_url}?lang=nl' @staticmethod def endorsement_to_json(endorsement): @@ -446,50 +442,54 @@ def endorsement_to_json(endorsement): issuer = endorser.cached_issuer faculty = issuer.faculty institution = faculty.institution - to_json = {'claim': endorsement.claim, - 'description': endorsement.description, - 'status': endorsement.status, - 'endorser': {'name': endorser.name, 'description': endorser.description, - 'entityId': endorser.entity_id, - 'issuer': {'nameDutch': issuer.name_dutch, 'nameEnglish': issuer.name_english, - 'entityId': issuer.entity_id, - 'faculty': {'nameDutch': faculty.name_dutch, - 'nameEnglish': faculty.name_english, - 'onBehalfOf': faculty.on_behalf_of, - 'onBehalfOfUrl': faculty.on_behalf_of_url, - 'onBehalfOfDisplayName': faculty.on_behalf_of_display_name, - 'entityId': faculty.entity_id, - 'institution': { - 'nameDutch': institution.name_dutch, - 'nameEnglish': institution.name_english, - 'entityId': institution.entity_id - } - } - } - }, - } + to_json = { + 'claim': endorsement.claim, + 'description': endorsement.description, + 'status': endorsement.status, + 'endorser': { + 'name': endorser.name, + 'description': endorser.description, + 'entityId': endorser.entity_id, + 'issuer': { + 'nameDutch': issuer.name_dutch, + 'nameEnglish': issuer.name_english, + 'entityId': issuer.entity_id, + 'faculty': { + 'nameDutch': faculty.name_dutch, + 'nameEnglish': faculty.name_english, + 'onBehalfOf': faculty.on_behalf_of, + 'onBehalfOfUrl': faculty.on_behalf_of_url, + 'onBehalfOfDisplayName': faculty.on_behalf_of_display_name, + 'entityId': faculty.entity_id, + 'institution': { + 'nameDutch': institution.name_dutch, + 'nameEnglish': institution.name_english, + 'entityId': institution.entity_id, + }, + }, + }, + }, + } BadgeClassJson._image_urls(issuer, 'issuer', to_json['endorser']['issuer']) BadgeClassJson._image_urls(institution, 'institution', to_json['endorser']['issuer']['faculty']['institution']) BadgeClassJson._image_urls(endorser, 'badgeclass', to_json['endorser']) return to_json def get_context_data(self, **kwargs): - image_url = "{}{}?type=png".format( - OriginSetting.HTTP, - reverse('badgeclass_image', kwargs={'entity_id': self.current_object.entity_id}) + image_url = '{}{}?type=png'.format( + OriginSetting.HTTP, reverse('badgeclass_image', kwargs={'entity_id': self.current_object.entity_id}) ) if self.is_wide_bot(): - image_url = "{}&fmt=wide".format(image_url) + image_url = '{}&fmt=wide'.format(image_url) return dict( title=self.current_object.name, description=self.current_object.description, public_url=self.current_object.public_url, - image_url=image_url + image_url=image_url, ) class BadgeClassPublicKeyJson(BadgeClassJson): - def get(self, request, **kwargs): self.current_object = self.get_object(request, **kwargs) self.log(self.current_object) @@ -530,6 +530,7 @@ class BadgeInstanceJson(JSONComponentView): ## You might see this screen because the badge you are looking for is *set to private*. ## Ask the recipient to set the badge to public, then try again. """ + permission_classes = (permissions.AllowAny,) model = BadgeInstance @@ -542,22 +543,22 @@ def get_json(self, request): request, expand_badgeclass=('badge' in expands), expand_issuer=('badge.issuer' in expands), - expand_user=('badge.user' in expands) + expand_user=('badge.user' in expands), ) return json def get_context_data(self, **kwargs): - image_url = "{}{}?type=png".format( + image_url = '{}{}?type=png'.format( OriginSetting.HTTP, - reverse('badgeclass_image', kwargs={'entity_id': self.current_object.cached_badgeclass.entity_id}) + reverse('badgeclass_image', kwargs={'entity_id': self.current_object.cached_badgeclass.entity_id}), ) if self.is_wide_bot(): - image_url = "{}&fmt=wide".format(image_url) + image_url = '{}&fmt=wide'.format(image_url) return dict( title=self.current_object.cached_badgeclass.name, description=self.current_object.cached_badgeclass.description, public_url=self.current_object.public_url, - image_url=image_url + image_url=image_url, ) @@ -590,7 +591,7 @@ def get(self, request, **kwargs): requested_version = request.query_params.get('v', utils.CURRENT_OBI_VERSION) if requested_version not in list(utils.OBI_VERSION_CONTEXT_IRIS.keys()): - raise ValidationError("Invalid OpenBadges version") + raise ValidationError('Invalid OpenBadges version') # self.log(assertion) diff --git a/requirements.txt b/requirements.txt index 0c310fde..74e93b04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -131,3 +131,4 @@ pycryptodome==3.21.0 django-api-proxy==0.1.1 django-upgrade==1.23.1 +django-prometheus==2.3.1