mirror of
				https://github.com/MarcZierle/photo-log-backend.git
				synced 2025-10-30 23:34:57 +00:00 
			
		
		
		
	merge changes from master
This commit is contained in:
		
						commit
						28f27080fd
					
				
							
								
								
									
										1
									
								
								api/autocrop
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										1
									
								
								api/autocrop
									
									
									
									
									
										Submodule
									
								
							| @ -0,0 +1 @@ | |||||||
|  | Subproject commit 70828ba4e14e67a1db819de5c6371713145f868c | ||||||
| @ -1,3 +1,4 @@ | |||||||
|  | from django.contrib.auth import get_user_model | ||||||
| from rest_framework import serializers | from rest_framework import serializers | ||||||
| from photo_log.models import ( | from photo_log.models import ( | ||||||
|         PhotoGroup, |         PhotoGroup, | ||||||
| @ -23,7 +24,7 @@ class PhotoLogTemplateSerializer(serializers.ModelSerializer): | |||||||
| class PhotoGroupSerializer(serializers.ModelSerializer): | class PhotoGroupSerializer(serializers.ModelSerializer): | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = PhotoGroup |         model = PhotoGroup | ||||||
|         fields = ('id', 'name', 'date') |         fields = ('id', 'name', 'date', 'parent') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PhotosSerializer(serializers.ModelSerializer): | class PhotosSerializer(serializers.ModelSerializer): | ||||||
| @ -37,7 +38,7 @@ class PhotoSerializer(serializers.ModelSerializer): | |||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Photo |         model = Photo | ||||||
|         fields = ('id', 'legacy_id', 'group', 'bbox_coords', 'rotate', 'intersections', 'original_image', 'cropped_image', 'ocr_text', 'tag') |         fields = ('id', 'owner', 'legacy_id', 'group', 'bbox_coords', 'rotate', 'intersections', 'original_image', 'cropped_image', 'ocr_text', 'tag') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class AddPhotoSerializer(serializers.ModelSerializer): | class AddPhotoSerializer(serializers.ModelSerializer): | ||||||
| @ -62,3 +63,8 @@ class PhotoLogsSerializer(serializers.ModelSerializer): | |||||||
|     class Meta: |     class Meta: | ||||||
|         model = PhotoLog |         model = PhotoLog | ||||||
|         fields = ('id', 'title', 'date', 'pdf') |         fields = ('id', 'title', 'date', 'pdf') | ||||||
|  | 
 | ||||||
|  | class UserSerializer(serializers.ModelSerializer): | ||||||
|  |     class Meta: | ||||||
|  |         model = get_user_model() | ||||||
|  |         fields = ('id', 'email') | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ from .views import ( | |||||||
|         PhotoLogTemplatesAPIView, |         PhotoLogTemplatesAPIView, | ||||||
|         CreatePhotoLogTemplateAPIView, |         CreatePhotoLogTemplateAPIView, | ||||||
|         RetrieveUpdateDestroyPhotoLogTemplateAPIView, |         RetrieveUpdateDestroyPhotoLogTemplateAPIView, | ||||||
|  |         UsersAPIView, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -47,4 +48,6 @@ urlpatterns = [ | |||||||
|     path('photolog/template/', CreatePhotoLogTemplateAPIView.as_view()), |     path('photolog/template/', CreatePhotoLogTemplateAPIView.as_view()), | ||||||
|     path('photolog/templates/', PhotoLogTemplatesAPIView.as_view()), |     path('photolog/templates/', PhotoLogTemplatesAPIView.as_view()), | ||||||
|     path('photolog/template/<int:pk>/', RetrieveUpdateDestroyPhotoLogTemplateAPIView.as_view()), |     path('photolog/template/<int:pk>/', RetrieveUpdateDestroyPhotoLogTemplateAPIView.as_view()), | ||||||
|  | 
 | ||||||
|  |     path('users/', UsersAPIView.as_view()), | ||||||
| ] | ] | ||||||
|  | |||||||
							
								
								
									
										33
									
								
								api/views.py
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								api/views.py
									
									
									
									
									
								
							| @ -1,6 +1,7 @@ | |||||||
| from rest_framework import generics, views, status | from rest_framework import generics, views, status | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from django.shortcuts import get_list_or_404 | from django.shortcuts import get_list_or_404 | ||||||
|  | from rest_framework.permissions import IsAuthenticated | ||||||
| from  photo_log.models import ( | from  photo_log.models import ( | ||||||
|         PhotoGroup, |         PhotoGroup, | ||||||
|         Photo, |         Photo, | ||||||
| @ -18,6 +19,7 @@ from .serializers import ( | |||||||
|         PhotoLogsSerializer, |         PhotoLogsSerializer, | ||||||
|         PhotoTagSerializer, |         PhotoTagSerializer, | ||||||
|         PhotoLogTemplateSerializer, |         PhotoLogTemplateSerializer, | ||||||
|  |         UserSerializer, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| from photo_log import tasks | from photo_log import tasks | ||||||
| @ -25,6 +27,7 @@ from photo_log import tasks | |||||||
| from django.db.models import FileField | from django.db.models import FileField | ||||||
| from django.core.files.uploadedfile import InMemoryUploadedFile | from django.core.files.uploadedfile import InMemoryUploadedFile | ||||||
| from django.core.files.base import ContentFile | from django.core.files.base import ContentFile | ||||||
|  | from django.contrib.auth import get_user_model | ||||||
| 
 | 
 | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
| from PIL import Image, ExifTags | from PIL import Image, ExifTags | ||||||
| @ -61,16 +64,31 @@ class RetrieveUpdateDestroyPhotoLogTemplateAPIView(generics.RetrieveUpdateDestro | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PhotoGroupAPIView(generics.ListAPIView): | class PhotoGroupAPIView(generics.ListAPIView): | ||||||
|     queryset = PhotoGroup.objects.all() |  | ||||||
|     serializer_class = PhotoGroupSerializer |     serializer_class = PhotoGroupSerializer | ||||||
|  |     permission_classes = [IsAuthenticated] | ||||||
|  | 
 | ||||||
|  |     def get_queryset(self): | ||||||
|  |         queryset = PhotoGroup.objects.all() | ||||||
|  | 
 | ||||||
|  |         user = self.request.user | ||||||
|  |         if not user.is_superuser: | ||||||
|  |             queryset = queryset.filter(owner=user) | ||||||
|  | 
 | ||||||
|  |         return queryset | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PhotosAPIView(generics.ListAPIView): | class PhotosAPIView(generics.ListAPIView): | ||||||
|      |      | ||||||
|     serializer_class = PhotoSerializer |     serializer_class = PhotoSerializer | ||||||
|  |     permission_classes = [IsAuthenticated] | ||||||
| 
 | 
 | ||||||
|     def get_queryset(self): |     def get_queryset(self): | ||||||
|         queryset = Photo.objects.all() |         queryset = Photo.objects.all() | ||||||
|  | 
 | ||||||
|  |         user = self.request.user | ||||||
|  |         if not user.is_superuser: | ||||||
|  |             queryset = queryset.filter(owner=user) | ||||||
|  | 
 | ||||||
|         self.serializer_class = PhotosSerializer |         self.serializer_class = PhotosSerializer | ||||||
| 
 | 
 | ||||||
|         photogroup = self.request.query_params.get('photogroup') |         photogroup = self.request.query_params.get('photogroup') | ||||||
| @ -89,11 +107,19 @@ class PhotoAPIView(generics.RetrieveAPIView): | |||||||
| class AddPhotoAPIView(generics.CreateAPIView): | class AddPhotoAPIView(generics.CreateAPIView): | ||||||
|     queryset = Photo.objects.all() |     queryset = Photo.objects.all() | ||||||
|     serializer_class = AddPhotoSerializer |     serializer_class = AddPhotoSerializer | ||||||
|  |     permission_classes = [IsAuthenticated] | ||||||
|  | 
 | ||||||
|  |     def perform_create(self, serializer): | ||||||
|  |         serializer.save(owner=self.request.user) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class AddPhotoGroupAPIView(generics.CreateAPIView): | class AddPhotoGroupAPIView(generics.CreateAPIView): | ||||||
|     queryset = PhotoGroup.objects.all() |     queryset = PhotoGroup.objects.all() | ||||||
|     serializer_class = PhotoGroupSerializer |     serializer_class = PhotoGroupSerializer | ||||||
|  |     permission_classes = [IsAuthenticated] | ||||||
|  | 
 | ||||||
|  |     def perform_create(self, serializer): | ||||||
|  |         serializer.save(owner=self.request.user) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PhotoLogAPIView(generics.RetrieveAPIView): | class PhotoLogAPIView(generics.RetrieveAPIView): | ||||||
| @ -220,3 +246,8 @@ class AutoCropPhotoAPIView(generics.RetrieveAPIView): | |||||||
|             return self.retrieve(request, *args, **kwargs) |             return self.retrieve(request, *args, **kwargs) | ||||||
| 
 | 
 | ||||||
|         return Response({"error": "Not Found"}, status=status.HTTP_404_NOT_FOUND) |         return Response({"error": "Not Found"}, status=status.HTTP_404_NOT_FOUND) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class UsersAPIView(generics.ListAPIView): | ||||||
|  |     queryset = get_user_model().objects.all() | ||||||
|  |     serializer_class = UserSerializer | ||||||
|  | |||||||
| @ -51,6 +51,7 @@ INSTALLED_APPS = [ | |||||||
|     'channels', # as high as possible (channels overloads 'runserver', may conflict with e.g. whitenoise) |     'channels', # as high as possible (channels overloads 'runserver', may conflict with e.g. whitenoise) | ||||||
|     'rest_framework', |     'rest_framework', | ||||||
|     'corsheaders', |     'corsheaders', | ||||||
|  |     'rest_framework_simplejwt', | ||||||
|     'drf_yasg', |     'drf_yasg', | ||||||
|     'storages', |     'storages', | ||||||
|     'django_extensions', |     'django_extensions', | ||||||
| @ -216,6 +217,7 @@ CELERY_EVENT_QUEUE_PREFIX = env('MSG_BROKER_PREFIX') | |||||||
| 
 | 
 | ||||||
| CELERY_TIMEZONE = 'CET' | CELERY_TIMEZONE = 'CET' | ||||||
| 
 | 
 | ||||||
|  | CELERY_TASK_DEFAULT_QUEUE = 'zierletraining_prod' | ||||||
| CELERY_BROKER_TRANSPORT_OPTIONS = { | CELERY_BROKER_TRANSPORT_OPTIONS = { | ||||||
|     'visibility_timeout': 300, |     'visibility_timeout': 300, | ||||||
| } | } | ||||||
| @ -250,3 +252,19 @@ CHANNEL_LAYERS = { | |||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | REST_FRAMEWORK = { | ||||||
|  |     'DEFAULT_AUTHENTICATION_CLASSES': ( | ||||||
|  |         'rest_framework.authentication.SessionAuthentication', | ||||||
|  |         'rest_framework.authentication.BasicAuthentication', | ||||||
|  |         'rest_framework_simplejwt.authentication.JWTAuthentication', | ||||||
|  |     ), | ||||||
|  |     'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', | ||||||
|  |     'PAGE_SIZE': 9999, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | from datetime import timedelta | ||||||
|  | 
 | ||||||
|  | SIMPLE_JWT = { | ||||||
|  |     'REFRESH_TOKEN_LIFETIME': timedelta(days=30), | ||||||
|  | } | ||||||
|  | |||||||
| @ -18,6 +18,13 @@ from django.urls import path, include | |||||||
| from django.views.generic import TemplateView | from django.views.generic import TemplateView | ||||||
| from django.conf.urls.static import static | from django.conf.urls.static import static | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django.http import HttpResponse | ||||||
|  | 
 | ||||||
|  | from rest_framework_simplejwt.views import ( | ||||||
|  |     TokenObtainPairView, | ||||||
|  |     TokenRefreshView, | ||||||
|  |     TokenVerifyView, | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| # API documentation | # API documentation | ||||||
| from rest_framework import permissions | from rest_framework import permissions | ||||||
| @ -27,6 +34,12 @@ from drf_yasg import openapi | |||||||
| 
 | 
 | ||||||
| api_patterns = [ | api_patterns = [ | ||||||
|     path('api/v1/', include('api.urls')), |     path('api/v1/', include('api.urls')), | ||||||
|  |     path('api/v1/api-auth/', include('rest_framework.urls')), | ||||||
|  |     path('api/v1/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), | ||||||
|  |     path('api/v1/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), | ||||||
|  |     path('api/v1/token/verify/', TokenVerifyView.as_view(), name='token_verify'), | ||||||
|  | 
 | ||||||
|  |     path('api/v1/ping/', lambda request: HttpResponse('pong'), name='ping_pong'), | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| from django.contrib.postgres.fields import ArrayField | from django.contrib.postgres.fields import ArrayField | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
|  | from django.contrib.auth import get_user_model | ||||||
| 
 | 
 | ||||||
| from model_utils import FieldTracker | from model_utils import FieldTracker | ||||||
| 
 | 
 | ||||||
| @ -14,6 +15,9 @@ import os | |||||||
| import uuid | import uuid | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | UserModel = get_user_model() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class PhotoTag(models.Model): | class PhotoTag(models.Model): | ||||||
|     name = models.CharField(unique=True, null=False, blank=False, max_length=100) |     name = models.CharField(unique=True, null=False, blank=False, max_length=100) | ||||||
|     color = ColorField(default='#FFE5B4') |     color = ColorField(default='#FFE5B4') | ||||||
| @ -21,10 +25,20 @@ class PhotoTag(models.Model): | |||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return self.name |         return self.name | ||||||
| 
 | 
 | ||||||
|  |     class Meta: | ||||||
|  |         ordering = ('name',) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class PhotoGroup(models.Model): | class PhotoGroup(models.Model): | ||||||
|     name = models.CharField(unique=True, null=False, max_length=200) |     name = models.CharField(unique=False, null=False, max_length=200) | ||||||
|     date = models.DateField(null=True) |     date = models.DateField(null=True) | ||||||
|  |     parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.SET_NULL) | ||||||
|  | 
 | ||||||
|  |     owner = models.ForeignKey( | ||||||
|  |         UserModel, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         related_name='photogroups', | ||||||
|  |     ) | ||||||
| 
 | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return self.name |         return self.name | ||||||
| @ -85,6 +99,12 @@ class Photo(models.Model): | |||||||
|         default=None, |         default=None, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  |     owner = models.ForeignKey( | ||||||
|  |         UserModel, | ||||||
|  |         on_delete=models.CASCADE, | ||||||
|  |         related_name='photos', | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|     tracker = FieldTracker() |     tracker = FieldTracker() | ||||||
| 
 | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| from celery import shared_task, chain, group, chord | from celery import shared_task, chain, group, chord | ||||||
| import boto3 | import boto3 | ||||||
|  | from boto3.s3.transfer import TransferConfig | ||||||
| import imghdr | import imghdr | ||||||
| from PIL import Image, ExifTags | from PIL import Image, ExifTags | ||||||
| 
 | 
 | ||||||
| @ -19,6 +20,15 @@ from .photolog_layout import generate_tex | |||||||
| from .autocrop.autocrop import autocrop | from .autocrop.autocrop import autocrop | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | s3_resource = boto3.resource( | ||||||
|  |     's3', | ||||||
|  |     endpoint_url=settings.AWS_S3_ENDPOINT_URL, | ||||||
|  |     aws_access_key_id=settings.AWS_ACCESS_KEY_ID, | ||||||
|  |     aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, | ||||||
|  | ) | ||||||
|  | client = s3_resource.meta.client | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @shared_task | @shared_task | ||||||
| def chordfinisher(*args, **kwargs): | def chordfinisher(*args, **kwargs): | ||||||
|     """ |     """ | ||||||
| @ -47,22 +57,21 @@ def download_s3_file(folder_path, s3_file_path, bucket): | |||||||
|     :param  bucket          The name of the bucket where the file is stored in. |     :param  bucket          The name of the bucket where the file is stored in. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|  |     global client | ||||||
|  | 
 | ||||||
|     # create local folder |     # create local folder | ||||||
|     if not os.path.exists(folder_path): |     if not os.path.exists(folder_path): | ||||||
|         os.makedirs(folder_path, exist_ok=True) # mkdir -p |         os.makedirs(folder_path, exist_ok=True) # mkdir -p | ||||||
| 
 | 
 | ||||||
|     s3_resource = boto3.resource( |  | ||||||
|         's3', |  | ||||||
|         endpoint_url=settings.AWS_S3_ENDPOINT_URL, |  | ||||||
|         aws_access_key_id=settings.AWS_ACCESS_KEY_ID, |  | ||||||
|         aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, |  | ||||||
|     ) |  | ||||||
|     client = s3_resource.meta.client |  | ||||||
| 
 |  | ||||||
|     # retrieve the file name if the file is stored in a sub dir on the S3 |     # retrieve the file name if the file is stored in a sub dir on the S3 | ||||||
|     file_name = s3_file_path.split('/')[-1] |     file_name = s3_file_path.split('/')[-1] | ||||||
| 
 | 
 | ||||||
|     client.download_file(bucket, s3_file_path, os.path.join(folder_path, file_name)) |     client.download_file( | ||||||
|  |             bucket, | ||||||
|  |             s3_file_path, | ||||||
|  |             os.path.join(folder_path, file_name), | ||||||
|  |             Config=TransferConfig(use_threads=False) | ||||||
|  |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @shared_task | @shared_task | ||||||
| @ -74,13 +83,7 @@ def upload_s3_file(file_path, s3_file_path, bucket, content_type='application/oc | |||||||
|     :param  bucket          The name of the bucket where the file will be stored in. |     :param  bucket          The name of the bucket where the file will be stored in. | ||||||
|     """ |     """ | ||||||
|      |      | ||||||
|     s3_resource = boto3.resource( |     global client | ||||||
|         's3', |  | ||||||
|         endpoint_url=settings.AWS_S3_ENDPOINT_URL, |  | ||||||
|         aws_access_key_id=settings.AWS_ACCESS_KEY_ID, |  | ||||||
|         aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, |  | ||||||
|     ) |  | ||||||
|     client = s3_resource.meta.client |  | ||||||
| 
 | 
 | ||||||
|     client.upload_file( |     client.upload_file( | ||||||
|         file_path, |         file_path, | ||||||
| @ -89,7 +92,8 @@ def upload_s3_file(file_path, s3_file_path, bucket, content_type='application/oc | |||||||
|         ExtraArgs={ |         ExtraArgs={ | ||||||
|             'ContentDisposition': 'inline', |             'ContentDisposition': 'inline', | ||||||
|             'ContentType': content_type, |             'ContentType': content_type, | ||||||
|         } |         }, | ||||||
|  |         Config=TransferConfig(use_threads=False) | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -162,6 +166,7 @@ def crop_image_bbox(image_path, bbox, rotate_angle): | |||||||
|             bbox[2][1] |             bbox[2][1] | ||||||
|         )) |         )) | ||||||
|      |      | ||||||
|  |     img = img.convert('RGB') | ||||||
|     img.save(image_path) |     img.save(image_path) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -192,15 +197,24 @@ def delete_folder(folder_path): | |||||||
| 
 | 
 | ||||||
| @shared_task | @shared_task | ||||||
| def generate_photolog_from_latex(title, date, render_date, start_slide_image, slides, id_to_name, work_dir, out_file): | def generate_photolog_from_latex(title, date, render_date, start_slide_image, slides, id_to_name, work_dir, out_file): | ||||||
|     template_name = 'photolog.tex' |     try: | ||||||
|     context = { |         template_name = 'photolog.tex' | ||||||
|         'content': generate_tex(title, date, render_date, start_slide_image, slides, id_to_name, work_dir) |         context = { | ||||||
|     } |             'content': generate_tex(title, date, render_date, start_slide_image, slides, id_to_name, work_dir) | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|     pdf_bytes = compile_template_to_pdf(template_name, context) |         pdf_bytes = compile_template_to_pdf(template_name, context) | ||||||
| 
 | 
 | ||||||
|     with open(out_file, 'wb+') as file: |         with open(out_file, 'wb+') as file: | ||||||
|         file.write(pdf_bytes) |             file.write(pdf_bytes) | ||||||
|  |     except Exception as e: | ||||||
|  |         notify_client( | ||||||
|  |             description='error', | ||||||
|  |             content={ | ||||||
|  |                 'exception': str(e), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         raise e | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def max_resize_image_chain(full_file_name, max_width, work_folder_name=uuid4()): | def max_resize_image_chain(full_file_name, max_width, work_folder_name=uuid4()): | ||||||
| @ -381,7 +395,8 @@ def generate_photo_log_chain(photo_log_id, work_folder_name=uuid4()): | |||||||
| 
 | 
 | ||||||
|     download_files_tasks.extend(get_photo_log_assets_tasks()) |     download_files_tasks.extend(get_photo_log_assets_tasks()) | ||||||
|          |          | ||||||
|     download_files_tasks = chord( group(download_files_tasks), chordfinisher.si() ) |     #download_files_tasks = chord( group(download_files_tasks), chordfinisher.si() ) | ||||||
|  |     download_files_tasks = chain(download_files_tasks) | ||||||
| 
 | 
 | ||||||
|     pdf_file = photo_log.pdf.name |     pdf_file = photo_log.pdf.name | ||||||
|     if pdf_file: |     if pdf_file: | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 MarcZierle
						MarcZierle