mirror of
https://github.com/HackSoftware/Django-Styleguide.git
synced 2024-11-25 11:03:46 +03:00
Add an example for class-based service
This commit is contained in:
parent
246c5916db
commit
be20a60cb0
114
README.md
114
README.md
|
@ -516,6 +516,120 @@ As you can see, this service calls 2 other services - `profile_create` and `conf
|
|||
|
||||
In this example, everything related to the user creation is in one place and can be traced.
|
||||
|
||||
**Additionally, we can have "class-based" services**, which is a fancy way of saying - wrap the logic in a class.
|
||||
|
||||
Here's an example, taken straight from the [Django Styleguide Example](https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/services.py#L22), related to file upload:
|
||||
|
||||
```python
|
||||
class FileDirectUploadService:
|
||||
"""
|
||||
This also serves as an example of a service class,
|
||||
which encapsulates 2 different behaviors (create & update) under a namespace.
|
||||
|
||||
Meaning, we use the class here for:
|
||||
|
||||
1. The namespace
|
||||
2. The ability to reuse `_infer_file_name_and_type` (which can also be an util)
|
||||
"""
|
||||
def __init__(self, user: BaseUser, file_obj):
|
||||
self.user = user
|
||||
self.file_obj = file_obj
|
||||
|
||||
def _infer_file_name_and_type(self, file_name: str = "", file_type: str = "") -> Tuple[str, str]:
|
||||
if not file_name:
|
||||
file_name = self.file_obj.name
|
||||
|
||||
if not file_type:
|
||||
guessed_file_type, encoding = mimetypes.guess_type(file_name)
|
||||
|
||||
if guessed_file_type is None:
|
||||
file_type = ""
|
||||
else:
|
||||
file_type = guessed_file_type
|
||||
|
||||
return file_name, file_type
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, file_name: str = "", file_type: str = "") -> File:
|
||||
file_name, file_type = self._infer_file_name_and_type(file_name, file_type)
|
||||
|
||||
obj = File(
|
||||
file=self.file_obj,
|
||||
original_file_name=file_name,
|
||||
file_name=file_generate_name(file_name),
|
||||
file_type=file_type,
|
||||
uploaded_by=self.user,
|
||||
upload_finished_at=timezone.now()
|
||||
)
|
||||
|
||||
obj.full_clean()
|
||||
obj.save()
|
||||
|
||||
return obj
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, file: File, file_name: str = "", file_type: str = "") -> File:
|
||||
file_name, file_type = self._infer_file_name_and_type(file_name, file_type)
|
||||
|
||||
file.file = self.file_obj
|
||||
file.original_file_name = file_name
|
||||
file.file_name = file_generate_name(file_name)
|
||||
file.file_type = file_type
|
||||
file.uploaded_by = self.user
|
||||
file.upload_finished_at = timezone.now()
|
||||
|
||||
file.full_clean()
|
||||
file.save()
|
||||
|
||||
return file
|
||||
```
|
||||
|
||||
As stated in the comment, we are using this approach for 2 main reasons:
|
||||
|
||||
1. **Namespace.** We have a single namespace for our create & update.
|
||||
1. **Reuse** of the `_infer_file_name_and_type` logic.
|
||||
|
||||
Here's how this service is used:
|
||||
|
||||
```python
|
||||
# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/apis.py
|
||||
|
||||
class FileDirectUploadApi(ApiAuthMixin, APIView):
|
||||
def post(self, request):
|
||||
service = FileDirectUploadService(
|
||||
user=request.user,
|
||||
file_obj=request.FILES["file"]
|
||||
)
|
||||
file = service.create()
|
||||
|
||||
return Response(data={"id": file.id}, status=status.HTTP_201_CREATED)
|
||||
```
|
||||
|
||||
And
|
||||
|
||||
```python
|
||||
@admin.register(File)
|
||||
class FileAdmin(admin.ModelAdmin):
|
||||
# ... other code here ...
|
||||
# https://github.com/HackSoftware/Django-Styleguide-Example/blob/master/styleguide_example/files/admin.py
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
try:
|
||||
cleaned_data = form.cleaned_data
|
||||
|
||||
service = FileDirectUploadService(
|
||||
file_obj=cleaned_data["file"],
|
||||
user=cleaned_data["uploaded_by"]
|
||||
)
|
||||
|
||||
if change:
|
||||
service.update(file=obj)
|
||||
else:
|
||||
service.create()
|
||||
except ValidationError as exc:
|
||||
self.message_user(request, str(exc), messages.ERROR)
|
||||
```
|
||||
|
||||
### Naming convention
|
||||
|
||||
Naming convention depends on your taste. It pays off to have something consistent throughout a project.
|
||||
|
|
Loading…
Reference in New Issue
Block a user