diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4bb9d97f..3724726b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,14 +14,15 @@ repos: rev: 21.12b0 hooks: - id: black + additional_dependencies: [click==8.0.4] - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 7.1.1 hooks: - id: flake8 args: ["--config=setup.cfg"] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 118b35a6..3441d931 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ The **branch name** is your first opportunity to give your task context. It is recommended to combine [**Github issues**](https://github.com/Coders-HQ/CodersHQ/issues) with a short description that describes the task resolved in this branch, for example: `Coders-HQ-portfolio/challenge-form` or `Coders-HQ-eventbrite/adding-api`. -If you don't have Github issue for you PR, then you may avoid the prefix, but keep in mind that more likely you have to create the issue first. +If you don't have Github issue for you PR, then you may avoid the prefix, but keep in mind that more likely you have to create the issue first. ## Commit your changes @@ -34,17 +34,17 @@ Be sure to **request reviews** from the appropriate people. This might include t ## Getting a better review -**Draft pull requests** in allow you to create a pull request that is still a work in progress and not ready for review. This is useful when you want to share your changes with others but aren't quite ready to merge them or request immediate feedback. +**Draft pull requests** in allow you to create a pull request that is still a work in progress and not ready for review. This is useful when you want to share your changes with others but aren't quite ready to merge them or request immediate feedback. https://github.blog/2019-02-14-introducing-draft-pull-requests/ -Once your pull request has been reviewed, be sure to **respond** to any feedback you receive. This might involve making additional changes to your code, addressing questions or concerns, or simply thanking reviewers for their feedback. +Once your pull request has been reviewed, be sure to **respond** to any feedback you receive. This might involve making additional changes to your code, addressing questions or concerns, or simply thanking reviewers for their feedback. -By using the **re-request review** feature, you can prompt the reviewer to take another look at your changes and provide feedback if necessary. +By using the **re-request review** feature, you can prompt the reviewer to take another look at your changes and provide feedback if necessary. https://github.blog/changelog/2019-02-21-re-request-review-on-a-pull-request/ -The **CODEOWNERS** file in GitHub allows you to specify who is responsible for code in a specific part of your repository. You can use this file to automatically assign pull requests to the appropriate people or teams, and to ensure that the right people are notified when changes are made to certain files or directories. +The **CODEOWNERS** file in GitHub allows you to specify who is responsible for code in a specific part of your repository. You can use this file to automatically assign pull requests to the appropriate people or teams, and to ensure that the right people are notified when changes are made to certain files or directories. https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners - + We use **scheduled reminders** to Slack for abandoned pull requests to will receive reminders to the team's channel for PRs that are non-draft and have no activity for a couple of days. https://docs.github.com/en/organizations/organizing-members-into-teams/managing-scheduled-reminders-for-your-team @@ -56,10 +56,10 @@ When your pull request is approved, be sure to **merge it responsibly**. This mi ### For curious minds -- How to write a Git commit message: +- How to write a Git commit message: https://cbea.ms/git-commit/ -- 13 tips to make your PR easier to review: +- 13 tips to make your PR easier to review: https://blog.codacy.com/13-tips-to-make-your-pr-easier-to-review/ Happy contributing! diff --git a/Makefile b/Makefile index c6744d82..b16bdd66 100644 --- a/Makefile +++ b/Makefile @@ -70,4 +70,4 @@ pro-collectstatic: docker compose -f production.yml run --rm django python manage.py collectstatic pro-rebuild: - make pro-down && git pull && make pro-build && make pro-up \ No newline at end of file + make pro-down && git pull && make pro-build && make pro-up diff --git a/README.md b/README.md index 00c79606..fe3b41f8 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ MIT License Badge - Discord Server Badge + Discord Server Badge

@@ -50,7 +50,7 @@ ## :wave: Introduction - + [![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors-) @@ -80,12 +80,16 @@ We also document the tasks in [Notion](https://suwaidi.notion.site/Coders-HQ-ae1 ## βš™οΈ Quick Setup +> Note: The recommended command is `docker compose` (Docker Compose v2). +> If your environment only has `docker-compose`, substitute accordingly. + + Make sure you have Docker version 2+ and then do the following to build the stack and update the databse : - $ docker-compose -f local.yml build - $ docker-compose -f local.yml run --rm django python manage.py makemigrations - $ docker-compose -f local.yml run --rm django python manage.py migrate - $ docker-compose -f local.yml run --rm django python manage.py createsuperuser + $ docker compose -f local.yml build + $ docker compose -f local.yml run --rm django python manage.py makemigrations + $ docker compose -f local.yml run --rm django python manage.py migrate + $ docker compose -f local.yml run --rm django python manage.py createsuperuser Follow the rest of the README for more information and use ``/admin`` to edit and create challenges. @@ -133,7 +137,7 @@ and might reappear if you generate a project multiple times with the same name. This can take a while, especially the first time you run this particular command on your development system:: - $ docker-compose -f local.yml build + $ docker compose -f local.yml build Generally, if you want to emulate production environment use [`production.yml`](production.yml) instead. And this is true for any other actions you might need to perform: whenever a switch is required, just do it! @@ -150,7 +154,7 @@ This brings up both Django and PostgreSQL. The first time it is run it might tak Open a terminal at the project root and run the following for local development:: - $ docker-compose -f local.yml up + $ docker compose -f local.yml up You can also set the environment variable ``COMPOSE_FILE`` pointing to [`local.yml`](local.yml) like this:: @@ -158,11 +162,11 @@ You can also set the environment variable ``COMPOSE_FILE`` pointing to [`local.y And then run:: - $ docker-compose up + $ docker compose up To run in a detached (background) mode, just:: - $ docker-compose up -d + $ docker compose up -d
@@ -173,10 +177,10 @@ To run in a detached (background) mode, just:: ## Execute Management Commands -As with any shell command that we wish to run in our container, this is done using the ``docker-compose -f local.yml run --rm`` command: :: +As with any shell command that we wish to run in our container, this is done using the ``docker compose -f local.yml run --rm`` command: :: - $ docker-compose -f local.yml run --rm django python manage.py migrate - $ docker-compose -f local.yml run --rm django python manage.py createsuperuser + $ docker compose -f local.yml run --rm django python manage.py migrate + $ docker compose -f local.yml run --rm django python manage.py createsuperuser Here, ``django`` is the target service we are executing the commands against. @@ -262,6 +266,20 @@ Please check `cookiecutter-django Docker documentation` for more details how to With MailHog running, to view messages that are sent by your application, open your browser and go to ``http://127.0.0.1:8025`` +### API Authentication (JWT) + +The API uses JWT authentication (SimpleJWT). + +- Obtain tokens: + - `POST /api/token/` with JSON body `{"username": "", "password": ""}` + - Response includes `access` and `refresh` +- Use the access token on requests: + - `Authorization: Bearer ` (legacy clients may also use `Authorization: JWT `) +- Refresh tokens: + - `POST /api/token/refresh/` with `{"refresh": ""}` + +For backwards compatibility, `POST /api-token-auth/` is still available and returns a `token` field. + ## Stargazers ⭐ ### Thanks to all of our `Stargazers` ⭐ πŸ”­ who are supporting CodersHQ project diff --git a/README.rst b/README.rst index f8f4273b..3e82987b 100644 --- a/README.rst +++ b/README.rst @@ -41,12 +41,15 @@ We also document the tasks in the `project`_ section and have a look at the `iss Quick Setup ----------- +.. note:: The recommended command is ``docker compose`` (Docker Compose v2). + If your environment only has ``docker-compose``, substitute accordingly. + Make sure you have Docker version 2+ and then do the following to build the stack and update the databse :: - $ docker-compose -f local.yml build - $ docker-compose -f local.yml run --rm django python manage.py makemigrations - $ docker-compose -f local.yml run --rm django python manage.py migrate - $ docker-compose -f local.yml run --rm django python manage.py createsuperuser + $ docker compose -f local.yml build + $ docker compose -f local.yml run --rm django python manage.py makemigrations + $ docker compose -f local.yml run --rm django python manage.py migrate + $ docker compose -f local.yml run --rm django python manage.py createsuperuser Follow the rest of the README for more information and use ``/admin`` to edit and create challenges. @@ -80,7 +83,7 @@ Build the Stack This can take a while, especially the first time you run this particular command on your development system:: - $ docker-compose -f local.yml build + $ docker compose -f local.yml build Generally, if you want to emulate production environment use ``production.yml`` instead. And this is true for any other actions you might need to perform: whenever a switch is required, just do it! @@ -92,7 +95,7 @@ This brings up both Django and PostgreSQL. The first time it is run it might tak Open a terminal at the project root and run the following for local development:: - $ docker-compose -f local.yml up + $ docker compose -f local.yml up You can also set the environment variable ``COMPOSE_FILE`` pointing to ``local.yml`` like this:: @@ -100,20 +103,20 @@ You can also set the environment variable ``COMPOSE_FILE`` pointing to ``local.y And then run:: - $ docker-compose up + $ docker compose up To run in a detached (background) mode, just:: - $ docker-compose up -d + $ docker compose up -d Execute Management Commands --------------------------- -As with any shell command that we wish to run in our container, this is done using the ``docker-compose -f local.yml run --rm`` command: :: +As with any shell command that we wish to run in our container, this is done using the ``docker compose -f local.yml run --rm`` command: :: - $ docker-compose -f local.yml run --rm django python manage.py migrate - $ docker-compose -f local.yml run --rm django python manage.py createsuperuser + $ docker compose -f local.yml run --rm django python manage.py migrate + $ docker compose -f local.yml run --rm django python manage.py createsuperuser Here, ``django`` is the target service we are executing the commands against. diff --git a/codershq/api/auth_views.py b/codershq/api/auth_views.py new file mode 100644 index 00000000..6f3f4de7 --- /dev/null +++ b/codershq/api/auth_views.py @@ -0,0 +1,25 @@ +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer + + +class LegacyTokenObtainView(APIView): + """Backwards-compatible JWT endpoint. + + Historically this project exposed `/api-token-auth/` (drf-jwt) which returned + `{"token": "..."}`. SimpleJWT returns `{access, refresh}`. + + This view preserves the legacy response shape while also returning the + modern fields. + """ + + permission_classes = [AllowAny] + + def post(self, request, *args, **kwargs): + serializer = TokenObtainPairSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + tokens = serializer.validated_data + access = tokens.get("access") + refresh = tokens.get("refresh") + return Response({"token": access, "access": access, "refresh": refresh}) diff --git a/codershq/api/serializers.py b/codershq/api/serializers.py index 50910554..dcc5ed68 100644 --- a/codershq/api/serializers.py +++ b/codershq/api/serializers.py @@ -1,62 +1,68 @@ from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group, User from django.contrib.auth.password_validation import validate_password -from djangosaml2idp.models import ServiceProvider from rest_framework import serializers from rest_framework.validators import UniqueValidator -from codershq.users.models import User from codershq.portfolio.models import Portfolio -User=get_user_model() # to point to the custom user model +User = get_user_model() class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = User - fields = ['url', 'username', 'email', 'groups','id'] + fields = ["url", "username", "email", "groups", "id"] + class PortfolioSerializer(serializers.ModelSerializer): class Meta: model = Portfolio - fields = '__all__' + fields = "__all__" + class RegisterSerializer(serializers.ModelSerializer): email = serializers.EmailField( - required=True, - validators=[UniqueValidator(queryset=User.objects.all())] - ) + required=True, validators=[UniqueValidator(queryset=User.objects.all())] + ) - password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) + password = serializers.CharField( + write_only=True, required=True, validators=[validate_password] + ) password2 = serializers.CharField(write_only=True, required=True) - - class Meta: model = User - fields = ('username', 'password', 'password2', 'email', 'first_name', 'last_name') + fields = ( + "username", + "password", + "password2", + "email", + "first_name", + "last_name", + ) extra_kwargs = { - 'first_name': {'required': True}, - 'last_name': {'required': True} + "first_name": {"required": True}, + "last_name": {"required": True}, } def validate(self, attrs): - if attrs['password'] != attrs['password2']: - raise serializers.ValidationError({"password": "Password fields didn't match."}) + if attrs["password"] != attrs["password2"]: + raise serializers.ValidationError( + {"password": "Password fields didn't match."} + ) return attrs def create(self, validated_data): user = User.objects.create( - username=validated_data['username'], - email=validated_data['email'], - first_name=validated_data['first_name'], - last_name=validated_data['last_name'] + username=validated_data["username"], + email=validated_data["email"], + first_name=validated_data["first_name"], + last_name=validated_data["last_name"], ) - - user.set_password(validated_data['password']) + user.set_password(validated_data["password"]) user.save() return user diff --git a/codershq/api/tests.py b/codershq/api/tests.py new file mode 100644 index 00000000..a03fa6dd --- /dev/null +++ b/codershq/api/tests.py @@ -0,0 +1,71 @@ +import pytest +from rest_framework.test import APIClient + + +@pytest.mark.django_db +def test_token_obtain_and_admin_endpoint_permissions(django_user_model): + client = APIClient() + + admin_password = "admin-pass-123" + admin = django_user_model.objects.create_user( + username="admin_user", + password=admin_password, + is_staff=True, + is_superuser=True, + ) + + user_password = "user-pass-123" + django_user_model.objects.create_user( + username="normal_user", + password=user_password, + is_staff=False, + is_superuser=False, + ) + + # Unauthenticated should be rejected (IsAdminUser) + unauth_resp = client.get("/api/users/data/") + assert unauth_resp.status_code in (401, 403) + + # Normal user token -> still forbidden + token_resp = client.post( + "/api/token/", + {"username": "normal_user", "password": user_password}, + format="json", + ) + assert token_resp.status_code == 200 + access = token_resp.data["access"] + client.credentials(HTTP_AUTHORIZATION=f"Bearer {access}") + forbidden_resp = client.get("/api/users/data/") + assert forbidden_resp.status_code in (401, 403) + + # Admin token -> allowed + token_resp = client.post( + "/api/token/", + {"username": admin.username, "password": admin_password}, + format="json", + ) + assert token_resp.status_code == 200 + admin_access = token_resp.data["access"] + client.credentials(HTTP_AUTHORIZATION=f"Bearer {admin_access}") + + ok_resp = client.get("/api/users/data/") + assert ok_resp.status_code == 200 + assert "data" in ok_resp.json() + + +@pytest.mark.django_db +def test_legacy_api_token_auth_returns_token_field(django_user_model): + client = APIClient() + + password = "pass-123" + django_user_model.objects.create_user(username="legacy_user", password=password) + + resp = client.post( + "/api-token-auth/", + {"username": "legacy_user", "password": password}, + format="json", + ) + assert resp.status_code == 200 + assert "token" in resp.data + assert "access" in resp.data + assert "refresh" in resp.data diff --git a/codershq/api/urls.py b/codershq/api/urls.py index 663e70b9..cfdefbf9 100644 --- a/codershq/api/urls.py +++ b/codershq/api/urls.py @@ -1,13 +1,13 @@ from django.urls import path + from . import views from .views import RegisterView - app_name = "api" urlpatterns = [ - path('', views.getRoutes), - path('register/', RegisterView.as_view(), name='auth_register'), + path("", views.getRoutes), + path("register/", RegisterView.as_view(), name="auth_register"), ] # assessment results diff --git a/codershq/api/utils/analytics.py b/codershq/api/utils/analytics.py index 4dae917b..1af82cf3 100644 --- a/codershq/api/utils/analytics.py +++ b/codershq/api/utils/analytics.py @@ -1,10 +1,8 @@ - from codershq.api.utils.pluralsight import PluralSight from codershq.portfolio.models import Portfolio class Analytics: - def __init__(self): self.all_skills = PluralSight.all_skills() @@ -26,7 +24,7 @@ def quintile_levels(self): eg: [5,5,5,6,3] - which means 5 novice, etc + which means 5 novice, etc """ quintile_levels = [] novice = 0 @@ -35,23 +33,25 @@ def quintile_levels(self): proficient_above_average = 0 expert = 0 for skill in self.all_skills: - quintile = skill['quintileLevel'] - if quintile == 'novice': + quintile = skill["quintileLevel"] + if quintile == "novice": novice += 1 - elif quintile == 'proficient-emerging': + elif quintile == "proficient-emerging": proficient_emerging += 1 - elif quintile == 'proficient-average': + elif quintile == "proficient-average": proficient_average += 1 - elif quintile == 'proficient-above-average': + elif quintile == "proficient-above-average": proficient_above_average += 1 - elif quintile == 'expert': + elif quintile == "expert": expert += 1 - quintile_levels = [novice, - proficient_emerging, - proficient_average, - proficient_above_average, - expert] + quintile_levels = [ + novice, + proficient_emerging, + proficient_average, + proficient_above_average, + expert, + ] return quintile_levels @@ -59,7 +59,7 @@ def repeat_num(self): """ return total number of repeats """ - retakes = [x for x in self.all_skills if x['measurementType'] == 'retake'] + retakes = [x for x in self.all_skills if x["measurementType"] == "retake"] return len(retakes) def local_num(self): @@ -67,35 +67,35 @@ def local_num(self): return number of locals """ - return Portfolio.objects.filter(nationality='AE').count() + return Portfolio.objects.filter(nationality="AE").count() def total_males(self): """ return total males """ - return Portfolio.objects.filter(gender='M').count() + return Portfolio.objects.filter(gender="M").count() def total_local_males(self): """ return total males """ - return Portfolio.objects.filter(gender='M', nationality='AE').count() + return Portfolio.objects.filter(gender="M", nationality="AE").count() def total_female(self): """ return total female """ - return Portfolio.objects.filter(gender='F').count() + return Portfolio.objects.filter(gender="F").count() def total_local_female(self): """ return total female """ - return Portfolio.objects.filter(gender='F', nationality='AE').count() + return Portfolio.objects.filter(gender="F", nationality="AE").count() def total_lfj(self): """ @@ -107,11 +107,11 @@ def local_lfj(self): """ return number of people looking for jobs """ - return Portfolio.objects.filter(is_seeking_job=True, nationality='AE').count() + return Portfolio.objects.filter(is_seeking_job=True, nationality="AE").count() def json(self): """ - return full data as a dict + return full data as a dict """ data = { "total_skill": self.total_skills(), diff --git a/codershq/api/utils/pluralsight.py b/codershq/api/utils/pluralsight.py index b1c03a61..cf097239 100644 --- a/codershq/api/utils/pluralsight.py +++ b/codershq/api/utils/pluralsight.py @@ -1,12 +1,15 @@ -import requests import os + +import requests + + class PluralSight: """ Get everything related pluralsight """ URL = "https://paas-api.pluralsight.com/graphql" - AUTH = "Bearer " + os.getenv('PLURAL_TOKEN', default='test') + AUTH = "Bearer " + os.getenv("PLURAL_TOKEN", default="test") @classmethod def all_skills(cls): @@ -24,11 +27,11 @@ def all_skills(cls): request = requests.post( PluralSight.URL, - headers={'Authorization': PluralSight.AUTH}, - json={'query': query} + headers={"Authorization": PluralSight.AUTH}, + json={"query": query}, ) - return request.json()['data']['skillAssessmentResults']['nodes'] + return request.json()["data"]["skillAssessmentResults"]["nodes"] @classmethod def get_user(cls, id): @@ -36,10 +39,13 @@ def get_user(cls, id): get specific user based on portfolio id """ - query = """ + query = ( + """ { users (filter: { - emails: \"""" + f'{id}@codershq.ae' + """\" + emails: \"""" + + f"{id}@codershq.ae" + + """\" }) { nodes { id @@ -48,14 +54,15 @@ def get_user(cls, id): } } """ + ) request = requests.post( PluralSight.URL, - headers={'Authorization': PluralSight.AUTH}, - json={'query': query} + headers={"Authorization": PluralSight.AUTH}, + json={"query": query}, ) - return request.json()['data']['users']['nodes'] + return request.json()["data"]["users"]["nodes"] @classmethod def get_psid(cls, id): @@ -72,12 +79,13 @@ def get_user_skill_psid(cls, psid): returns user skill based on pluralsight id """ - - - query = """ + query = ( + """ { skillAssessmentResults (filter: { - userIds: \"""" + psid + """\" + userIds: \"""" + + psid + + """\" }) { nodes { quintileLevel @@ -87,17 +95,17 @@ def get_user_skill_psid(cls, psid): } } """ + ) request = requests.post( PluralSight.URL, - headers={'Authorization': PluralSight.AUTH}, - json={'query': query} + headers={"Authorization": PluralSight.AUTH}, + json={"query": query}, ) - return request.json()['data']['skillAssessmentResults']['nodes'] - + return request.json()["data"]["skillAssessmentResults"]["nodes"] @classmethod def get_user_skill(cls, id): psid = PluralSight.get_psid(id) - return PluralSight.get_user_skill_psid(psid) \ No newline at end of file + return PluralSight.get_user_skill_psid(psid) diff --git a/codershq/api/views.py b/codershq/api/views.py index 069bfb15..9582d122 100644 --- a/codershq/api/views.py +++ b/codershq/api/views.py @@ -13,23 +13,23 @@ from rest_framework.parsers import JSONParser from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated from rest_framework.response import Response -from rest_framework_jwt.authentication import JSONWebTokenAuthentication +from rest_framework_simplejwt.authentication import JWTAuthentication from codershq.api.utils.analytics import Analytics from codershq.api.utils.pluralsight import PluralSight from codershq.portfolio.models import Portfolio -from codershq.users.models import User from .serializers import PortfolioSerializer, RegisterSerializer User = get_user_model() -@api_view(['GET']) +@api_view(["GET"]) def getRoutes(request): routes = [ - '/api/token', - '/api/token/refresh', + "/api/token/", + "/api/token/refresh/", + "/api-token-auth/", "users/all/", "assessment/skills/all/", "users/skills//", @@ -44,52 +44,53 @@ class RegisterView(generics.CreateAPIView): serializer_class = RegisterSerializer -@api_view(['GET']) -@authentication_classes([SessionAuthentication, BasicAuthentication, JSONWebTokenAuthentication]) +@api_view(["GET"]) +@authentication_classes([SessionAuthentication, BasicAuthentication, JWTAuthentication]) @permission_classes([IsAdminUser]) def users_all(request): """ return all users with pluralsight data """ all_users = Portfolio.objects.all() - serialized_obj = serializers.serialize('json', all_users) + serialized_obj = serializers.serialize("json", all_users) data = {"data": json.loads(serialized_obj)} return JsonResponse(data, safe=True) -@api_view(['GET']) -@authentication_classes([SessionAuthentication, BasicAuthentication, JSONWebTokenAuthentication]) + +@api_view(["GET"]) +@authentication_classes([SessionAuthentication, BasicAuthentication, JWTAuthentication]) @permission_classes([IsAdminUser]) def users_data(request): """ return all users with pluralsight data """ all_users = User.objects.all() - serialized_obj = serializers.serialize('json', all_users) + serialized_obj = serializers.serialize("json", all_users) data = {"data": json.loads(serialized_obj)} return JsonResponse(data, safe=True) -@api_view(['GET','POST']) -@authentication_classes([SessionAuthentication, BasicAuthentication, JSONWebTokenAuthentication, ]) +@api_view(["GET", "POST"]) +@authentication_classes([SessionAuthentication, BasicAuthentication, JWTAuthentication]) @permission_classes([IsAuthenticated]) def user(request, username): """ return current logged in user portfolio """ - if request.method == 'GET': + if request.method == "GET": user = User.objects.get(username=username) portfolio = Portfolio.objects.get(user=user.id) serializer = PortfolioSerializer(portfolio) return JsonResponse(serializer.data, safe=False) - elif request.method == 'POST': + elif request.method == "POST": data = JSONParser().parse(request) print(data) -@api_view(['GET']) +@api_view(["GET"]) def skills_all(requests): """ return all pluralsight skills taken as a json list @@ -98,7 +99,7 @@ def skills_all(requests): return JsonResponse(data, safe=True) -@api_view(['GET']) +@api_view(["GET"]) def user_id_skills(requests, id): """ return specific user skills based on id @@ -108,7 +109,7 @@ def user_id_skills(requests, id): return JsonResponse(data, safe=True) -@api_view(['GET']) +@api_view(["GET"]) def analytics_public(requests): """ return important analytics @@ -119,7 +120,7 @@ def analytics_public(requests): return JsonResponse(data, safe=True) -@api_view(['GET']) +@api_view(["GET"]) def analytics_private(requests): """ return private analytics @@ -127,7 +128,7 @@ def analytics_private(requests): pass -@api_view(['GET']) +@api_view(["GET"]) def leaderboard(requests): """ return top users based on skills diff --git a/codershq/assessment/admin.py b/codershq/assessment/admin.py index 8c38f3f3..e9204527 100755 --- a/codershq/assessment/admin.py +++ b/codershq/assessment/admin.py @@ -1,3 +1,4 @@ -from django.contrib import admin +"""Assessment admin. -# Register your models here. +No admin registrations currently. +""" diff --git a/codershq/assessment/apps.py b/codershq/assessment/apps.py index 068fa35b..bc18eb9d 100755 --- a/codershq/assessment/apps.py +++ b/codershq/assessment/apps.py @@ -2,6 +2,6 @@ class AssessmentConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' + default_auto_field = "django.db.models.BigAutoField" name = "codershq.assessment" - verbose_name = ("Assessment") + verbose_name = "Assessment" diff --git a/codershq/assessment/forms.py b/codershq/assessment/forms.py index 474903c1..268b7802 100644 --- a/codershq/assessment/forms.py +++ b/codershq/assessment/forms.py @@ -1,4 +1,5 @@ from django import forms + class PluralPasswordForm(forms.Form): - password = forms.PasswordInput('password', max_length=255) + password = forms.PasswordInput("password", max_length=255) diff --git a/codershq/assessment/models.py b/codershq/assessment/models.py index 71a83623..d2b19643 100755 --- a/codershq/assessment/models.py +++ b/codershq/assessment/models.py @@ -1,3 +1,4 @@ -from django.db import models +"""Assessment models. -# Create your models here. +This app currently defines no database models. +""" diff --git a/codershq/assessment/processors.py b/codershq/assessment/processors.py index 780047bc..59ce6e90 100644 --- a/codershq/assessment/processors.py +++ b/codershq/assessment/processors.py @@ -3,13 +3,15 @@ class GroupProcessor(BaseProcessor): """ - Example implementation of access control for users: - - superusers are allowed - - staff is allowed - - they have to belong to a certain group + Example implementation of access control for users: + - superusers are allowed + - staff is allowed + - they have to belong to a certain group """ + group = "ExampleGroup" + # def has_access(self, request): # user = request.user # return user.is_superuser or user.is_staff or user.groups.filter(name=self.group).exists() diff --git a/codershq/assessment/tests.py b/codershq/assessment/tests.py index 7ce503c2..80a88615 100755 --- a/codershq/assessment/tests.py +++ b/codershq/assessment/tests.py @@ -1,3 +1,4 @@ -from django.test import TestCase +"""App tests. -# Create your tests here. +This module is intentionally empty for now. +""" diff --git a/codershq/assessment/urls.py b/codershq/assessment/urls.py index ba319266..ea6253fc 100644 --- a/codershq/assessment/urls.py +++ b/codershq/assessment/urls.py @@ -1,11 +1,12 @@ +from django.contrib.auth.views import LoginView, LogoutView from django.urls import include, path -from django.contrib.auth.views import LogoutView, LoginView + from . import views app_name = "assessment" urlpatterns = [ - path('idp/', include('djangosaml2idp.urls', namespace='djangosaml2')), - path('login/', LoginView.as_view(template_name='idp/login.html'), name='login'), - path('logout/', LogoutView.as_view()), - path('', views.IndexView.as_view()), + path("idp/", include("djangosaml2idp.urls", namespace="djangosaml2")), + path("login/", LoginView.as_view(template_name="idp/login.html"), name="login"), + path("logout/", LogoutView.as_view()), + path("", views.IndexView.as_view()), ] diff --git a/codershq/assessment/views.py b/codershq/assessment/views.py index 5cecd8ce..2e46a18c 100755 --- a/codershq/assessment/views.py +++ b/codershq/assessment/views.py @@ -8,13 +8,18 @@ class IndexView(TemplateView): def get_context_data(self, **kwargs): context = super(IndexView, self).get_context_data(**kwargs) - context.update({ - "logout_url": settings.LOGOUT_URL, - "login_url": settings.LOGIN_URL, - }) + context.update( + { + "logout_url": settings.LOGOUT_URL, + "login_url": settings.LOGIN_URL, + } + ) if self.request.user.is_authenticated: - context.update({ - "known_sp_ids": [sp for sp in ServiceProvider.objects.filter(active=True)], - }) + context.update( + { + "known_sp_ids": [ + sp for sp in ServiceProvider.objects.filter(active=True) + ], + } + ) return context - diff --git a/codershq/portfolio/admin.py b/codershq/portfolio/admin.py index e9b2748e..5815a1b7 100644 --- a/codershq/portfolio/admin.py +++ b/codershq/portfolio/admin.py @@ -1,7 +1,9 @@ # Register your models here. from django.contrib import admin + from .models import Portfolio + @admin.register(Portfolio) class AuthorAdmin(admin.ModelAdmin): - pass \ No newline at end of file + pass diff --git a/codershq/portfolio/forms.py b/codershq/portfolio/forms.py index d500ac34..edc5becc 100644 --- a/codershq/portfolio/forms.py +++ b/codershq/portfolio/forms.py @@ -1,8 +1,9 @@ - from django import forms + from .models import Portfolio + class ProfileCreateForm(forms.ModelForm): class Meta: model = Portfolio - exclude = ('user',) + exclude = ("user",) diff --git a/codershq/portfolio/models.py b/codershq/portfolio/models.py index 24d70ce5..67df10cb 100644 --- a/codershq/portfolio/models.py +++ b/codershq/portfolio/models.py @@ -1,11 +1,8 @@ # Create your models here. +from django.core.validators import MaxValueValidator, MinValueValidator, URLValidator from django.db import models from django.utils.translation import gettext_lazy as _ from django_countries.fields import CountryField -from django.core.validators import URLValidator -from django.core.validators import MaxValueValidator, MinValueValidator -from django import forms - from codershq.api.utils.pluralsight import PluralSight @@ -19,13 +16,14 @@ class Portfolio(models.Model): """ Portfolio model """ + GENDER_CHOICES = ( - ('M', 'Male'), - ('F', 'Female'), + ("M", "Male"), + ("F", "Female"), ) EMPLOYMENT_TIME_CHOICES = ( - ('F', 'Fulltime'), - ('P', 'Part-Time'), + ("F", "Fulltime"), + ("P", "Part-Time"), ) first_name = models.CharField(_("First Name"), blank=False, max_length=255) last_name = models.CharField(_("Last Name"), blank=False, max_length=255) @@ -37,44 +35,48 @@ class Portfolio(models.Model): choices=GENDER_CHOICES, ) nationality = CountryField( - blank_label='(select nationality)', + blank_label="(select nationality)", blank=False, ) country_residence = CountryField( - blank_label='(select country)', + blank_label="(select country)", blank=False, ) academic_qualification = models.CharField( - _("Your highest academic qualification"), help_text="e.g. MSc Sustainable Energy Technologies", blank=False, max_length=30 + _("Your highest academic qualification"), + help_text="e.g. MSc Sustainable Energy Technologies", + blank=False, + max_length=30, ) mobile_number = models.CharField( _("Your mobile number"), max_length=100, blank=True, null=True, - help_text=_("(eg: +97150XXXXXXX)")) + help_text=_("(eg: +97150XXXXXXX)"), + ) is_seeking_job = models.BooleanField( _("Seeking employment?"), default=False, - help_text=_("Are you looking for employment?")) + help_text=_("Are you looking for employment?"), + ) is_working = models.BooleanField( _("Currently employed?"), default=False, - help_text=_("Are you employed currently?")) + help_text=_("Are you employed currently?"), + ) employer = models.CharField( _("Your employer"), max_length=150, blank=True, null=True, - help_text=_("(If currently employed)")) + help_text=_("(If currently employed)"), + ) years_experience = models.IntegerField( _("Years of experience"), null=True, blank=True, - validators=[ - MaxValueValidator(100), - MinValueValidator(0) - ] + validators=[MaxValueValidator(100), MinValueValidator(0)], ) employment_time = models.CharField( _("Would you prefer to work full-time or part-time?"), @@ -89,14 +91,16 @@ class Portfolio(models.Model): blank=True, validators=[URLValidator], null=True, - help_text=_("https://github.com/Coders-HQ")) + help_text=_("https://github.com/Coders-HQ"), + ) linkedin = models.CharField( _("Linkedin account"), max_length=100, validators=[URLValidator], blank=True, null=True, - help_text=_("https://linkedin.com/in/XXXX")) + help_text=_("https://linkedin.com/in/XXXX"), + ) # twitter = models.CharField( # _("Twitter account"), # max_length=100, @@ -115,25 +119,28 @@ class Portfolio(models.Model): help_text=_("This can be a language or framework"), null=True, blank=True, - max_length=50) + max_length=50, + ) about = models.TextField( _("Use this space to tell us about yourself"), blank=True, help_text=_("Tell us a bit about yourself"), - max_length=1500) + max_length=1500, + ) proud_project = models.TextField( _("Tell us about the project that you are most proud of"), blank=True, help_text=_("This can be any project that you were involved in"), - max_length=2500) + max_length=2500, + ) # profile_image = models.ImageField( # _("Profile image"), upload_to=user_image_path, null=True, blank=True # ) # auto generated fields created_at = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey('users.User', on_delete=models.CASCADE) + user = models.ForeignKey("users.User", on_delete=models.CASCADE) updated_at = models.DateTimeField(auto_now=True) @property diff --git a/codershq/portfolio/urls.py b/codershq/portfolio/urls.py index 86aeb23e..ae5c9ac9 100644 --- a/codershq/portfolio/urls.py +++ b/codershq/portfolio/urls.py @@ -1,8 +1,6 @@ from django.urls import path -from codershq.portfolio.views import ( - create_profile, -) +from codershq.portfolio.views import create_profile app_name = "portfolio" urlpatterns = [ diff --git a/codershq/portfolio/views.py b/codershq/portfolio/views.py index f2cb4574..c711cb3b 100644 --- a/codershq/portfolio/views.py +++ b/codershq/portfolio/views.py @@ -1,21 +1,21 @@ # Create your views here. from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, render + from .forms import ProfileCreateForm -from django.urls import reverse from .models import Portfolio + @login_required def create_profile(request): - profile = None try: profile = Portfolio.objects.get(user=request.user) - except: - pass + except Portfolio.DoesNotExist: + profile = None - if profile==None: - if request.method == 'POST': + if profile is None: + if request.method == "POST": form = ProfileCreateForm(request.POST) if form.is_valid(): profile = form.save(commit=False) @@ -26,6 +26,6 @@ def create_profile(request): form = ProfileCreateForm() return render(request, "assessment/profile_form.html", {"form": form}) - + else: - return redirect("users:plural", username=request.user.username) \ No newline at end of file + return redirect("users:plural", username=request.user.username) diff --git a/codershq/static/images/favicon.svg b/codershq/static/images/favicon.svg index b022ddb9..25180256 100644 --- a/codershq/static/images/favicon.svg +++ b/codershq/static/images/favicon.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/codershq/static/images/svgs/SuccessSVG.svg b/codershq/static/images/svgs/SuccessSVG.svg index 9c90b3d9..ef5598ea 100644 --- a/codershq/static/images/svgs/SuccessSVG.svg +++ b/codershq/static/images/svgs/SuccessSVG.svg @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/codershq/templates/404.html b/codershq/templates/404.html index c55ad2c9..100f93aa 100644 --- a/codershq/templates/404.html +++ b/codershq/templates/404.html @@ -18,4 +18,4 @@

Back to Homepage -

+ {% endblock content %} diff --git a/codershq/templates/account/base.html b/codershq/templates/account/base.html index e267ffeb..096bf25d 100644 --- a/codershq/templates/account/base.html +++ b/codershq/templates/account/base.html @@ -4,11 +4,11 @@ {% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %} {% block content %} - +
- +
@@ -17,7 +17,7 @@ {% block header %} {% endblock header %} - + {% block inner %}{% endblock %}
diff --git a/codershq/templates/account/email/email_confirmation_message copy.html b/codershq/templates/account/email/email_confirmation_message copy.html index 5fcb4961..21674ea8 100644 --- a/codershq/templates/account/email/email_confirmation_message copy.html +++ b/codershq/templates/account/email/email_confirmation_message copy.html @@ -1,9 +1,9 @@ - @@ -17,7 +17,7 @@ {% user_display user as user_display %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}
-

Welcome to {{ site_domain }} {{ site_name }}!

+

Welcome to {{ site_domain }} {{ site_name }}!

Welcome to CodersHQ πŸ‘‹

Welcome to CodersHQ πŸ‘‹ Click to Verify Email - +

If you’re having trouble clicking the "Verify Email Address" button, copy and paste the URL below into your web browser: {{ activate_url }}

@@ -61,7 +61,7 @@

Welcome to CodersHQ πŸ‘‹