diff --git a/dispatch/api/serializers.py b/dispatch/api/serializers.py index 20c880da5..7443ae54b 100644 --- a/dispatch/api/serializers.py +++ b/dispatch/api/serializers.py @@ -3,6 +3,7 @@ from rest_framework.validators import UniqueValidator from django.conf import settings +import json from dispatch.modules.content.models import ( Article, Image, ImageAttachment, ImageGallery, Issue, @@ -93,7 +94,7 @@ class Meta: ) def create(self, validated_data): - instance = User.objects.create_user(validated_data['email'], validated_data['password_a'], validated_data['permission_level'], validated_data['person']) + instance = User.objects.create_user(validated_data['email'], validated_data['password_a'], validated_data['permission_level']) return self.update(instance, validated_data) def update(self, instance, validated_data): @@ -211,6 +212,8 @@ class VideoSerializer(DispatchModelSerializer): authors = AuthorSerializer(many=True, read_only=True) author_ids = serializers.ListField( write_only=True, + allow_empty=False, + required=True, child=serializers.JSONField(), validators=[AuthorValidator(True)]) @@ -266,14 +269,15 @@ class ImageSerializer(serializers.HyperlinkedModelSerializer): authors = AuthorSerializer(many=True, read_only=True) author_ids = serializers.ListField( write_only=True, + allow_empty=False, + required=True, child=serializers.JSONField(), validators=[AuthorValidator(False)]) tags = TagSerializer(many=True, read_only=True) tag_ids = serializers.ListField( write_only=True, - required=False, - child=serializers.IntegerField()) + required=False) width = serializers.IntegerField(read_only=True) height = serializers.IntegerField(read_only=True) @@ -305,11 +309,21 @@ def update(self, instance, validated_data): instance = super(ImageSerializer, self).update(instance, validated_data) # Save authors + authors = validated_data.get('author_ids') + + if authors and isinstance(authors, list) and len(authors) == 1 and isinstance(authors[0], str): + authors = authors[0][1:-1].replace('},{', '};;{').split(';;') + authors = [json.loads(author) for author in authors] + if authors: instance.save_authors(authors) tag_ids = validated_data.get('tag_ids', False) + + if tag_ids and isinstance(tag_ids, list) and len(tag_ids) == 1 and isinstance(tag_ids[0], str): + tag_ids = [int(tag) for tag in tag_ids[0].split(',')] + if tag_ids != False: instance.save_tags(tag_ids) @@ -664,6 +678,8 @@ class ArticleSerializer(DispatchModelSerializer, DispatchPublishableSerializer): authors = AuthorSerializer(many=True, read_only=True) author_ids = serializers.ListField( write_only=True, + allow_empty=False, + required=True, child=serializers.JSONField(), validators=[AuthorValidator(False)]) authors_string = serializers.CharField(source='get_author_string', read_only=True) @@ -1131,4 +1147,4 @@ class Meta: if settings.GS_USE_SIGNED_URLS: fields += ( 'file_upload_url', - ) + ) \ No newline at end of file diff --git a/dispatch/api/validators.py b/dispatch/api/validators.py index 3faebccf3..bfbe3b359 100644 --- a/dispatch/api/validators.py +++ b/dispatch/api/validators.py @@ -98,12 +98,22 @@ def __call__(self, data): # Convert single instance to a list data = [data] + AUTHOR_TYPES = {'author', 'photographer', 'illustrator', 'videographer'} + for author in data: if 'person' not in author: raise ValidationError('An author must contain a person.') - if 'type' in author and not isinstance(author['type'], str): - # If type is defined, it should be a string - raise ValidationError('The author type must be a string.') + if 'type' in author: + if isinstance(author, dict): + if not isinstance(author['type'], str) or author['type'].lower() not in AUTHOR_TYPES: + raise ValidationError('The author type must be a string, matching a predefined type.') + elif isinstance(author, str): + tokens = author.split('"') + for i in range(0, len(tokens)): + if 5 + 6 * i < len(tokens): + author_type = tokens[5 + 6 * i] + if author_type.lower() not in AUTHOR_TYPES: + raise ValidationError('The author type must be a string, matching a predefined type.') def TemplateValidator(template, template_data, tags, subsection_id): diff --git a/dispatch/modules/auth/managers.py b/dispatch/modules/auth/managers.py index eafdce86c..6bdcbfb9d 100644 --- a/dispatch/modules/auth/managers.py +++ b/dispatch/modules/auth/managers.py @@ -15,9 +15,6 @@ def _create_user(self, email, password=None, permissions=None, is_active=True, i if not self.is_valid_password(password): raise ValueError('Password is invalid') - if not person: - raise ValueError('User must have a valid person') - user = self.model(email=email, is_active=is_active, is_superuser=is_superuser) user.set_password(password) diff --git a/dispatch/static/manager/src/js/components/GalleryEditor/DragDropContext.js b/dispatch/static/manager/src/js/components/GalleryEditor/DragDropContext.js new file mode 100644 index 000000000..684705e39 --- /dev/null +++ b/dispatch/static/manager/src/js/components/GalleryEditor/DragDropContext.js @@ -0,0 +1,3 @@ +import HTML5Backend from 'react-dnd-html5-backend' +import { DragDropContext } from 'react-dnd' +export default DragDropContext(HTML5Backend) \ No newline at end of file diff --git a/dispatch/static/manager/src/js/components/GalleryEditor/GalleryForm.js b/dispatch/static/manager/src/js/components/GalleryEditor/GalleryForm.js index 7a6687c1d..b4484fff5 100644 --- a/dispatch/static/manager/src/js/components/GalleryEditor/GalleryForm.js +++ b/dispatch/static/manager/src/js/components/GalleryEditor/GalleryForm.js @@ -1,8 +1,7 @@ import R from 'ramda' import React from 'react' import { connect } from 'react-redux' -import { DragDropContext } from 'react-dnd' -import HTML5Backend from 'react-dnd-html5-backend' +import DragDropContext from './DragDropContext' import Measure from 'react-measure' import autobind from 'class-autobind' @@ -307,10 +306,9 @@ const mapDispatchToProps = (dispatch) => { } } - const GalleryForm = connect( mapStateToProps, mapDispatchToProps -)(DragDropContext(HTML5Backend)(GalleryFormComponent)) +)(DragDropContext(GalleryFormComponent)) -export default GalleryForm +export default GalleryForm \ No newline at end of file diff --git a/dispatch/static/manager/src/js/components/ImageEditor/ImageForm.js b/dispatch/static/manager/src/js/components/ImageEditor/ImageForm.js index f3dd34a3f..bd7b14134 100644 --- a/dispatch/static/manager/src/js/components/ImageEditor/ImageForm.js +++ b/dispatch/static/manager/src/js/components/ImageEditor/ImageForm.js @@ -45,6 +45,7 @@ export default class ImageForm extends React.Component { this.props.update('authors', authors)} + authorErrors={this.props.authorErrors} defaultAuthorType={AuthorSelectInput.PHOTOGRAPHER} /> diff --git a/dispatch/static/manager/src/js/components/ItemEditor/index.js b/dispatch/static/manager/src/js/components/ItemEditor/index.js index 8a8153b5e..e0935cb88 100644 --- a/dispatch/static/manager/src/js/components/ItemEditor/index.js +++ b/dispatch/static/manager/src/js/components/ItemEditor/index.js @@ -11,6 +11,14 @@ const NEW_LISTITEM_ID = 'new' class ItemEditor extends React.Component { + constructor(props) { + super(props) + + this.state = { + noAuthorError: null + } + } + componentDidMount() { if (this.props.isNew) { // Create empty listItem @@ -53,19 +61,26 @@ class ItemEditor extends React.Component { } saveListItem() { + const listItem = this.getListItem() + if (listItem.authors && !listItem.authors.length) { + this.setState({ noAuthorError: 'This field is required.' }) + return + } + if (this.props.isNew) { - this.props.createListItem(this.props.token, this.getListItem()) + this.props.createListItem(this.props.token, listItem) } else { this.props.saveListItem( this.props.token, this.props.itemId, - this.getListItem() + listItem ) } } handleUpdate(field, value) { this.props.setListItem(R.assoc(field, value, this.getListItem())) + if(field == 'authors' && value.length) { this.setState({ noAuthorError: null }) } } render() { @@ -94,6 +109,7 @@ class ItemEditor extends React.Component { this.handleUpdate(field, value)} settings={this.props.settings ? this.props.settings : {}} /> @@ -108,4 +124,4 @@ ItemEditor.defaultProps = { multipart: false } -export default withRouter(ItemEditor) +export default withRouter(ItemEditor) \ No newline at end of file diff --git a/dispatch/static/manager/src/js/components/SubsectionEditor/SubsectionForm.js b/dispatch/static/manager/src/js/components/SubsectionEditor/SubsectionForm.js index 8e4689d1e..921551e12 100644 --- a/dispatch/static/manager/src/js/components/SubsectionEditor/SubsectionForm.js +++ b/dispatch/static/manager/src/js/components/SubsectionEditor/SubsectionForm.js @@ -73,6 +73,7 @@ export default function SubsectionForm(props) { error={props.errors.author_ids}> props.update('authors', authors)} /> diff --git a/dispatch/static/manager/src/js/components/VideoEditor/VideoForm.js b/dispatch/static/manager/src/js/components/VideoEditor/VideoForm.js index b4d91729a..06f9ce2ce 100644 --- a/dispatch/static/manager/src/js/components/VideoEditor/VideoForm.js +++ b/dispatch/static/manager/src/js/components/VideoEditor/VideoForm.js @@ -37,6 +37,7 @@ export default function VideoForm(props) { props.update('authors', authors)} + authorErrors={props.authorErrors} defaultAuthorType={AuthorSelectInput.VIDEOGRAPHER} /> diff --git a/dispatch/static/manager/src/js/components/inputs/SortableList/index.js b/dispatch/static/manager/src/js/components/inputs/SortableList/index.js index 06293d66a..f5ca3586b 100644 --- a/dispatch/static/manager/src/js/components/inputs/SortableList/index.js +++ b/dispatch/static/manager/src/js/components/inputs/SortableList/index.js @@ -1,9 +1,7 @@ import React from 'react' import R from 'ramda' import PropTypes from 'prop-types' -import { DragDropContext } from 'react-dnd' -import HTML5Backend from 'react-dnd-html5-backend' - +import DragDropContext from '../../GalleryEditor/DragDropContext' import Item from './Item' require('../../../../styles/components/sortable_list.scss') @@ -55,4 +53,4 @@ SortableList.defaultProps = { inline: false } -export default DragDropContext(HTML5Backend)(SortableList) +export default DragDropContext(SortableList) \ No newline at end of file diff --git a/dispatch/static/manager/src/js/components/inputs/selects/AuthorSelectInput.js b/dispatch/static/manager/src/js/components/inputs/selects/AuthorSelectInput.js index 6d152cfd2..8f76b3298 100644 --- a/dispatch/static/manager/src/js/components/inputs/selects/AuthorSelectInput.js +++ b/dispatch/static/manager/src/js/components/inputs/selects/AuthorSelectInput.js @@ -36,11 +36,14 @@ class AuthorSelectInputComponent extends React.Component { } render() { - const value = this.props.value - .map(author => author.person) - - const extraFields = this.props.value + let value = [] + let extraFields = [] + + if(this.props.value){ + value = this.props.value.map(author => author.person) + extraFields = this.props.value .reduce((fields, author) => R.assoc(author.person, author.type, fields), {}) + } return ( this.listPersons(query)} extraFieldOptions={AUTHOR_TYPES} attribute='full_name' - editMessage={this.props.value.length ? 'Edit authors' : 'Add authors'} /> + errors={this.props.authorErrors} + editMessage={this.props.value && this.props.value.length ? 'Edit authors' : 'Add authors'} /> ) } } @@ -93,4 +97,4 @@ AuthorSelectInput.defaultProps = { defaultAuthorType: AuthorSelectInput.AUTHOR } -export default AuthorSelectInput +export default AuthorSelectInput \ No newline at end of file diff --git a/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js b/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js index 87bb586a0..add339c7d 100644 --- a/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js +++ b/dispatch/static/manager/src/js/components/inputs/selects/ItemSelectInput.js @@ -251,6 +251,12 @@ class ItemSelectInput extends React.Component { ) + const error = ( + + {Array.isArray(this.props.errors) ? this.props.errors.join(' ') : this.props.errors} + + ) + return (
@@ -261,6 +267,7 @@ class ItemSelectInput extends React.Component { inline={this.props.inline}> {this.props.tag ? tagButton : Button} + {this.props.errors && error}
) } @@ -276,4 +283,4 @@ ItemSelectInput.defaultProps = { inline: true } -export default ItemSelectInput +export default ItemSelectInput \ No newline at end of file diff --git a/dispatch/static/manager/src/js/components/inputs/selects/TagSelectInput.js b/dispatch/static/manager/src/js/components/inputs/selects/TagSelectInput.js index e971c7889..77599e209 100644 --- a/dispatch/static/manager/src/js/components/inputs/selects/TagSelectInput.js +++ b/dispatch/static/manager/src/js/components/inputs/selects/TagSelectInput.js @@ -27,7 +27,7 @@ class TagSelectInputComponent extends React.Component { fetchResults={(query) => this.listTags(query)} create={(name, cb) => this.props.createTag(this.props.token, { name }, cb)} attribute='name' - editMessage={this.props.value.length ? 'Edit tags' : 'Add tags'} /> + editMessage={this.props.value && this.props.value.length ? 'Edit tags' : 'Add tags'} /> ) } diff --git a/dispatch/static/manager/src/js/components/modals/ImageManager/ImagePanel.js b/dispatch/static/manager/src/js/components/modals/ImageManager/ImagePanel.js index d863826a0..2f678a079 100644 --- a/dispatch/static/manager/src/js/components/modals/ImageManager/ImagePanel.js +++ b/dispatch/static/manager/src/js/components/modals/ImageManager/ImagePanel.js @@ -16,14 +16,14 @@ export default function ImagePanel(props) {
+ onClick={() => {props.save()}}>{props.successBtnName || 'Update'} + onClick={() => props.delete()}>{props.dangerBtnName || 'Delete'}
- -
{props.image.filename}
+ +
{props.image.filename? props.image.filename: props.image.img.name}
@@ -35,7 +35,8 @@ export default function ImagePanel(props) { props.update('authors', authors)} + update={authors => {props.update('authors', authors)}} + authorErrors={props.authorErrors} defaultAuthorType={AuthorSelectInput.PHOTOGRAPHER} /> @@ -46,4 +47,4 @@ export default function ImagePanel(props) { ) -} +} \ No newline at end of file diff --git a/dispatch/static/manager/src/js/components/modals/ImageManager/index.js b/dispatch/static/manager/src/js/components/modals/ImageManager/index.js index d4a19b2f0..b2c1e6d78 100644 --- a/dispatch/static/manager/src/js/components/modals/ImageManager/index.js +++ b/dispatch/static/manager/src/js/components/modals/ImageManager/index.js @@ -29,6 +29,10 @@ class ImageManagerComponent extends React.Component { this.scrollListener = this.scrollListener.bind(this) this.state = { + uploadNoAuthorError: null, + editNoAuthorError: null, + uploadedImages: null, + newImage: {'img': null, 'title': null, 'authors': [], 'tags': [] }, author: '', tags: [], q: '', @@ -85,6 +89,10 @@ class ImageManagerComponent extends React.Component { handleSave() { const image = this.getImage() + if(!image.authors.length){ + this.setState({ editNoAuthorError: 'This field is required.' }) + return + } this.props.saveImage(this.props.token, image.id, image) } @@ -96,6 +104,7 @@ class ImageManagerComponent extends React.Component { this.props.setImage( R.assoc(field, data, this.getImage()) ) + if(field == 'authors' && data.length) { this.setState({ editNoAuthorError: null }) } } insertImage() { @@ -115,12 +124,56 @@ class ImageManagerComponent extends React.Component { }, this.searchImages) } - onDrop(files) { - files.forEach(file => { - this.props.createImage(this.props.token, { img: file }) + onDrop(images) { + this.setNextImage(images) + } + + setNextImage(images) { + const [first, ...rest] = images + this.setState({ + uploadNoAuthorError: null, + uploadedImages: rest, + newImage: {'img': first, 'title': null, 'authors': [], 'tags': [] } }) } + modalHandleSave() { + var payload = { 'img': this.state.newImage.img } + if(this.state.newImage.authors.length){ + payload['authors'] = JSON.stringify(this.state.newImage.authors) + } + else { + this.setState({ + uploadNoAuthorError: 'This field is required.' + }) + return + } + if(this.state.newImage.title){ + payload['title'] = this.state.newImage.title + } + if(this.state.newImage.tags.length){ + payload['tags'] = this.state.newImage.tags + } + + this.props.createImage(this.props.token, payload, () => { + this.setNextImage(this.state.uploadedImages) + }) + } + + modalHandleCancel() { + this.setNextImage(this.state.uploadedImages) + } + + modalHandleUpdate(field, data) { + this.setState(state => ( state.newImage[field] = data, state )) + if(field == 'authors' && data.length) { this.setState({ uploadNoAuthorError: null }) } + } + + handleSelectImage(imageId) { + this.props.selectImage(imageId) + this.setState({ editNoAuthorError: null}) + } + render() { const image = this.getImage() @@ -131,18 +184,30 @@ class ImageManagerComponent extends React.Component { key={image.id} image={image} isSelected={this.props.many ? R.contains(id, this.props.images.selected) : this.props.image.id === id} - selectImage={this.props.many ? this.props.toggleImage : this.props.selectImage} /> + selectImage={this.props.many ? this.props.toggleImage : (imageId) => {this.handleSelectImage(imageId)}} /> ) }) - const imagePanel = ( + const editImagePanel = ( this.handleUpdate(field, data)} save={() => this.handleSave()} delete={() => this.handleDelete()} /> ) + const uploadImagePanel = ( + this.modalHandleUpdate(field, data)} + save={() => this.modalHandleSave()} + delete={() => this.modalHandleCancel()} /> + ) + const filters = [ {!this.props.many ?
- {image ? imagePanel : null} + {image ? editImagePanel : null}
: null} - + + {this.state.newImage.img && +
+
+ {uploadImagePanel} +
+
+ } +
+ + {this.state.newImage.img && +
+
+ {uploadImagePanel} +
+
+ } +
this.onDropzoneClick()}>

Drag images into window or click here to upload

@@ -199,8 +269,8 @@ const mapDispatchToProps = (dispatch) => { toggleImage: (imageId) => { dispatch(imageActions.toggle(imageId)) }, - createImage: (token, image) => { - dispatch(imageActions.create(token, image)) + createImage: (token, image, callback) => { + dispatch(imageActions.create(token, image, null, callback)) }, toggleAllImages: (imageIds) => { dispatch(imageActions.toggleAll(imageIds)) @@ -225,4 +295,4 @@ const ImagesPage = connect( mapDispatchToProps )(ImagesPageComponent) -export default ImagesPage +export default ImagesPage \ No newline at end of file diff --git a/dispatch/static/manager/src/styles/components/image_manager.scss b/dispatch/static/manager/src/styles/components/image_manager.scss index e5332f633..1dad8658f 100644 --- a/dispatch/static/manager/src/styles/components/image_manager.scss +++ b/dispatch/static/manager/src/styles/components/image_manager.scss @@ -1,4 +1,5 @@ @import '../utilities/colors'; +@import '../utilities/variables'; // Component: ImageManager .c-image-manager { @@ -73,6 +74,26 @@ $image-manager-footer-box-shadow: rgba(0,0,0,0.05) 0px -1px 1px; } } +.c-modal-body { + background-color: $color-white; + margin: 15% auto; /* 15% from the top and centered */ + padding: 20px; + border: 1px solid $color-border-gray; + width: 80%; /* Could be more or less, depending on screen size */ +} + +.c-modal-container-scrollable { + position: fixed; /* Stay in place */ + z-index: $z-index-3; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: $color-bg-black; /* Fallback color */ + background-color: $color-bg-black-opacity; /* Black w/ opacity */ +} + .c-image-manager__body { // Structure display: flex; @@ -110,4 +131,4 @@ $image-manager-footer-box-shadow: rgba(0,0,0,0.05) 0px -1px 1px; // Border border-left: 1px solid $color-border-gray; -} +} \ No newline at end of file diff --git a/dispatch/static/manager/src/styles/components/modal_container.scss b/dispatch/static/manager/src/styles/components/modal_container.scss index 97851d463..595165d1d 100644 --- a/dispatch/static/manager/src/styles/components/modal_container.scss +++ b/dispatch/static/manager/src/styles/components/modal_container.scss @@ -12,7 +12,7 @@ left: 0; // Background - background: rgba(0, 0, 0, 0.5); + background: $color-bg-black-opacity; // Extras z-index: $z-index-3; @@ -35,8 +35,8 @@ // Background background: $color-white; - box-shadow: rgba(0,0,0,0.5) 0px 0px 5px; + box-shadow: $color-bg-black-opacity 0px 0px 5px; // Extras z-index: $z-index-4; -} +} \ No newline at end of file diff --git a/dispatch/static/manager/src/styles/utilities/_colors.scss b/dispatch/static/manager/src/styles/utilities/_colors.scss index 11e291ca7..d6b407370 100644 --- a/dispatch/static/manager/src/styles/utilities/_colors.scss +++ b/dispatch/static/manager/src/styles/utilities/_colors.scss @@ -19,6 +19,8 @@ $color-border-blue: #51a7e8; $color-bg-gray: #eeeeee; $color-bg-gray-light: #fcfcfc; $color-bg-gray-dark: #dddddd; +$color-bg-black: rgb(0,0,0); +$color-bg-black-opacity: rgba(0,0,0,0.5); $color-dispatch-header-hover: rgba(138, 155, 168, 0.15); // Inputs diff --git a/dispatch/tests/helpers.py b/dispatch/tests/helpers.py index 36f2a11e3..e99cb1d6c 100644 --- a/dispatch/tests/helpers.py +++ b/dispatch/tests/helpers.py @@ -6,6 +6,7 @@ from dispatch.models import Article, Author, Person, Section, Image from dispatch.tests.cases import DispatchMediaTestMixin from dispatch.modules.content.mixins import AuthorMixin +import json class DispatchTestHelpers(object): @@ -45,7 +46,7 @@ def create_subsection(cls, client, name='Test subsection', slug='test-subsection authors = [] for author in author_names: - (person, created) = Person.objects.get_or_create(full_name=author, slug='author') + (person, created) = Person.objects.get_or_create(full_name=author, slug=author.lower().replace(' ', '-')) authors.append({ 'person': person.id, 'type': 'author' @@ -92,15 +93,25 @@ def upload_file(cls, client, filename='TestFile'): return response @classmethod - def create_image(cls, client): + def create_image(cls, client, filename='test_image_a.png', author_names=['Test Person']): """Upload an image that can be linked by galleries""" + # Create test person + authors = [] + + for author in author_names: + (person, created) = Person.objects.get_or_create(full_name=author, slug=author.lower().replace(' ', '-')) + authors.append({ + 'person': person.id, + 'type': 'photographer' + }) + obj = DispatchMediaTestMixin() url = reverse('api-images-list') - with open(obj.get_input_file('test_image_a.png'), 'rb') as test_image: - response = client.post(url, { 'img': test_image }, format='multipart') + with open(obj.get_input_file(filename), 'rb') as test_image: + response = client.post(url, { 'img': test_image, 'authors': json.dumps(authors), 'author_ids': json.dumps(authors) }, format='multipart') return response @@ -181,8 +192,8 @@ def create_person(cls, client, full_name=('Person' + str(randint(0,1000000))), s data = { 'full_name': full_name, - 'image': image, 'slug': slug, + 'image': image, 'description': description, 'title': title } diff --git a/dispatch/tests/test_api_articles.py b/dispatch/tests/test_api_articles.py index b671a7b8f..a686a8bf0 100644 --- a/dispatch/tests/test_api_articles.py +++ b/dispatch/tests/test_api_articles.py @@ -246,6 +246,10 @@ def test_author_type_format(self): response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + data['author_ids'][0]['type'] = 'article author' + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_update_article_tags(self): """Should be able to update and remove article tags""" @@ -736,13 +740,7 @@ def test_set_featured_image(self): """Ensure that a featured image can be set""" article = DispatchTestHelpers.create_article(self.client) - - url = reverse('api-images-list') - - image_file = 'test_image_a.jpg' - - with open(self.get_input_file(image_file), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') + image = DispatchTestHelpers.create_image(self.client) data = { 'featured_image': { @@ -761,13 +759,6 @@ def test_set_featured_image_no_id(self): article = DispatchTestHelpers.create_article(self.client) - url = reverse('api-images-list') - - image_file = 'test_image_a.jpg' - - with open(self.get_input_file(image_file), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') - data = { 'featured_image': { 'image_id': None, @@ -792,14 +783,8 @@ def test_remove_featured_image(self): """Ensure that a featured image can be removed""" article = DispatchTestHelpers.create_article(self.client) - - url = reverse('api-images-list') - - image_file = 'test_image_a.jpg' - - with open(self.get_input_file(image_file), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') - + image = DispatchTestHelpers.create_image(self.client) + data = { 'featured_image': { 'image_id': image.data['id'] diff --git a/dispatch/tests/test_api_images.py b/dispatch/tests/test_api_images.py index e9e607999..ee73ff532 100644 --- a/dispatch/tests/test_api_images.py +++ b/dispatch/tests/test_api_images.py @@ -21,11 +21,7 @@ def test_create_image_unauthorized(self): # Clear client credentials self.client.credentials() - - url = reverse('api-images-list') - - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - response = self.client.post(url, { 'img': test_image }, format='multipart') + response = DispatchTestHelpers.create_image(self.client) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(Image.objects.count(), 0) @@ -40,6 +36,16 @@ def test_create_image_empty(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(Image.objects.count(), 0) + def test_create_image_no_authors(self): + """Should not be able to create an image without authors.""" + + url = reverse('api-images-list') + + with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: + response = self.client.post(url, { 'img': test_image }, format='multipart') + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_create_image_jpeg(self): """Should be able to upload a JPEG image.""" @@ -52,8 +58,7 @@ def test_create_image_jpeg(self): ] for image_file in files: - with open(self.get_input_file(image_file), 'rb') as test_image: - response = self.client.post(url, { 'img': test_image }, format='multipart') + response = DispatchTestHelpers.create_image(self.client, filename=image_file) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertTrue(self.fileExists(response.data['url'])) @@ -72,8 +77,7 @@ def test_create_image_png(self): ] for image_file in files: - with open(self.get_input_file(image_file), 'rb') as test_image: - response = self.client.post(url, { 'img': test_image }, format='multipart') + response = DispatchTestHelpers.create_image(self.client, filename=image_file) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertTrue(self.fileExists(response.data['url'])) @@ -93,8 +97,7 @@ def test_upload_image_gif(self): ] for image_file in files: - with open(self.get_input_file(image_file), 'rb') as test_image: - response = self.client.post(url, { 'img': test_image }, format='multipart') + response = DispatchTestHelpers.create_image(self.client, filename=image_file) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertTrue(self.fileExists(response.data['url'])) @@ -108,11 +111,8 @@ def test_upload_duplicate_filenames(self): url = reverse('api-images-list') - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - image_1 = self.client.post(url, { 'img': test_image }, format='multipart') - - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - image_2 = self.client.post(url, { 'img': test_image }, format='multipart') + image_1 = DispatchTestHelpers.create_image(self.client) + image_2 = DispatchTestHelpers.create_image(self.client) self.assertEqual(image_1.status_code, status.HTTP_201_CREATED) self.assertEqual(image_2.status_code, status.HTTP_201_CREATED) @@ -135,8 +135,7 @@ def test_create_image_invalid_filename(self): with open(self.get_input_file(invalid_filename), 'wb') as invalid_image: invalid_image.writelines(valid_image.readlines()) - with open(self.get_input_file(invalid_filename), 'rb') as test_image: - response = self.client.post(url, { 'img': test_image }, format='multipart') + response = DispatchTestHelpers.create_image(self.client, filename=invalid_filename) self.remove_input_file(invalid_filename) @@ -147,12 +146,9 @@ def test_create_image_invalid_filename(self): def test_update_image_unauthorized(self): """Should not be able to update an image without authorization.""" - url = reverse('api-images-list') + image = DispatchTestHelpers.create_image(self.client) - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') - - person = Person.objects.create(full_name='Test Person', slug='test-person') + person = Person.objects.create(full_name='Some Test Person', slug='some-test-person') # Clear client credentials self.client.credentials() @@ -171,21 +167,20 @@ def test_update_image_unauthorized(self): image_instance = Image.objects.get(pk=image.data['id']) self.assertEqual(image_instance.title, None) - self.assertEqual(image_instance.authors.count(), 0) + self.assertEqual(image_instance.authors.count(), 1) def test_update_image(self): """Should be able to update an image.""" - url = reverse('api-images-list') - - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') + image = DispatchTestHelpers.create_image(self.client) + TEST_PERSON_NAME = 'Random Test Person' + TEST_PERSON_SLUG = 'random-test-person' - person = Person.objects.create(full_name='Test Person', slug='test-person') + person = Person.objects.create(full_name=TEST_PERSON_NAME, slug=TEST_PERSON_SLUG) new_data = { 'title': 'Test image', - 'author_ids': [{'person': person.id}] + 'author_ids': [{'person': person.id, 'type': 'photographer'}] } url = reverse('api-images-detail', args=[image.data['id']]) @@ -194,20 +189,17 @@ def test_update_image(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['title'], 'Test image') - self.assertEqual(response.data['authors'][0]['person']['full_name'], 'Test Person') + self.assertEqual(response.data['authors'][0]['person']['full_name'], TEST_PERSON_NAME) image_instance = Image.objects.get(pk=image.data['id']) self.assertEqual(image_instance.title, 'Test image') - self.assertEqual(image_instance.authors.all()[0].person.full_name, 'Test Person') + self.assertEqual(image_instance.authors.all()[0].person.full_name, TEST_PERSON_NAME) def test_delete_image_unauthorized(self): """Should not be able to delete an image without authorization.""" - url = reverse('api-images-list') - - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') + image = DispatchTestHelpers.create_image(self.client) # Clear client credentials self.client.credentials() @@ -227,10 +219,7 @@ def test_delete_image_unauthorized(self): def test_delete_image(self): """Should be able to delete an image.""" - url = reverse('api-images-list') - - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') + image = DispatchTestHelpers.create_image(self.client) url = reverse('api-images-detail', args=[image.data['id']]) @@ -247,11 +236,7 @@ def test_delete_image(self): def test_get_image(self): """Should be able to fetch an image by ID.""" - - url = reverse('api-images-list') - - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') + image = DispatchTestHelpers.create_image(self.client) url = reverse('api-images-detail', args=[image.data['id']]) @@ -263,18 +248,12 @@ def test_get_image(self): def test_list_images(self): """Should be able to list all images.""" - url = reverse('api-images-list') - # Upload three images - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - image_1 = self.client.post(url, { 'img': test_image }, format='multipart') - - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - image_2 = self.client.post(url, { 'img': test_image }, format='multipart') - - with open(self.get_input_file('test_image_a.jpg'), 'rb') as test_image: - image_3 = self.client.post(url, { 'img': test_image }, format='multipart') + image_1 = DispatchTestHelpers.create_image(self.client) + image_2 = DispatchTestHelpers.create_image(self.client) + image_3 = DispatchTestHelpers.create_image(self.client) + url = reverse('api-images-list') response = self.client.get(url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -288,8 +267,6 @@ def test_tag_query(self): tag = Tag.objects.create(name='Test Tag') - url = reverse('api-images-list') - filesa = [ 'test_image_a.jpg', 'test_image_a.jpeg', @@ -304,14 +281,12 @@ def test_tag_query(self): } for image_file in filesa: - with open(self.get_input_file(image_file), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') - imageurl = reverse('api-images-detail', args=[image.data['id']]) - response = self.client.patch(imageurl, new_data, format='json') + image = DispatchTestHelpers.create_image(self.client, filename=image_file) + imageurl = reverse('api-images-detail', args=[image.data['id']]) + response = self.client.patch(imageurl, new_data, format='json') for image_file in filesb: - with open(self.get_input_file(image_file), 'rb') as test_image: - response = self.client.post(url, { 'img': test_image }, format='multipart') + response = DispatchTestHelpers.create_image(self.client, filename=image_file) url = reverse('api-images-detail', args=[image.data['id']]) @@ -329,9 +304,7 @@ def test_tag_query(self): def test_author_query(self): """Should be able to search images by authors""" - person = Person.objects.create(full_name='Test Person', slug='test-person') - - url = reverse('api-images-list') + person = Person.objects.create(full_name='Ubyssey Person', slug='ubyssey-person') filesa = [ 'test_image_a.jpg', @@ -343,18 +316,16 @@ def test_author_query(self): ] new_data = { - 'author_ids': [{'person': person.id}] + 'author_ids': [{'person': person.id, 'type': 'photographer'}] } for image_file in filesa: - with open(self.get_input_file(image_file), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') - imageurl = reverse('api-images-detail', args=[image.data['id']]) - response = self.client.patch(imageurl, new_data, format='json') + image = DispatchTestHelpers.create_image(self.client, filename=image_file) + imageurl = reverse('api-images-detail', args=[image.data['id']]) + response = self.client.patch(imageurl, new_data, format='json') for image_file in filesb: - with open(self.get_input_file(image_file), 'rb') as test_image: - response = self.client.post(url, { 'img': test_image }, format='multipart') + response = DispatchTestHelpers.create_image(self.client, filename=image_file) url = reverse('api-images-detail', args=[image.data['id']]) @@ -382,8 +353,7 @@ def test_name_query(self): filename = 'test_image_b' for image_file in files: - with open(self.get_input_file(image_file), 'rb') as test_image: - response = self.client.post(url, { 'img': test_image }, format='multipart') + response = DispatchTestHelpers.create_image(self.client, filename=image_file) url = '%s?q=%s' % (reverse('api-images-list'), filename) response = self.client.get(url, format='json') diff --git a/dispatch/tests/test_api_pages.py b/dispatch/tests/test_api_pages.py index 482f88198..ce997575b 100644 --- a/dispatch/tests/test_api_pages.py +++ b/dispatch/tests/test_api_pages.py @@ -231,13 +231,8 @@ def test_set_featured_image(self): """Ensure that a featured image can be set""" page = DispatchTestHelpers.create_page(self.client) - - url = reverse('api-images-list') - - image_file = 'test_image_a.jpg' - with open(self.get_input_file(image_file), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') + image = DispatchTestHelpers.create_image(self.client) data = { 'featured_image': { @@ -256,12 +251,7 @@ def test_remove_featured_image(self): page = DispatchTestHelpers.create_page(self.client) - url = reverse('api-images-list') - - image_file = 'test_image_a.jpg' - - with open(self.get_input_file(image_file), 'rb') as test_image: - image = self.client.post(url, { 'img': test_image }, format='multipart') + image = DispatchTestHelpers.create_image(self.client) data = { 'featured_image': { diff --git a/dispatch/tests/test_api_persons.py b/dispatch/tests/test_api_persons.py index 4a63320c6..ae092c2f1 100644 --- a/dispatch/tests/test_api_persons.py +++ b/dispatch/tests/test_api_persons.py @@ -137,7 +137,7 @@ def test_duplicate_slug(self): ) response2 = DispatchTestHelpers.create_person( self.client, - full_name='Test Person 2', + full_name='Test Person 20', slug='test-person' ) diff --git a/dispatch/tests/test_api_users.py b/dispatch/tests/test_api_users.py index 803107dcf..dc09b570f 100644 --- a/dispatch/tests/test_api_users.py +++ b/dispatch/tests/test_api_users.py @@ -21,6 +21,8 @@ TEST_USER_SLUG_4 = 'john-doe-fourth' TEST_USER_FULL_NAME_5 = 'John Doe Fifth' TEST_USER_SLUG_5 = 'john-doe-fifth' +TEST_USER_FULL_NAME_6 = 'John Doe Sixth' +TEST_USER_SLUG_6 = 'john-doe-sixth' class UserTests(DispatchAPITestCase): """A class to test the user API methods""" @@ -45,7 +47,10 @@ def test_admin_creation(self): url = reverse('api-users-list') - person_id = DispatchTestHelpers.create_person(self.client, full_name=TEST_USER_FULL_NAME, slug=TEST_USER_SLUG).data['id'] + person_id = DispatchTestHelpers.create_person(self.client, + full_name=TEST_USER_FULL_NAME, + slug=TEST_USER_SLUG).data['id'] + data = { 'email' : TEST_USER_EMAIL, 'person' : person_id, @@ -58,7 +63,6 @@ def test_admin_creation(self): user = User.objects.get(person=person_id) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data['email'], TEST_USER_EMAIL)