Skip to content

Instantly share code, notes, and snippets.

@chisler
Last active November 16, 2022 07:29
Show Gist options
  • Save chisler/2fc59d7129c4a5424c461203649cca84 to your computer and use it in GitHub Desktop.
Save chisler/2fc59d7129c4a5424c461203649cca84 to your computer and use it in GitHub Desktop.
Approach to designing Django Views

Django Views

This is a pre-ADR doc, notes from the meeting about django-app design patterns.

JSON-like views vs Page-views

The objective is to write clear code.

Function based views

Use function based views for serving pages, react apps

Open function based view example

@waffle_flag("wip_frm")
@require_GET
@yj_user_passes_test(lambda u: u.is_agency)
def contacts(request, **kwargs):
    """Renders a page with a react app for Employer Contacts (FRM tool)"""
    agency = request.user.agency
    # as FRM is based on black book contacts, we check that "black_book" feature is enabled
    if not is_feature_setting_active("black_book", agency):
        raise Http404

    context = {"selected_nav": ["contacts"]}
    return render(
        request, template_name="contacts/employer_contacts.html", context=context
    )

DRF (Django Rest Framework) views

For everything else, use RESTful views. Advantages: declarativity, consistency throughout the app, allows using permission classes.

RESTful API endpoints

For the API endoints that serve/change specific objects within JSON-like response we should utilize Django Rest Framework features, inherit from RetrieveAPIView, ListAPIView, CreateAPIView, UpdateAPIView.

Open DRF class view example

class ContactDetailsApi(RetrieveAPIView):
    """
    Returns summary info for a particular Employer's contact
    """

    serializer_class = ContactDetailsSerializer
    permission_classes = (IsAgencyWithBlackBook,)

    def get_object(self):
        agency = self.request.user.agency
        return get_object_or_404(
            EmployerContact.objects.live()
            .filter(employer_id=agency.employer_id)
            .select_related("freelancer__user")
            .prefetch_related(
                Prefetch(
                    "employee_contacts",
                    EmployeeContact.live_objects.filter(
                        employee=agency
                    ).prefetch_related(
                        Prefetch("notes", BlackBookNote.objects.order_by("-created_at"))
                    ),
                    to_attr="employee_contact",
                )
            ),
            freelancer_id=self.kwargs["freelancer_id"],
        )

Action based endpoints

For action based views, like save_draft() we also use class based views, but we inherit from GenericAPIView

Open GenericAPIView usage example

class ContactNotesApi(GenericAPIView):
    """
    Allows to manage notes for a particular freelancer
    """

    permission_classes = (IsAgencyWithBlackBook,)

    def post(self, request, *args, **kwargs):
        validator = AddNoteValidator(
            data=request.data, context=self.get_serializer_context()
        )
        validator.is_valid(raise_exception=True)

        agency = request.user.agency
        contact = get_object_or_404(
            EmployeeContact.objects.live().filter(employee=agency),
            parent__freelancer_id=self.kwargs["freelancer_id"],
        )

        note = add_contact_note(
            contact=contact,
            comment=validator.validated_data["comment"],
            added_by=request.user,
        )

        return response.Response(
            data=NoteSerializer(
                instance=note, context=self.get_serializer_context()
            ).data,
            status=status.HTTP_201_CREATED,
        )

Where to put views and serializers

We should always try to keep it as "ONE FILE == ONE VIEW + ONE SERIALIZER".

App structure

This is the app example. By the time you read this, it may not exist in YunoJuno app.

contacts/
    api/                            <<< api means REST-like class-based views
        __init__.py
        contact_details.py          <<< this file has both Serializer and a View class
    commands/
        __init__.py
        notes.py                    <<< helper functions that mutate data
    queries/
        __init__.py
        contact_stats.py            <<< helper functions for querying data
    views/
        __init__.py
        employer_contacts.py        <<< function based view that serves the data
        contact_details/            
            details_create.py       <<< if multiple serializers are used in 2 different views — of the same app — you can create a folder for view files, take common serializers to a separate
            details_edit.py         <<< only view and view specific serializer files aer here
            serializers.py          <<< common serializers, try not to use it, don't worry about code duplication too much. 
    __init__.py
    urls.py

Serializers

We discourage reusing/inheriting serializers throughout django apps, because it has caused a lot of pain and unwanted complexity.

In order to minimize reusing, we put serializer in the view file, make nested serializers private and put them as nested classes to the part class.

Open the example of a nested serializer

