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.
|
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
|
||||||
|
|
||||||
Naming convention depends on your taste. It pays off to have something consistent throughout a project.
|
Naming convention depends on your taste. It pays off to have something consistent throughout a project.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user