Skip to content

Commit

Permalink
Merge pull request theriverman#18 from theriverman/gh-adding-tests
Browse files Browse the repository at this point in the history
Adding GH workflow config + Django unit tests
  • Loading branch information
theriverman authored Mar 13, 2021
2 parents 01c37bc + 45b3862 commit 4dd15a7
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 30 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/django-tests.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: publish-py-dist-to-pypi
name: PyPI Publish

on:
push:
Expand Down
35 changes: 35 additions & 0 deletions DjangoExampleApplication/migrations/0002_auto_20210313_1049.py
Original file line number Diff line number Diff line change
@@ -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"),
),
]
16 changes: 12 additions & 4 deletions DjangoExampleApplication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
88 changes: 88 additions & 0 deletions DjangoExampleApplication/tests.py
Original file line number Diff line number Diff line change
@@ -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)
Empty file.
15 changes: 0 additions & 15 deletions DjangoExampleApplication/tests/test_image_upload.py

This file was deleted.

9 changes: 5 additions & 4 deletions DjangoExampleProject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

import os
import distutils.util
from datetime import timedelta
from typing import List, Tuple

Expand Down Expand Up @@ -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',
]
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -14,15 +16,15 @@ 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
]
```

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
]
```
Expand Down Expand Up @@ -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*.
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Django>=2.2.2
minio>=7.0.0
minio>=7.0.2
Pillow
setuptools
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
author_email='[email protected]',
install_requires=[
'Django>=2.2.2',
'minio>=7.0.0'
'minio>=7.0.2'
],
classifiers=[
'Environment :: Web Environment',
Expand Down

0 comments on commit 4dd15a7

Please sign in to comment.