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/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/__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/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/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/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..abec8b3 --- /dev/null +++ b/challenges/models.py @@ -0,0 +1,60 @@ +from django.db import models +from accounts.models import User + +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() + tags = models.ManyToManyField(Tag, related_name='challenges') + +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, related_name='sources', on_delete=models.DO_NOTHING) diff --git a/challenges/serializers.py b/challenges/serializers.py new file mode 100644 index 0000000..37b91a8 --- /dev/null +++ b/challenges/serializers.py @@ -0,0 +1,65 @@ +from rest_framework_json_api import serializers +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', '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', '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/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/tests/test_requests/test_challenges.py b/challenges/tests/test_requests/test_challenges.py new file mode 100644 index 0000000..2aaa0b6 --- /dev/null +++ b/challenges/tests/test_requests/test_challenges.py @@ -0,0 +1,117 @@ +from django.test import TestCase +from rest_framework.test import APIClient, APITestCase +from accounts.models import User +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 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, + 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', + ) + + 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) + + response.status_code.should.equal(200) + + response_data = json.loads(response.content) + attributes = response_data['data'][0]['attributes'] + attributes['title'].should.equal(challenge.title) + + def test_challenge_create_with_tags(self): + """ + Ensure we get the correct tags information with a 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 sources information with the 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) + + 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) diff --git a/challenges/urls.py b/challenges/urls.py new file mode 100644 index 0000000..29f4b9c --- /dev/null +++ b/challenges/urls.py @@ -0,0 +1,11 @@ +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') +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 new file mode 100644 index 0000000..6104289 --- /dev/null +++ b/challenges/views.py @@ -0,0 +1,25 @@ +from rest_framework import viewsets +from .models import Challenge, Tag +from .serializers import ChallengeSerializer, TagGetSerializer + +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 + +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