class ContactProfileSerializer(serializers.Serializer):
    """Serializes freelancer's profile"""

    class _Brief(serializers.Serializer):
        id = serializers.IntegerField()
        title = serializers.CharField()

    class _ProfileUrl(serializers.Serializer):
        name = serializers.CharField()
        category = serializers.CharField()
        url = serializers.URLField()

    class _WorkHistoryRecord(serializers.Serializer):
        class _Validation(serializers.Serializer):
            comment = serializers.CharField()
            validator_name = serializers.CharField(source="validator.get_full_name")

        validation_state = serializers.CharField()
        validations = serializers.SerializerMethodField()
        employer_name = serializers.CharField(source="organisation")
        role = serializers.CharField()
        industry_sectors = serializers.SerializerMethodField()
        period = serializers.SerializerMethodField()

        def get_industry_sectors(self, whr):
            return [s.name for s in whr.industry_sectors.all()]

        def get_period(self, whr):
            start_date, end_date = whr.start_date, whr.end_date
            if start_date:
                if end_date and end_date != start_date:
                    return f"{start_date:%b %Y} \u2013 {end_date:%b %Y}"
                return f"{start_date:%b %Y}"
            return None

        def get_validations(self, whr):
            # show all positive and published validations
            # also show all validations from
            q = Q(published=True, verdict=WorkHistoryValidation.EXPERIENCE_POSITIVE)
            user = self.context["request"].user
            if user.is_agency:
                q = q | Q(validator__agency__employer=user.agency.employer_id)

            validations = whr.get_responder_validations().filter(q)
            return self._Validation(validations, many=True).data

    class _Recommendation(serializers.Serializer):
        quote = serializers.CharField(source="reference")
        referee_name = serializers.CharField()
        referee_job = serializers.CharField()

    id = serializers.IntegerField()
    avatar_url = serializers.CharField()
    is_approved = serializers.BooleanField()
    full_name = serializers.CharField()
    job_title = serializers.CharField(source="jobtitle")
    summary = serializers.CharField()
    profile_quote = serializers.CharField(source="other_skills")
    additional_work_highlights = serializers.CharField(source="work_history")
    cv_url = serializers.SerializerMethodField()
    skills = serializers.SerializerMethodField()
    location = serializers.SerializerMethodField()
    briefs_available = serializers.SerializerMethodField()
    work_history = serializers.SerializerMethodField()
    urls = _ProfileUrl(many=True)
    recommendations = _Recommendation(source="published_references", many=True)

    @property
    def agency(self):
        return self.context["request"].user.agency

    def get_cv_url(self, freelancer):
        if freelancer.cv:
            return reverse(
                "freelancer_profile_download_cv", kwargs={"slug": freelancer.slug}
            )
        return None

    def get_skills(self, freelancer):
        return [s.name for s in freelancer.skills.all()]

    def get_location(self, freelancer):
        city, country = freelancer.city.name, freelancer.country
        if city.lower() != "other":
            if country != "XX":
                return f"{city}, {country}"
            return city
        return None

    def get_briefs_available(self, freelancer):
        return self._Brief(
            get_matching_agency_briefs_for_freelancer(self.agency, freelancer),
            many=True,
        ).data

    def get_work_history(self, freelancer):
        whrs = freelancer.live_work_history_records.prefetch_related("industry_sectors")
        return self._WorkHistoryRecord(whrs, many=True, context=self.context).data


class ContactProfileApi(RetrieveAPIView):
    """
    Returns profile info for a particular Employer's contact
    """

    serializer_class = ContactProfileSerializer
    permission_classes = (IsAgencyWithBlackBook,)

    def get_object(self):
        agency = self.request.user.agency

        contact = get_object_or_404(
            EmployerContact.objects.live()
            .filter(employer_id=agency.employer_id)
            .select_related("freelancer__user", "freelancer__city")
            .prefetch_related("freelancer__skills", "freelancer__user__profile_urls"),
            freelancer_id=self.kwargs["freelancer_id"],
        )

        return contact.freelancer

Important note

If the serializers grows too fat, take private serializers out of class, but keep it in the view file and make it private (_ClassName) to discourage reusing.

Never use 3 level deep nesting serializer classes. If you think you should do this, either think of other data structure, flatten data or take serializers out of the class to the file-level.

Testing views

JSON-like APIS

  1. Use APIRequestFactory for api testing, test permission failures and all possible business logic usecases, don't test the serializer here
Open example

@pytest.mark.django_db
class TestContactProfileApi:
    def test_access__require_black_book(self):
        agency = ApprovedAgencyProfileFactory(employer__settings__black_book=False)
        request = APIRequestFactory().get("/api-url")
        request.user = agency.user
        view = ContactProfileApi.as_view()

        response = view(request, freelancer_id=123)

        assert response.status_code == 403
        assert response.data["detail"].code == "permission_denied"
        assert (
            response.data["detail"]
            == "You do not have permission to perform this action."
        )

  1. Test the serializer seperately, test all SerializerMethodFields, test Validators
Open serializer testing example

@pytest.mark.django_db
class TestContactProfileSerializer:
    def test_get_cv_url__no_cv(self):
        freelancer = ApprovedFreelancerProfileFactory(cv=None)
        assert ContactProfileSerializer().get_cv_url(freelancer) is None

    def test_get_cv_url__link_to_download_cv(self):
        freelancer = ApprovedFreelancerProfileFactory(cv="/link")
        assert (
            ContactProfileSerializer().get_cv_url(freelancer)
            == f"/profile/p/{freelancer.slug}/cv/"
        )

  1. Test function based views that render templates: for permissions failures, flags, accessability status_200_ok

No example :)

How to pass data from BE to FE

I'm glad you are still reading this doc!

For React apps, all the page-specific data should be loaded asynchronously, that's why.

With React SPA, we serve the js bundle from one django view, and then route it on the frontend side. These routes will require different data. Let's say we're building Marketplace app which has BriefCreation, Search and Shortlist page. We don't need to use ElasticSearch to render shortlist page.

Page data like "Disciplines", "Industries", "FormOptions" can be passed to the template, but search should not be fired on Shortlist page load => we do it the async way.

Exceptions: there might be exception to this rule. If the page takes ages to load something async way, we should consider putting data to the template.

Data What to do
Form options, current user information, things that can be reused in different react app routes, or if it takes ages to load, asynchronously Pass from the function based view to the template, through YJ object
Page specific data (doesn't have to be separate requests for different data) Async load with get request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment