diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..df1b201 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +################################# +# GitHub Dependabot Config info # +################################# +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 + + # Maintain dependencies for docker + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + + # Maintain dependencies for python with pip + - package-ecosystem: "pip" + directory: "/dependencies" + schedule: + interval: "daily" + open-pull-requests-limit: 10 diff --git a/.github/workflows/code-ql.yml b/.github/workflows/code-ql.yml new file mode 100644 index 0000000..882cfea --- /dev/null +++ b/.github/workflows/code-ql.yml @@ -0,0 +1,51 @@ +name: "Code Scanning - Action" + +on: + push: + pull_request: + schedule: + # ┌───────────── minute (0 - 59) + # │ ┌───────────── hour (0 - 23) + # │ │ ┌───────────── day of the month (1 - 31) + # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) + # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) + # │ │ │ │ │ + # │ │ │ │ │ + # │ │ │ │ │ + # * * * * * + - cron: '30 1 * * 0' + +jobs: + CodeQL-Build: + # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + # Override language selection by uncommenting this and choosing your languages + with: + languages: python + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below). + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following + # three lines and modify them (or add more) to build your code if your + # project uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..0292cea --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,54 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python application + +on: + push: + branches-ignore: master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Set up Python 3.5 + uses: actions/setup-python@v2 + with: + python-version: 3.5.4 + # 3.5.2 was not found + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pipenv + # if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pipenv install --clear --system + + - name: Install pyenv and pyenv-virtualenv + run: | + brew update + brew install pyenv pyenv-virtualenv + + - name: Pyenv install + run: | + pyenv install 3.5.4 + pyenv virtualenv 3.5.4 productionready + pyenv local productionready + pyenv rehash + + - name: Lint with Super-Linter + # uses: github/super-linter@v3 + # env: + # VALIDATE_ALL_CODEBASE: false + # DEFAULT_BRANCH: master + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "Would have run linting across code base and failed on errors" + + - name: Test with pytest + run: | + pytest . diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..f114d52 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,21 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'Stale issue message' + stale-pr-message: 'Stale pull request message' + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' + days-before-stale: 5 + days-before-close: 6 diff --git a/README.md b/README.md index a90f2e3..08dfc78 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,21 @@ # ![Django DRF Example App](project-logo.png) -> ### Example Django DRF codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld-example-apps) API spec. +# Example Django DRF codebase containing real world examples (CRUD, auth, +# advanced patterns, etc) that adheres to the +# [RealWorld](https://github.com/gothinkster/realworld-example-apps) API +# spec. +> - + < /a > This repo is functionality complete — PR's and issues welcome! -## Installation +# Installation -1. Clone this repository: `git clone git@github.com:gothinkster/productionready-django-api.git`. -2. `cd` into `conduit-django`: `cd productionready-django-api`. -3. Install [pyenv](https://github.com/yyuu/pyenv#installation). -4. Install [pyenv-virtualenv](https://github.com/yyuu/pyenv-virtualenv#installation). +1. Clone this repository: `git clone git @ github.com: gothinkster / productionready - django - api.git`. +2. `cd` into `conduit - django`: `cd productionready - django - api`. +3. Install[pyenv](https: // github.com / yyuu / pyenv # installation). +4. Install[pyenv - virtualenv](https: // github.com / yyuu / pyenv - virtualenv # installation). 5. Install Python 3.5.2: `pyenv install 3.5.2`. 6. Create a new virtualenv called `productionready`: `pyenv virtualenv 3.5.2 productionready`. 7. Set the local virtualenv to `productionready`: `pyenv local productionready`. @@ -19,6 +23,6 @@ This repo is functionality complete — PR's and issues welcome! If all went well then your command line prompt should now start with `(productionready)`. -If your command line prompt does not start with `(productionready)` at this point, try running `pyenv activate productionready` or `cd ../productionready-django-api`. +If your command line prompt does not start with `(productionready)` at this point, try running `pyenv activate productionready` or `cd .. / productionready - django - api`. If pyenv is still not working, visit us in the Thinkster Slack channel so we can help you out. diff --git a/conduit/apps/articles/__init__.py b/conduit/apps/articles/__init__.py index c6be3f5..1e64079 100644 --- a/conduit/apps/articles/__init__.py +++ b/conduit/apps/articles/__init__.py @@ -9,4 +9,5 @@ class ArticlesAppConfig(AppConfig): def ready(self): import conduit.apps.articles.signals + default_app_config = 'conduit.apps.articles.ArticlesAppConfig' diff --git a/conduit/apps/articles/migrations/0001_initial.py b/conduit/apps/articles/migrations/0001_initial.py index 2898383..a655c25 100644 --- a/conduit/apps/articles/migrations/0001_initial.py +++ b/conduit/apps/articles/migrations/0001_initial.py @@ -2,8 +2,8 @@ # Generated by Django 1.10 on 2016-08-28 15:43 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): @@ -18,14 +18,16 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Article', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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)), ('slug', models.SlugField(max_length=255, unique=True)), ('title', models.CharField(db_index=True, max_length=255)), ('description', models.TextField()), ('body', models.TextField()), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to='profiles.Profile')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='articles', to='profiles.Profile')), ], options={ 'abstract': False, diff --git a/conduit/apps/articles/migrations/0002_comment.py b/conduit/apps/articles/migrations/0002_comment.py index f66f188..8c21511 100644 --- a/conduit/apps/articles/migrations/0002_comment.py +++ b/conduit/apps/articles/migrations/0002_comment.py @@ -2,8 +2,8 @@ # Generated by Django 1.10 on 2016-08-28 16:00 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): @@ -17,12 +17,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Comment', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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)), ('body', models.TextField()), - ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='articles.Article')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='profiles.Profile')), + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='comments', to='articles.Article')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='comments', to='profiles.Profile')), ], options={ 'ordering': ['-created_at', '-updated_at'], diff --git a/conduit/apps/articles/migrations/0003_auto_20160828_1656.py b/conduit/apps/articles/migrations/0003_auto_20160828_1656.py index bacc6dc..9fd5749 100644 --- a/conduit/apps/articles/migrations/0003_auto_20160828_1656.py +++ b/conduit/apps/articles/migrations/0003_auto_20160828_1656.py @@ -15,7 +15,8 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Tag', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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)), ('tag', models.CharField(max_length=255)), @@ -29,6 +30,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='article', name='tags', - field=models.ManyToManyField(related_name='articles', to='articles.Tag'), + field=models.ManyToManyField( + related_name='articles', to='articles.Tag'), ), ] diff --git a/conduit/apps/articles/signals.py b/conduit/apps/articles/signals.py index 5cb9a0a..a5b8b6d 100644 --- a/conduit/apps/articles/signals.py +++ b/conduit/apps/articles/signals.py @@ -6,6 +6,7 @@ from .models import Article + @receiver(pre_save, sender=Article) def add_slug_to_article_if_not_exists(sender, instance, *args, **kwargs): MAXIMUM_SLUG_LENGTH = 255 diff --git a/conduit/apps/articles/urls.py b/conduit/apps/articles/urls.py index ed3e46e..5ceafc4 100644 --- a/conduit/apps/articles/urls.py +++ b/conduit/apps/articles/urls.py @@ -1,11 +1,9 @@ from django.conf.urls import include, url - from rest_framework.routers import DefaultRouter -from .views import ( - ArticleViewSet, ArticlesFavoriteAPIView, ArticlesFeedAPIView, - CommentsListCreateAPIView, CommentsDestroyAPIView, TagListAPIView -) +from .views import (ArticlesFavoriteAPIView, ArticlesFeedAPIView, + ArticleViewSet, CommentsDestroyAPIView, + CommentsListCreateAPIView, TagListAPIView) router = DefaultRouter(trailing_slash=False) router.register(r'articles', ArticleViewSet) @@ -18,7 +16,7 @@ url(r'^articles/(?P[-\w]+)/favorite/?$', ArticlesFavoriteAPIView.as_view()), - url(r'^articles/(?P[-\w]+)/comments/?$', + url(r'^articles/(?P[-\w]+)/comments/?$', CommentsListCreateAPIView.as_view()), url(r'^articles/(?P[-\w]+)/comments/(?P[\d]+)/?$', diff --git a/conduit/apps/articles/views.py b/conduit/apps/articles/views.py index 5da36d6..3804b5d 100644 --- a/conduit/apps/articles/views.py +++ b/conduit/apps/articles/views.py @@ -1,8 +1,7 @@ from rest_framework import generics, mixins, status, viewsets from rest_framework.exceptions import NotFound -from rest_framework.permissions import ( - AllowAny, IsAuthenticated, IsAuthenticatedOrReadOnly -) +from rest_framework.permissions import (AllowAny, IsAuthenticated, + IsAuthenticatedOrReadOnly) from rest_framework.response import Response from rest_framework.views import APIView @@ -11,7 +10,7 @@ from .serializers import ArticleSerializer, CommentSerializer, TagSerializer -class ArticleViewSet(mixins.CreateModelMixin, +class ArticleViewSet(mixins.CreateModelMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): @@ -49,7 +48,7 @@ def create(self, request): serializer_data = request.data.get('article', {}) serializer = self.serializer_class( - data=serializer_data, context=serializer_context + data=serializer_data, context=serializer_context ) serializer.is_valid(raise_exception=True) serializer.save() @@ -83,7 +82,6 @@ def retrieve(self, request, slug): return Response(serializer.data, status=status.HTTP_200_OK) - def update(self, request, slug): serializer_context = {'request': request} @@ -91,13 +89,13 @@ def update(self, request, slug): serializer_instance = self.queryset.get(slug=slug) except Article.DoesNotExist: raise NotFound('An article with this slug does not exist.') - + serializer_data = request.data.get('article', {}) serializer = self.serializer_class( - serializer_instance, + serializer_instance, context=serializer_context, - data=serializer_data, + data=serializer_data, partial=True ) serializer.is_valid(raise_exception=True) diff --git a/conduit/apps/authentication/__init__.py b/conduit/apps/authentication/__init__.py index 3df2e3c..e02c569 100644 --- a/conduit/apps/authentication/__init__.py +++ b/conduit/apps/authentication/__init__.py @@ -9,6 +9,7 @@ class AuthenticationAppConfig(AppConfig): def ready(self): import conduit.apps.authentication.signals + # This is how we register our custom app config with Django. Django is smart # enough to look for the `default_app_config` property of each registered app # and use the correct app config based on that value. diff --git a/conduit/apps/authentication/backends.py b/conduit/apps/authentication/backends.py index 3d9d7fc..31addab 100644 --- a/conduit/apps/authentication/backends.py +++ b/conduit/apps/authentication/backends.py @@ -1,7 +1,5 @@ import jwt - from django.conf import settings - from rest_framework import authentication, exceptions from .models import User @@ -33,7 +31,7 @@ def authenticate(self, request): request.user = None # `auth_header` should be an array with two elements: 1) the name of - # the authentication header (in this case, "Token") and 2) the JWT + # the authentication header (in this case, "Token") and 2) the JWT # that we should authenticate against. auth_header = authentication.get_authorization_header(request).split() auth_header_prefix = self.authentication_header_prefix.lower() diff --git a/conduit/apps/authentication/migrations/0001_initial.py b/conduit/apps/authentication/migrations/0001_initial.py index bbd2d9c..beda6d3 100644 --- a/conduit/apps/authentication/migrations/0001_initial.py +++ b/conduit/apps/authentication/migrations/0001_initial.py @@ -17,18 +17,26 @@ class Migration(migrations.Migration): migrations.CreateModel( name='User', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(db_index=True, max_length=255, unique=True)), - ('email', models.EmailField(db_index=True, max_length=254, unique=True)), + ('id', models.AutoField(auto_created=True, + primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField( + max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField( + blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, + help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField( + db_index=True, max_length=255, unique=True)), + ('email', models.EmailField( + db_index=True, max_length=254, unique=True)), ('is_active', models.BooleanField(default=True)), ('is_staff', models.BooleanField(default=False)), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', + related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', + related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), ], options={ 'abstract': False, diff --git a/conduit/apps/authentication/models.py b/conduit/apps/authentication/models.py index 2129957..a6a6686 100644 --- a/conduit/apps/authentication/models.py +++ b/conduit/apps/authentication/models.py @@ -1,11 +1,9 @@ -import jwt - from datetime import datetime, timedelta +import jwt from django.conf import settings -from django.contrib.auth.models import ( - AbstractBaseUser, BaseUserManager, PermissionsMixin -) +from django.contrib.auth.models import (AbstractBaseUser, BaseUserManager, + PermissionsMixin) from django.db import models from conduit.apps.core.models import TimestampedModel @@ -36,21 +34,21 @@ def create_user(self, username, email, password=None): return user def create_superuser(self, username, email, password): - """ - Create and return a `User` with superuser powers. + """ + Create and return a `User` with superuser powers. - Superuser powers means that this use is an admin that can do anything - they want. - """ - if password is None: - raise TypeError('Superusers must have a password.') + Superuser powers means that this use is an admin that can do anything + they want. + """ + if password is None: + raise TypeError('Superusers must have a password.') - user = self.create_user(username, email, password) - user.is_superuser = True - user.is_staff = True - user.save() + user = self.create_user(username, email, password) + user.is_superuser = True + user.is_staff = True + user.save() - return user + return user class User(AbstractBaseUser, PermissionsMixin, TimestampedModel): @@ -109,12 +107,12 @@ def token(self): return self._generate_jwt_token() def get_full_name(self): - """ - This method is required by Django for things like handling emails. - Typically, this would be the user's first and last name. Since we do - not store the user's real name, we return their username instead. - """ - return self.username + """ + This method is required by Django for things like handling emails. + Typically, this would be the user's first and last name. Since we do + not store the user's real name, we return their username instead. + """ + return self.username def get_short_name(self): """ diff --git a/conduit/apps/authentication/serializers.py b/conduit/apps/authentication/serializers.py index d9894ce..e441eb9 100644 --- a/conduit/apps/authentication/serializers.py +++ b/conduit/apps/authentication/serializers.py @@ -1,5 +1,4 @@ from django.contrib.auth import authenticate - from rest_framework import serializers from conduit.apps.profiles.serializers import ProfileSerializer @@ -84,8 +83,6 @@ def validate(self, data): 'This user has been deactivated.' ) - - # The `validate` method should return a dictionary of validated data. # This is the data that is passed to the `create` and `update` methods # that we will see later on. @@ -99,7 +96,7 @@ def validate(self, data): class UserSerializer(serializers.ModelSerializer): """Handles serialization and deserialization of User objects.""" - # Passwords must be at least 8 characters, but no more than 128 + # Passwords must be at least 8 characters, but no more than 128 # characters. These values are the default provided by Django. We could # change them, but that would create extra work while introducing no real # benefit, so let's just stick with the defaults. @@ -113,7 +110,7 @@ class UserSerializer(serializers.ModelSerializer): # so. Moreover, `UserSerializer` should never expose profile information, # so we set `write_only=True`. profile = ProfileSerializer(write_only=True) - + # We want to get the `bio` and `image` fields from the related Profile # model. bio = serializers.CharField(source='profile.bio', read_only=True) @@ -122,7 +119,7 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ( - 'email', 'username', 'password', 'token', 'profile', 'bio', + 'email', 'username', 'password', 'token', 'profile', 'bio', 'image', ) @@ -130,12 +127,11 @@ class Meta: # specifying the field with `read_only=True` like we did for password # above. The reason we want to use `read_only_fields` here is because # we don't need to specify anything else about the field. For the - # password field, we needed to specify the `min_length` and + # password field, we needed to specify the `min_length` and # `max_length` properties too, but that isn't the case for the token # field. read_only_fields = ('token',) - def update(self, instance, validated_data): """Performs an update on a User.""" @@ -169,7 +165,7 @@ def update(self, instance, validated_data): # We're doing the same thing as above, but this time we're making # changes to the Profile model. setattr(instance.profile, key, value) - + # Save the profile just like we saved the user. instance.profile.save() diff --git a/conduit/apps/authentication/signals.py b/conduit/apps/authentication/signals.py index 2e5168f..4df1bbd 100644 --- a/conduit/apps/authentication/signals.py +++ b/conduit/apps/authentication/signals.py @@ -5,6 +5,7 @@ from .models import User + @receiver(post_save, sender=User) def create_related_profile(sender, instance, created, *args, **kwargs): # Notice that we're checking for `created` here. We only want to do this diff --git a/conduit/apps/authentication/urls.py b/conduit/apps/authentication/urls.py index 2a594ca..3f3e3b1 100644 --- a/conduit/apps/authentication/urls.py +++ b/conduit/apps/authentication/urls.py @@ -1,8 +1,6 @@ from django.conf.urls import url -from .views import ( - LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView -) +from .views import LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView urlpatterns = [ url(r'^user/?$', UserRetrieveUpdateAPIView.as_view()), diff --git a/conduit/apps/authentication/views.py b/conduit/apps/authentication/views.py index b12d867..880ccd4 100644 --- a/conduit/apps/authentication/views.py +++ b/conduit/apps/authentication/views.py @@ -5,9 +5,8 @@ from rest_framework.views import APIView from .renderers import UserJSONRenderer -from .serializers import ( - LoginSerializer, RegistrationSerializer, UserSerializer -) +from .serializers import (LoginSerializer, RegistrationSerializer, + UserSerializer) class RegistrationAPIView(APIView): @@ -82,4 +81,3 @@ def update(self, request, *args, **kwargs): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) - diff --git a/conduit/apps/core/exceptions.py b/conduit/apps/core/exceptions.py index 44baf3c..fdd43f7 100644 --- a/conduit/apps/core/exceptions.py +++ b/conduit/apps/core/exceptions.py @@ -1,5 +1,6 @@ from rest_framework.views import exception_handler + def core_exception_handler(exc, context): # If an exception is thrown that we don't explicitly handle here, we want # to delegate to the default exception handler offered by DRF. If we do @@ -17,12 +18,13 @@ def core_exception_handler(exc, context): if exception_class in handlers: # If this exception is one that we can handle, handle it. Otherwise, - # return the response generated earlier by the default exception + # return the response generated earlier by the default exception # handler. return handlers[exception_class](exc, context, response) return response + def _handle_generic_error(exc, context, response): # This is about the most straightforward exception handler we can create. # We take the response generated by DRF and wrap it in the `errors` key. @@ -32,6 +34,7 @@ def _handle_generic_error(exc, context, response): return response + def _handle_not_found_error(exc, context, response): view = context.get('view', None) diff --git a/conduit/apps/core/utils.py b/conduit/apps/core/utils.py index 48a09ac..20e12e1 100644 --- a/conduit/apps/core/utils.py +++ b/conduit/apps/core/utils.py @@ -3,5 +3,6 @@ DEFAULT_CHAR_STRING = string.ascii_lowercase + string.digits + def generate_random_string(chars=DEFAULT_CHAR_STRING, size=6): return ''.join(random.choice(chars) for _ in range(size)) diff --git a/conduit/apps/profiles/migrations/0001_initial.py b/conduit/apps/profiles/migrations/0001_initial.py index 85825ec..12bd12b 100644 --- a/conduit/apps/profiles/migrations/0001_initial.py +++ b/conduit/apps/profiles/migrations/0001_initial.py @@ -2,9 +2,9 @@ # Generated by Django 1.10 on 2016-08-28 15:06 from __future__ import unicode_literals +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): @@ -19,12 +19,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Profile', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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)), ('bio', models.TextField(blank=True)), ('image', models.URLField(blank=True)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('user', models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], options={ 'ordering': ['-created_at', '-updated_at'], diff --git a/conduit/apps/profiles/migrations/0002_profile_follows.py b/conduit/apps/profiles/migrations/0002_profile_follows.py index 45dfbdc..9685e9e 100644 --- a/conduit/apps/profiles/migrations/0002_profile_follows.py +++ b/conduit/apps/profiles/migrations/0002_profile_follows.py @@ -15,6 +15,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='profile', name='follows', - field=models.ManyToManyField(related_name='followed_by', to='profiles.Profile'), + field=models.ManyToManyField( + related_name='followed_by', to='profiles.Profile'), ), ] diff --git a/conduit/apps/profiles/migrations/0003_profile_favorites.py b/conduit/apps/profiles/migrations/0003_profile_favorites.py index 21ed426..57a22ae 100644 --- a/conduit/apps/profiles/migrations/0003_profile_favorites.py +++ b/conduit/apps/profiles/migrations/0003_profile_favorites.py @@ -16,6 +16,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='profile', name='favorites', - field=models.ManyToManyField(related_name='favorited_by', to='articles.Article'), + field=models.ManyToManyField( + related_name='favorited_by', to='articles.Article'), ), ] diff --git a/conduit/apps/profiles/models.py b/conduit/apps/profiles/models.py index 6c47aa8..4841f3a 100644 --- a/conduit/apps/profiles/models.py +++ b/conduit/apps/profiles/models.py @@ -37,7 +37,6 @@ class Profile(TimestampedModel): related_name='favorited_by' ) - def __str__(self): return self.user.username diff --git a/conduit/apps/profiles/urls.py b/conduit/apps/profiles/urls.py index 7ca5610..9d2ab6f 100644 --- a/conduit/apps/profiles/urls.py +++ b/conduit/apps/profiles/urls.py @@ -1,9 +1,9 @@ from django.conf.urls import url -from .views import ProfileRetrieveAPIView, ProfileFollowAPIView +from .views import ProfileFollowAPIView, ProfileRetrieveAPIView urlpatterns = [ url(r'^profiles/(?P\w+)/?$', ProfileRetrieveAPIView.as_view()), - url(r'^profiles/(?P\w+)/follow/?$', + url(r'^profiles/(?P\w+)/follow/?$', ProfileFollowAPIView.as_view()), ] diff --git a/dependencies/Pipfile b/dependencies/Pipfile new file mode 100644 index 0000000..091a76e --- /dev/null +++ b/dependencies/Pipfile @@ -0,0 +1,20 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] +Django = "*" +django-cors-middleware = "*" +django-extensions = "*" +djangorestframework = "*" +PyJWT = "*" +six = "*" + +[requires] +python_version = "3.5" + +[pipenv] +allow_prereleases = true \ No newline at end of file diff --git a/project-logo.png b/project-logo.png index 38af45f..2fc614d 100644 Binary files a/project-logo.png and b/project-logo.png differ diff --git a/requirements.txt b/requirements.txt index 6eb9d54..83836d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ -Django==1.10.5 +Django==1.11.29 django-cors-middleware==1.3.1 django-extensions==1.7.1 djangorestframework==3.4.4 PyJWT==1.4.2 six==1.10.0 +