This is a pre-ADR doc, notes from the meeting about django-app design patterns.
The objective is to write clear code.
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
)
For everything else, use RESTful views. Advantages: declarativity, consistency throughout the app, allows using permission classes.
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"],
)
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,
)
We should always try to keep it as "ONE FILE == ONE VIEW + ONE SERIALIZER".
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
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
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.
- 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."
)
- Test the serializer seperately, test all
SerializerMethodField
s, 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/"
)
- Test function based views that render templates: for permissions failures, flags, accessability status_200_ok
No example :)
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 |