diff --git a/.github/workflows/django-tests.yml b/.github/workflows/django-tests.yml new file mode 100644 index 0000000..2b67f90 --- /dev/null +++ b/.github/workflows/django-tests.yml @@ -0,0 +1,35 @@ +name: Django Unit Tests + +on: + push: + branches: + - '**' + +jobs: + job-run-django-app-tests: + name: Deploy DjangoExampleProject and run its integrated tests + runs-on: ubuntu-latest + steps: + # Checkout the repository + - uses: actions/checkout@v2 + # Start the minIO container + - name: Start the minIO container + run: docker run --name miniotest -p 9000:9000 -d minio/minio server /data + # Setup Python + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + # Install Dependencies + - name: Install pypa/build + run: >- + python -m + pip install + -r + requirements.txt + # Setup Django + - name: Deploy DjangoExampleProject + run: python manage.py migrate + # Run Django Tests + - name: Run Django unit tests + run: python manage.py test diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 4198d04..3e6e02f 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -1,4 +1,4 @@ -name: publish-py-dist-to-pypi +name: PyPI Publish on: push: diff --git a/DjangoExampleApplication/migrations/0002_auto_20210313_1049.py b/DjangoExampleApplication/migrations/0002_auto_20210313_1049.py new file mode 100644 index 0000000..8f00d6d --- /dev/null +++ b/DjangoExampleApplication/migrations/0002_auto_20210313_1049.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.3 on 2021-03-13 10:49 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('DjangoExampleApplication', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='privateattachment', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Content Type'), + ), + migrations.AlterField( + model_name='privateattachment', + name='object_id', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Related Object's ID"), + ), + migrations.AlterField( + model_name='publicattachment', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Content Type'), + ), + migrations.AlterField( + model_name='publicattachment', + name='object_id', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Related Object's ID"), + ), + ] diff --git a/DjangoExampleApplication/models.py b/DjangoExampleApplication/models.py index 564ed7f..5de4e5a 100644 --- a/DjangoExampleApplication/models.py +++ b/DjangoExampleApplication/models.py @@ -21,6 +21,14 @@ class Image(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) image = models.ImageField(upload_to=iso_date_prefix, storage=MinioBackend(bucket_name='django-backend-dev-public')) + def delete(self, *args, **kwargs): + """ + Delete must be overridden because the inherited delete method does not call `self.file.delete()`. + """ + # noinspection PyUnresolvedReferences + self.image.delete() + super(Image, self).delete(*args, **kwargs) + # Create your models here. class PublicAttachment(models.Model): @@ -54,9 +62,9 @@ def __str__(self): return str(self.file) id = models.AutoField(primary_key=True, verbose_name="Public Attachment ID") - content_type: ContentType = models.ForeignKey(ContentType, null=False, blank=False, on_delete=models.CASCADE, + content_type: ContentType = models.ForeignKey(ContentType, null=True, blank=True, on_delete=models.CASCADE, verbose_name="Content Type") - object_id = models.PositiveIntegerField(null=False, blank=False, verbose_name="Related Object's ID") + object_id = models.PositiveIntegerField(null=True, blank=True, verbose_name="Related Object's ID") content_object = GenericForeignKey("content_type", "object_id") file: FieldFile = models.FileField(verbose_name="Object Upload", @@ -97,9 +105,9 @@ def __str__(self): return str(self.file) id = models.AutoField(primary_key=True, verbose_name="Public Attachment ID") - content_type: ContentType = models.ForeignKey(ContentType, null=False, blank=False, on_delete=models.CASCADE, + content_type: ContentType = models.ForeignKey(ContentType, null=True, blank=True, on_delete=models.CASCADE, verbose_name="Content Type") - object_id = models.PositiveIntegerField(null=False, blank=False, verbose_name="Related Object's ID") + object_id = models.PositiveIntegerField(null=True, blank=True, verbose_name="Related Object's ID") content_object = GenericForeignKey("content_type", "object_id") file: FieldFile = models.FileField(verbose_name="Object Upload", diff --git a/DjangoExampleApplication/tests.py b/DjangoExampleApplication/tests.py new file mode 100644 index 0000000..df8bf7c --- /dev/null +++ b/DjangoExampleApplication/tests.py @@ -0,0 +1,88 @@ +import time +from pathlib import Path +from django.conf import settings +from django.core.files import File +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase +from django.core.validators import URLValidator + +from DjangoExampleApplication.models import Image, PublicAttachment, PrivateAttachment + + +test_file_path = Path(settings.BASE_DIR) / "DjangoExampleApplication" / "assets" / "audience-868074_1920.jpg" +test_file_size = 339085 + + +class ImageTestCase(TestCase): + obj: Image = None + + def setUp(self): + # Open a test file from disk and upload to minIO as an image + with open(test_file_path, 'rb') as f: + self.obj = Image.objects.create() + self.obj.image.save(name='audience-868074_1920.jpg', content=f) + + def tearDown(self): + # Remove uploaded file from minIO and remove the Image entry from Django's database + self.obj.delete() # deletes from both locations + + def test_url_generation_works(self): + """Accessing the value of obj.image.url""" + val = URLValidator() + val(self.obj.image.url) # 1st make sure it's an URL + self.assertTrue('audience-868074_1920' in self.obj.image.url) # 2nd make sure our filename matches + + def test_read_image_size(self): + self.assertEqual(self.obj.image.size, test_file_size) + + +class PublicAttachmentTestCase(TestCase): + obj: PublicAttachment = None + filename = f'public_audience-868074_1920_{int(time.time())}.jpg' # adding unix time makes our filename unique + + def setUp(self): + ct = ContentType.objects.get(app_label='auth', model='user') # PublicAttachment is generic so this is needed + with open(test_file_path, 'rb') as f: + # noinspection PyUnresolvedReferences + self.obj = PublicAttachment.objects.create() + self.obj.ct = ct + self.obj.object_id = 1 # we associate this uploaded file to user with pk=1 + self.obj.file.save(name=self.filename, content=File(f), save=True) + + def test_url_generation_works(self): + """Accessing the value of obj.file.url""" + val = URLValidator() + val(self.obj.file.url) # 1st make sure it's an URL + self.assertTrue('public_audience-868074_1920' in self.obj.file.url) # 2nd make sure our filename matches + + def test_read_file_size(self): + self.assertEqual(self.obj.file_size, test_file_size) + + def test_read_file_name(self): + self.assertEqual(self.obj.file_name, self.filename) + + +class PrivateAttachmentTestCase(TestCase): + obj: PrivateAttachment = None + filename = f'private_audience-868074_1920_{int(time.time())}.jpg' # adding unix time makes our filename unique + + def setUp(self): + ct = ContentType.objects.get(app_label='auth', model='user') # PublicAttachment is generic so this is needed + with open(test_file_path, 'rb') as f: + # noinspection PyUnresolvedReferences + self.obj = PublicAttachment.objects.create() + self.obj.ct = ct + self.obj.object_id = 1 # we associate this uploaded file to user with pk=1 + self.obj.file.save(name=self.filename, content=File(f), save=True) + + def test_url_generation_works(self): + """Accessing the value of obj.file.url""" + val = URLValidator() + val(self.obj.file.url) # 1st make sure it's an URL + self.assertTrue('private_audience-868074_1920' in self.obj.file.url) # 2nd make sure our filename matches + + def test_read_file_size(self): + self.assertEqual(self.obj.file_size, test_file_size) + + def test_read_file_name(self): + self.assertEqual(self.obj.file_name, self.filename) diff --git a/DjangoExampleApplication/tests/__init__.py b/DjangoExampleApplication/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/DjangoExampleApplication/tests/test_image_upload.py b/DjangoExampleApplication/tests/test_image_upload.py deleted file mode 100644 index 3bb4a72..0000000 --- a/DjangoExampleApplication/tests/test_image_upload.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.test import TestCase - -from DjangoExampleApplication.models import Image - - -class AnimalTestCase(TestCase): - def setUp(self): - with open('DjangoExampleApplication/assets/audience-868074_1920.jpg', 'rb') as f: - img = Image.objects.create() - img.image.save(name='audience-868074_1920.jpg', content=f) - - def test_url_generation_works(self): - """Animals that can speak are correctly identified""" - img = Image.objects.last() - self.assertTrue('audience-868074_1920' in img.image.url) diff --git a/DjangoExampleProject/settings.py b/DjangoExampleProject/settings.py index 5403954..81caca4 100644 --- a/DjangoExampleProject/settings.py +++ b/DjangoExampleProject/settings.py @@ -11,6 +11,7 @@ """ import os +import distutils.util from datetime import timedelta from typing import List, Tuple @@ -145,10 +146,10 @@ } ]} -MINIO_ENDPOINT = 'play.min.io' -MINIO_ACCESS_KEY = 'Q3AM3UQ867SPQQA43P2F' -MINIO_SECRET_KEY = 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG' -MINIO_USE_HTTPS = True +MINIO_ENDPOINT = os.getenv("GH_MINIO_ENDPOINT", "play.min.io") +MINIO_ACCESS_KEY = os.getenv("GH_MINIO_ACCESS_KEY", "Q3AM3UQ867SPQQA43P2F") +MINIO_SECRET_KEY = os.getenv("GH_MINIO_SECRET_KEY", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG") +MINIO_USE_HTTPS = bool(distutils.util.strtobool(os.getenv("GH_MINIO_USE_HTTPS", "true"))) MINIO_PRIVATE_BUCKETS = [ 'django-backend-dev-private', ] diff --git a/README.md b/README.md index 8cf89cc..f9b3af7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -[![Actions Status](https://github.com/theriverman/django-minio-backend/workflows/publish-py-dist-to-pypi/badge.svg)](https://github.com/theriverman/django-minio-backend/actions) +[![django-app-tests](https://github.com/theriverman/django-minio-backend/actions/workflows/django-tests.yml/badge.svg)](https://github.com/theriverman/django-minio-backend/actions/workflows/django-tests.yml) +[![publish-py-dist-to-pypi](https://github.com/theriverman/django-minio-backend/actions/workflows/publish-to-pypi.yml/badge.svg)](https://github.com/theriverman/django-minio-backend/actions/workflows/publish-to-pypi.yml) [![PYPI](https://img.shields.io/pypi/v/django-minio-backend.svg)](https://pypi.python.org/pypi/django-minio-backend) # django-minio-backend The **django-minio-backend** provides a wrapper around the [MinIO Python SDK](https://docs.min.io/docs/python-client-quickstart-guide.html). +See [minio/minio-py](https://github.com/minio/minio-py) for the source. ## Integration 1. Get and install the package: @@ -14,7 +16,7 @@ pip install django-minio-backend 2. Add `django_minio_backend` to `INSTALLED_APPS`: ```python INSTALLED_APPS = [ - '...' + # '...' 'django_minio_backend', # https://github.com/theriverman/django-minio-backend ] ``` @@ -22,7 +24,7 @@ INSTALLED_APPS = [ If you would like to enable on-start consistency check, install via `DjangoMinioBackendConfig`: ```python INSTALLED_APPS = [ - '...' + # '...' 'django_minio_backend.apps.DjangoMinioBackendConfig', # https://github.com/theriverman/django-minio-backend ] ``` @@ -111,7 +113,7 @@ For a reference implementation, see [Examples](examples). ## Compatibility * Django 2.2 or later * Python 3.6.0 or later - * MinIO SDK 7.0.0 or later + * MinIO SDK 7.0.2 or later **Note:** This library relies heavily on [PEP 484 -- Type Hints](https://www.python.org/dev/peps/pep-0484/) which was introduced in *Python 3.5.0*. diff --git a/requirements.txt b/requirements.txt index acb5598..c596115 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Django>=2.2.2 -minio>=7.0.0 +minio>=7.0.2 Pillow setuptools diff --git a/setup.py b/setup.py index 992a68c..444a11f 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ author_email='kristof@daja.hu', install_requires=[ 'Django>=2.2.2', - 'minio>=7.0.0' + 'minio>=7.0.2' ], classifiers=[ 'Environment :: Web Environment',