From 04b629c00e58fe98ff6f9ef49da84d8e1ce99345 Mon Sep 17 00:00:00 2001 From: David Silva Date: Sat, 6 Oct 2018 20:13:12 -0400 Subject: [PATCH 1/8] Created skeleton app for challenges --- api/settings.py | 1 + challenges/__init__.py | 0 challenges/admin.py | 3 +++ challenges/apps.py | 5 +++++ challenges/migrations/__init__.py | 0 challenges/models.py | 3 +++ challenges/tests/__init__.py | 0 challenges/tests/test_requests/__init__.py | 0 challenges/views.py | 3 +++ 9 files changed, 15 insertions(+) create mode 100644 challenges/__init__.py create mode 100644 challenges/admin.py create mode 100644 challenges/apps.py create mode 100644 challenges/migrations/__init__.py create mode 100644 challenges/models.py create mode 100644 challenges/tests/__init__.py create mode 100644 challenges/tests/test_requests/__init__.py create mode 100644 challenges/views.py diff --git a/api/settings.py b/api/settings.py index 84f3cc7..5bd9e69 100644 --- a/api/settings.py +++ b/api/settings.py @@ -38,6 +38,7 @@ 'django.contrib.staticfiles', 'accounts.apps.AccountsConfig', 'profile.apps.ProfileConfig', + 'challenges.apps.ChallengesConfig', 'rest_framework', ] diff --git a/challenges/__init__.py b/challenges/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/challenges/admin.py b/challenges/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/challenges/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/challenges/apps.py b/challenges/apps.py new file mode 100644 index 0000000..621ad57 --- /dev/null +++ b/challenges/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ChallengesConfig(AppConfig): + name = 'challenges' diff --git a/challenges/migrations/__init__.py b/challenges/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/challenges/models.py b/challenges/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/challenges/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/challenges/tests/__init__.py b/challenges/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/challenges/tests/test_requests/__init__.py b/challenges/tests/test_requests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/challenges/views.py b/challenges/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/challenges/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 8a1abc835fcf65869034b6869731ea4655b045a3 Mon Sep 17 00:00:00 2001 From: David Silva Date: Sat, 6 Oct 2018 21:19:14 -0400 Subject: [PATCH 2/8] created failing test for challenges --- .../tests/test_requests/test_challenges.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 challenges/tests/test_requests/test_challenges.py diff --git a/challenges/tests/test_requests/test_challenges.py b/challenges/tests/test_requests/test_challenges.py new file mode 100644 index 0000000..419bfc6 --- /dev/null +++ b/challenges/tests/test_requests/test_challenges.py @@ -0,0 +1,42 @@ +from django.test import TestCase +from rest_framework.test import APIClient, APITestCase +from accounts.models import User +from challenges.models import Challenge +import sure +import json + +class ChallengeApiTests(APITestCase): + def setUp(self): + self.client = APIClient() + self.content_type = 'application/vnd.api+json' + + def test_challenge_create(self): + """ + Ensure we get the correct challenge + """ + user = User.objects.filter(username="codecorgi").first() + + challenge = Challenge.objects.create(user=user, + title = 'Test Challenge', + short_title = 'Test Challenge', + owner = 'Gandalf', + difficulty = '5', + challenge_type = 'feature', + priority = 'High', + description = 'This is a description', + short_description = 'This is shorter', + extra_points = 'Put it on github', + technical_notes = 'Use Django', + procedure = 'Clone the code, and then run it locally', + code_tips = 'Use a linter', + ) + + response = self.client.get(f'/services/api/challenges?pk={ challenge.id }', + content_type=self.content_type) + + response.status_code.should.equal(200) + + response_data = json.loads(response.content) + attributes = response_data['data'][0]['attributes'] + relationships = response_data['data'][0]['relationships'] + attributes['title'].should.equal(challenge.title) From 572cd517de4fa8c7f2296e8253a6b0aab79535f2 Mon Sep 17 00:00:00 2001 From: David Silva Date: Sat, 6 Oct 2018 21:19:31 -0400 Subject: [PATCH 3/8] created models and migrations for challenges --- challenges/migrations/0001_initial.py | 76 +++++++++++++++++++++++++++ challenges/models.py | 58 +++++++++++++++++++- 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 challenges/migrations/0001_initial.py diff --git a/challenges/migrations/0001_initial.py b/challenges/migrations/0001_initial.py new file mode 100644 index 0000000..c13ee9b --- /dev/null +++ b/challenges/migrations/0001_initial.py @@ -0,0 +1,76 @@ +# Generated by Django 2.1.2 on 2018-10-07 00:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Attachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=250)), + ('url', models.TextField()), + ('active', models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name='Challenge', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_visible', models.BooleanField(default=True)), + ('title', models.CharField(max_length=250)), + ('short_title', models.CharField(max_length=250)), + ('owner', models.CharField(max_length=250)), + ('difficulty', models.CharField(max_length=250)), + ('challenge_type', models.CharField(blank=True, choices=[('feature', 'Feature'), ('bug', 'Bug'), ('improvement', 'Improvement'), ('task', 'Task'), ('subtask', 'Sub Task')], max_length=30, null=True)), + ('priority', models.CharField(max_length=250)), + ('description', models.TextField()), + ('short_description', models.TextField()), + ('extra_points', models.TextField()), + ('technical_notes', models.TextField()), + ('procedure', models.TextField()), + ('code_tips', models.TextField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Source', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=250)), + ('url', models.TextField()), + ('active', models.BooleanField(default=True)), + ('challenge', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='challenges.Challenge')), + ], + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=250)), + ], + ), + migrations.AddField( + model_name='attachment', + name='challenge', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='challenges.Challenge'), + ), + ] diff --git a/challenges/models.py b/challenges/models.py index 71a8362..e9708d6 100644 --- a/challenges/models.py +++ b/challenges/models.py @@ -1,3 +1,59 @@ from django.db import models +from accounts.models import User -# Create your models here. +class Tag(models.Model): + def __str__(self): + return self.name + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + name = models.CharField(max_length=250,) + +class Challenge(models.Model): + + CHALLENGE_TYPES = ( + ('feature', 'Feature'), + ('bug', 'Bug'), + ('improvement', 'Improvement'), + ('task', 'Task'), + ('subtask', 'Sub Task'), + ) + + user = models.ForeignKey(User, on_delete=models.DO_NOTHING) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + is_visible = models.BooleanField(default=True) + title = models.CharField(max_length=250,) + short_title = models.CharField(max_length=250,) + owner = models.CharField(max_length=250,) + difficulty = models.CharField(max_length=250,) + challenge_type = models.CharField(max_length=30, choices=CHALLENGE_TYPES, null=True, blank=True) + priority = models.CharField(max_length=250) + description = models.TextField() + short_description = models.TextField() + extra_points = models.TextField() + technical_notes = models.TextField() + procedure = models.TextField() + code_tips = models.TextField() + +class Attachment(models.Model): + def __str__(self): + return self.name + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + name = models.CharField(max_length=250,) + url = models.TextField() + active = models.BooleanField(default=True) + challenge = models.ForeignKey(Challenge, on_delete=models.DO_NOTHING) + +class Source(models.Model): + def __str__(self): + return self.name + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + name = models.CharField(max_length=250,) + url = models.TextField() + active = models.BooleanField(default=True) + challenge = models.ForeignKey(Challenge, on_delete=models.DO_NOTHING) From 66e7b99aa20c012f0f69f88ffc850a907ab8486b Mon Sep 17 00:00:00 2001 From: David Silva Date: Sat, 6 Oct 2018 21:20:10 -0400 Subject: [PATCH 4/8] added urls, views and serializers for challenges --- api/urls.py | 1 + challenges/serializers.py | 23 +++++++++++++++++++++++ challenges/urls.py | 10 ++++++++++ challenges/views.py | 15 +++++++++++++-- 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 challenges/serializers.py create mode 100644 challenges/urls.py diff --git a/api/urls.py b/api/urls.py index 93f9ce0..89a6a89 100644 --- a/api/urls.py +++ b/api/urls.py @@ -7,4 +7,5 @@ url(admin_root_url, admin.site.urls), url(r'^services/api/', include('accounts.urls')), url(r'^services/api/', include('profile.urls')), + url(r'^services/api/', include('challenges.urls')), ] diff --git a/challenges/serializers.py b/challenges/serializers.py new file mode 100644 index 0000000..146ddee --- /dev/null +++ b/challenges/serializers.py @@ -0,0 +1,23 @@ +from rest_framework_json_api import serializers +from .models import Challenge +from accounts.models import User +from accounts.serializers import UserSerializer +from rest_framework_json_api.relations import ResourceRelatedField + +class ChallengeSerializer(serializers.ModelSerializer): + class Meta: + model = Challenge + fields = ('user', 'created_at', 'updated_at', 'title', 'short_title', 'owner', 'difficulty', + 'challenge_type', 'priority', 'description', 'short_description', + 'extra_points', 'technical_notes', 'procedure', 'code_tips') + + included_serializers = { + 'user': UserSerializer, + } + + user = ResourceRelatedField( + queryset=User.objects + ) + + class JSONAPIMeta: + included_resources = ['user',] diff --git a/challenges/urls.py b/challenges/urls.py new file mode 100644 index 0000000..2ddce46 --- /dev/null +++ b/challenges/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url, include +from . import views +from rest_framework import routers + +router = routers.DefaultRouter(trailing_slash=False) +router.register(r'challenges', views.ChallengesViewSet, base_name='Challenges') + +urlpatterns = [ + url(r'^', include(router.urls)), +] diff --git a/challenges/views.py b/challenges/views.py index 91ea44a..b1f379b 100644 --- a/challenges/views.py +++ b/challenges/views.py @@ -1,3 +1,14 @@ -from django.shortcuts import render +from rest_framework import viewsets +from .models import Challenge +from .serializers import ChallengeSerializer -# Create your views here. +class ChallengesViewSet(viewsets.ModelViewSet): + + def get_queryset(self): + queryset = Challenge.objects.all() + pk = self.request.query_params.get('pk', None) + if pk is not None: + queryset = queryset.filter(pk=pk) + return queryset + + serializer_class = ChallengeSerializer From 8de9b970e58d56e6b0d473be0ad4a237e618744a Mon Sep 17 00:00:00 2001 From: David Silva Date: Sun, 7 Oct 2018 13:12:19 -0400 Subject: [PATCH 5/8] Created failing tests for Tag and Source --- .../tests/test_requests/test_challenges.py | 69 ++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/challenges/tests/test_requests/test_challenges.py b/challenges/tests/test_requests/test_challenges.py index 419bfc6..a3d6775 100644 --- a/challenges/tests/test_requests/test_challenges.py +++ b/challenges/tests/test_requests/test_challenges.py @@ -1,22 +1,22 @@ from django.test import TestCase from rest_framework.test import APIClient, APITestCase from accounts.models import User -from challenges.models import Challenge +from challenges.models import Challenge, Tag, Attachment, Source import sure import json +import pdb class ChallengeApiTests(APITestCase): def setUp(self): self.client = APIClient() self.content_type = 'application/vnd.api+json' - def test_challenge_create(self): - """ - Ensure we get the correct challenge - """ - user = User.objects.filter(username="codecorgi").first() + def get_sample_challenge(self, user=None, tags=None): + if user is None: + user = User.objects.filter(username="codecorgi").first() - challenge = Challenge.objects.create(user=user, + challenge = Challenge.objects.create( + user=user, title = 'Test Challenge', short_title = 'Test Challenge', owner = 'Gandalf', @@ -31,6 +31,18 @@ def test_challenge_create(self): code_tips = 'Use a linter', ) + if not tags is None: + challenge.tags.set(tags) + + return challenge + + + def test_challenge_create(self): + """ + Ensure we get the correct challenge + """ + challenge = self.get_sample_challenge() + response = self.client.get(f'/services/api/challenges?pk={ challenge.id }', content_type=self.content_type) @@ -38,5 +50,46 @@ def test_challenge_create(self): response_data = json.loads(response.content) attributes = response_data['data'][0]['attributes'] - relationships = response_data['data'][0]['relationships'] attributes['title'].should.equal(challenge.title) + + def test_challenge_create_with_tags(self): + """ + Ensure we get the correct challenge + """ + tag1 = Tag.objects.create(name='Javascript') + + challenge = self.get_sample_challenge(tags=[ tag1 ]) + + response = self.client.get(f'/services/api/challenges?pk={ challenge.id }', + content_type=self.content_type) + + response.status_code.should.equal(200) + + response_data = json.loads(response.content) + attributes = response_data['data'][0]['attributes'] + included = response_data['included'] + + attributes['title'].should.equal(challenge.title) + + [item for item in included if item.get('type') == 'tags'][0]['attributes']['name'].should.equal(tag1.name) + + def test_challenge_create_with_source(self): + """ + Ensure we get the correct challenge + """ + challenge = self.get_sample_challenge() + source = Source.objects.create( + challenge=challenge, + name='github', + url='https://github.com/corgicode' + ) + + response = self.client.get(f'/services/api/challenges?pk={ challenge.id }', + content_type=self.content_type) + + response.status_code.should.equal(200) + + response_data = json.loads(response.content) + included = response_data['included'] + + [item for item in included if item.get('type') == 'sources'][0]['attributes']['name'].should.equal(source.name) From 3589d1bd25325542bacf27714e02f47701fa2bdc Mon Sep 17 00:00:00 2001 From: David Silva Date: Sun, 7 Oct 2018 13:12:32 -0400 Subject: [PATCH 6/8] fixed the models, views and serializers to read challenge info --- challenges/migrations/0002_challenge_tags.py | 18 +++++++++++ challenges/models.py | 3 +- challenges/serializers.py | 32 ++++++++++++++++++-- 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 challenges/migrations/0002_challenge_tags.py diff --git a/challenges/migrations/0002_challenge_tags.py b/challenges/migrations/0002_challenge_tags.py new file mode 100644 index 0000000..7459e14 --- /dev/null +++ b/challenges/migrations/0002_challenge_tags.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.2 on 2018-10-07 16:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('challenges', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='challenge', + name='tags', + field=models.ManyToManyField(related_name='challenges', to='challenges.Tag'), + ), + ] diff --git a/challenges/models.py b/challenges/models.py index e9708d6..abec8b3 100644 --- a/challenges/models.py +++ b/challenges/models.py @@ -35,6 +35,7 @@ class Challenge(models.Model): technical_notes = models.TextField() procedure = models.TextField() code_tips = models.TextField() + tags = models.ManyToManyField(Tag, related_name='challenges') class Attachment(models.Model): def __str__(self): @@ -56,4 +57,4 @@ def __str__(self): name = models.CharField(max_length=250,) url = models.TextField() active = models.BooleanField(default=True) - challenge = models.ForeignKey(Challenge, on_delete=models.DO_NOTHING) + challenge = models.ForeignKey(Challenge, related_name='sources', on_delete=models.DO_NOTHING) diff --git a/challenges/serializers.py b/challenges/serializers.py index 146ddee..1031582 100644 --- a/challenges/serializers.py +++ b/challenges/serializers.py @@ -1,23 +1,49 @@ from rest_framework_json_api import serializers -from .models import Challenge +from .models import Challenge, Tag, Source from accounts.models import User from accounts.serializers import UserSerializer from rest_framework_json_api.relations import ResourceRelatedField +class TagsSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = ('name',) + +class SourcesSerializer(serializers.ModelSerializer): + + queryset = Source.objects.filter(active=True,) + + class Meta: + model = Source + fields = ('name', 'url') + class ChallengeSerializer(serializers.ModelSerializer): class Meta: model = Challenge - fields = ('user', 'created_at', 'updated_at', 'title', 'short_title', 'owner', 'difficulty', + fields = ('user', 'tags', 'sources', 'created_at', 'updated_at', 'title', 'short_title', 'owner', 'difficulty', 'challenge_type', 'priority', 'description', 'short_description', 'extra_points', 'technical_notes', 'procedure', 'code_tips') included_serializers = { 'user': UserSerializer, + 'tags': TagsSerializer, + 'sources': SourcesSerializer, } user = ResourceRelatedField( queryset=User.objects ) + tags = ResourceRelatedField( + queryset=Tag.objects, + many=True, + ) + + sources = ResourceRelatedField( + queryset=Source.objects, + many=True, + ) + class JSONAPIMeta: - included_resources = ['user',] + included_resources = ['user', 'tags', 'sources'] + From 1d2a01bb2a79758fc00fd78fa580bbee0d3bf738 Mon Sep 17 00:00:00 2001 From: David Silva Date: Sun, 7 Oct 2018 13:35:32 -0400 Subject: [PATCH 7/8] Created failing test to read tag and its challenge --- .../tests/test_requests/test_challenges.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/challenges/tests/test_requests/test_challenges.py b/challenges/tests/test_requests/test_challenges.py index a3d6775..2aaa0b6 100644 --- a/challenges/tests/test_requests/test_challenges.py +++ b/challenges/tests/test_requests/test_challenges.py @@ -54,7 +54,7 @@ def test_challenge_create(self): def test_challenge_create_with_tags(self): """ - Ensure we get the correct challenge + Ensure we get the correct tags information with a challenge """ tag1 = Tag.objects.create(name='Javascript') @@ -75,7 +75,7 @@ def test_challenge_create_with_tags(self): def test_challenge_create_with_source(self): """ - Ensure we get the correct challenge + Ensure we get the correct sources information with the challenge """ challenge = self.get_sample_challenge() source = Source.objects.create( @@ -93,3 +93,25 @@ def test_challenge_create_with_source(self): included = response_data['included'] [item for item in included if item.get('type') == 'sources'][0]['attributes']['name'].should.equal(source.name) + + def get_challenges_with_tag_name(self): + """ + Ensure we get the correct challenge + """ + tag = Tag.objects.create(name='testTag') + + challenge1 = self.get_sample_challenge(tags=[ tag ]) + challenge2 = self.get_sample_challenge(tags=[ tag ]) + + response = self.client.get(f'/services/api/tags?pk={ tag.name }', + content_type=self.content_type) + + response.status_code.should.equal(200) + + response_data = json.loads(response.content) + attributes = response_data['data'][0]['attributes'] + included = response_data['included'] + + attributes['name'].should.equal(tag.name) + + len([item for item in included if item.get('type') == 'challenge']).should.equal(2) From 8fb97e5a3c438bfad013b97b1f98e392052960e1 Mon Sep 17 00:00:00 2001 From: David Silva Date: Sun, 7 Oct 2018 13:35:45 -0400 Subject: [PATCH 8/8] created serializers, url, views for Tag --- challenges/serializers.py | 16 ++++++++++++++++ challenges/urls.py | 1 + challenges/views.py | 15 +++++++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/challenges/serializers.py b/challenges/serializers.py index 1031582..37b91a8 100644 --- a/challenges/serializers.py +++ b/challenges/serializers.py @@ -47,3 +47,19 @@ class Meta: class JSONAPIMeta: included_resources = ['user', 'tags', 'sources'] +class TagGetSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = ('name', 'challenges') + + challenges = ResourceRelatedField( + queryset=Challenge.objects, + many=True, + ) + + included_serializers = { + 'challenges': ChallengeSerializer, + } + + class JSONAPIMeta: + included_resources = ['challenges',] diff --git a/challenges/urls.py b/challenges/urls.py index 2ddce46..29f4b9c 100644 --- a/challenges/urls.py +++ b/challenges/urls.py @@ -4,6 +4,7 @@ router = routers.DefaultRouter(trailing_slash=False) router.register(r'challenges', views.ChallengesViewSet, base_name='Challenges') +router.register(r'tags', views.TagsViewSet, base_name='Tags') urlpatterns = [ url(r'^', include(router.urls)), diff --git a/challenges/views.py b/challenges/views.py index b1f379b..6104289 100644 --- a/challenges/views.py +++ b/challenges/views.py @@ -1,6 +1,6 @@ from rest_framework import viewsets -from .models import Challenge -from .serializers import ChallengeSerializer +from .models import Challenge, Tag +from .serializers import ChallengeSerializer, TagGetSerializer class ChallengesViewSet(viewsets.ModelViewSet): @@ -12,3 +12,14 @@ def get_queryset(self): return queryset serializer_class = ChallengeSerializer + +class TagsViewSet(viewsets.ModelViewSet): + + def get_queryset(self): + queryset = Tag.objects.all() + pk = self.request.query_params.get('name', None) + if pk is not None: + queryset = queryset.filter(name=name) + return queryset + + serializer_class = TagGetSerializer