diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index f5f4cf4..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,42 +0,0 @@ -version: 2 -jobs: - build: - working_directory: ~/django-api - docker: - - image: circleci/python:3.6.4 - environment: - PIPENV_VENV_IN_PROJECT: true - DJANGO_SECRET_KEY: qAhCPjJP5wrixDBf0qhQd62TxJ0CirclECi - DJANGO_DEBUG: True - DB_POSTGRES_DATABASE_NAME: circle_test - DB_POSTGRES_USERNAME: root - DB_POSTGRES_PASSWORD: '' - DB_POSTGRES_HOSTNAME: localhost - DB_POSTGRES_PORT: 5432 - API_SERVICES_URL: http://localhost:9000/services/api/ - FRONTEND_APP_URL: http://localhost:3000/ - - - image: circleci/postgres:9.6.2 - environment: - POSTGRES_USER: root - POSTGRES_DB: circle_test - POSTGRES_PASSWORD: '' - steps: - - checkout - - run: sudo chown -R circleci:circleci /usr/local/bin - - run: sudo chown -R circleci:circleci /usr/local/lib/python3.6/site-packages - - restore_cache: - key: deps9-{{ .Branch }}-{{ checksum "requirements.txt" }} - - run: - command: | - sudo pip install pipenv - pipenv install - - save_cache: - key: deps9-{{ .Branch }}-{{ checksum "requirements.txt" }} - paths: - - ".venv" - - "/usr/local/bin" - - "/usr/local/lib/python3.6/site-packages" - - run: - command: | - pipenv run "python manage.py test" diff --git a/.gitignore b/.gitignore index aa1db36..de02bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ __pycache__/ .DS_Store .envrc .vscode -data/postgres \ No newline at end of file +backups +data diff --git a/README.md b/README.md index 3d3bfd3..e1b7fd5 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,166 @@ -# DjangoAPI for codecorgi.co +# codecorgi -[![Waffle.io - Issues in progress](https://badge.waffle.io/corgicode/django-api.png?label=in%20progress&title=In%20Progress)](http://waffle.io/corgicode/django-api) +Checkout our progress on [![Waffle.io](https://badge.waffle.io/corgicode/django-api.svg?columns=all)](https://waffle.io/corgicode/django-api) -[![CircleCI](https://circleci.com/gh/corgicode/django-api/tree/dev.svg?style=svg)](https://circleci.com/gh/corgicode/django-api/tree/dev) +[![Build Status](https://circleci.com/gh/corgicode/django-api.svg?style=shield)](https://circleci.com/gh/corgicode/django-api) -## Requirements + Codecorgi is a visual portfolio for front-end developers to showcase their + code and experience. Codecorgi's vision is to help employ the workforce by + providing developers continuous training and project experience. Our company + believes that junior developers have a difficulty landing their first + programming career. -### Using Docker +# Contributing -Install docker and docker-compose and run the command +Todo :( +Follow our [code of conduct on github](https://github.com/corgicode/frontend-react/blob/dev/CODE_OF_CONDUCT.md). + +## Installing + +We're using docker and docker-compose to start the application. + +Install Docker. + +```bash +http://www.docker.com/products/docker#/mac +``` + +I used this tutorial to help me get the hang of things. I recommend it for getting started. + +```bash +https://prakhar.me/docker-curriculum/ ``` + +Grab the repo: + +```bash +git clone git@github.com:corgicode/django-api.git +``` + +Then start the containers: + +```bash docker-compose up ``` -This will start the containers and link them +It will take a long time running the first time while downloading all the +dependencies, but the future times it will be super quick. + +Run `docker-compose down` when you're not working to save resources in your machine, +and `docker-compose restart` if you need to restart the application. -### Launching locally: +Now you can visit your local version going to `http://localhost:9000`. -Install postgres and run it. +The watcher should restart the application everytime a file changes in the backend, +but if you notice that is not happening, run the restart command manually. -Create a virtual environment using python3, like: +If the application can't be accessed an error might have occurred, to look at the logs +run the command `docker-compose logs -f --tail=10 web`. + +## Env variables + +The four following variables are needed to run the application. ``` -mkvirtualenv corgi -p /usr/local/bin/python3.6 +GITHUB_CLIENT_ID +GITHUB_CLIENT_SECRET +GITHUB_CALLBACK_URL +GITHUB_APP_NAME +ADMIN_API_KEY +BASE_URL +MAILCHIMP_LIST_ID +MAILCHIMP_API_KEY ``` -Add the following env variables, with the correct values. +The github ones are pretty self explanatory, register an application [here](https://github.com/settings/applications/new), +is used for authentication and to get some information about the users. +The Admin api key is a key that can be included in the request headers to allow for admin access, temp solution. + +Base url is mostly used to redirect to routes in the front end. + +## Migrations (Seed data) + +Make an admin request to `docker-compose run web python3 manage.py migrate` to run the migrations. + +## Developing + + +### PyLint + +Linting will help identify: + +- formatting discrepancy +- non-adherence to coding standards and conventions +- pinpointing possible logical errors in your program + +Running a Lint program over your source code, helps to ensure that source code +is legible, readable, less polluted and easy to maintain. + +### Git Flow + +Git flow is a branching model and a plugin for git that +helps you manage your branches easier, that way we don't +overstep in each other codes, all the contributions should go through pull requests, +so git flow will help you manage your workflow easier. + +First you need to install and activate git flow, to install on mac use +[homebrew](https://brew.sh/): + +```bash +brew install git-flow ``` -export DJANGO_SECRET_KEY= -export DJANGO_DEBUG= -export DB_POSTGRES_DATABASE_NAME=corgi_development -export DB_POSTGRES_USERNAME= -export DB_POSTGRES_PASSWORD= -export DB_POSTGRES_HOSTNAME=localhost -export DB_POSTGRES_PORT=5432 - -export API_SERVICES_URL=http://localhost:8000/services/api/ -export FRONTEND_APP_URL=http://localhost:3000/ -``` + +Then run `git flow init` on the root of the project to set up your git flow configuration. + +To start a new feature, like to close a ticket or add some code, run the command, where +NAME is a short description of the issue or the ticket number from gitlab. For example +`git flow feature start adding-users`. + +Commit often, and push to your branch, and when you're ready create a merge request on gitlab. +An admin will approve the request and merge your code into develop, and create new releases. + +For now that's all you need to know, you can find more information about git flow +[here](http://nvie.com/posts/a-successful-git-branching-model/). + +Detailed installation instructions [here](https://github.com/nvie/gitflow/wiki/Installation). + +## Dependencies + +The following are tools, packages or technologies used. + +### Django + +Django makes it easier to build better Web apps more quickly and with less code. + +[Homepage](https://www.djangoproject.com/). + +I recommend following a short django tutorial before jumping into the code, +to help you understand what requiring is, middleware, routes, etc. + +This [one in their documentation](https://www.djangoproject.com/start/) +seems complete enough, but feel free to use whatever one you prefer. + +### JSON Api + +[JSON Api](http://jsonapi.org/) is an specification for building APIs in JSON. +By following shared conventions, you can increase productivity, take advantage of +generalized tooling, and focus on what matters: your application + +### Postgres + +### Redis + +Redis is a fast, open source, in-memory key-value data structure store. + +Writing to Redis is a lot faster than writing to Mongo or other data stores, +the data can be set with an expiration date and it doesn't offer the same reliability. +Making it perfect for caching data. Every time we run a long operation we can store +the result of that operation in the redis database, and the next time that same result +is needed we can fetch it from redis instead of running the operation again, that will +make the users happier and save resources on the server. + +[What is redis](https://aws.amazon.com/elasticache/what-is-redis/). + +[Try a demon of redis online](http://try.redis.io/). diff --git a/accounts/admin.py b/accounts/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/accounts/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/accounts/apps.py b/accounts/apps.py deleted file mode 100644 index 9b3fc5a..0000000 --- a/accounts/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class AccountsConfig(AppConfig): - name = 'accounts' diff --git a/accounts/models.py b/accounts/models.py deleted file mode 100644 index ed4614c..0000000 --- a/accounts/models.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.db import models -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager -from django.contrib.auth import password_validation - -class UserManager(BaseUserManager): - use_in_migrations = True - - def create_user(self, email, password=None, **kwargs): - user = self.model(email=email, password=password, **kwargs) - - user.save(using=self._db) - return user - - def create_superuser(self, email, username, password): - user = self.create_user(email, password=password, username=username, is_admin=True) - user.save(using=self._db) - return user - - def get_by_natural_key(self, username): - case_insensitive_username_field = '{}__iexact'.format(self.model.USERNAME_FIELD) - return self.get(**{case_insensitive_username_field: username}) - -class User(AbstractBaseUser): - USERNAME_FIELD = 'username' - - REQUIRED_FIELDS = ['email'] - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - password = models.CharField( - max_length=128, - verbose_name='password', - validators=[password_validation.validate_password] - ) - email = models.EmailField( - verbose_name='email address', - max_length=255, - unique=True, - ) - username = models.CharField(max_length=100, unique=True) - is_active = models.BooleanField(default=True) - is_verified = models.BooleanField(default=False) - is_admin = models.BooleanField(default=False) - flagged = models.BooleanField(default=False) - - objects = UserManager() - - def __str__(self): - return self.username - - @property - def is_staff(self): - return self.is_admin diff --git a/accounts/serializers.py b/accounts/serializers.py deleted file mode 100644 index 01c7087..0000000 --- a/accounts/serializers.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework_json_api import serializers -from accounts.models import User - - -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ('username',) \ No newline at end of file diff --git a/accounts/tests/test_requests/__init__.py b/accounts/tests/test_requests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/accounts/tests/test_requests/test_users.py b/accounts/tests/test_requests/test_users.py deleted file mode 100644 index e80485e..0000000 --- a/accounts/tests/test_requests/test_users.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.test import TestCase -from rest_framework.test import APIClient, APITestCase -from accounts.models import User -import sure -import json - - -class UserApiTests(APITestCase): - def setUp(self): - self.client = APIClient() - self.content_type = 'application/vnd.api+json' - - def test_patient_current_user_get(self): - """ - Ensure we get a user. - """ - response = self.client.get("/services/api/users", - content_type=self.content_type) - - response.status_code.should.equal(200) - - response_data = json.loads(response.content) - attributes = json.loads(response.content)['data'][0]['attributes'] - attributes['username'].should.equal('codecorgi') diff --git a/accounts/views.py b/accounts/views.py deleted file mode 100644 index 601632f..0000000 --- a/accounts/views.py +++ /dev/null @@ -1,7 +0,0 @@ -from rest_framework import viewsets -from accounts.models import User -from accounts.serializers import UserSerializer - -class UserViewSet(viewsets.ModelViewSet): - queryset = User.objects.all() - serializer_class = UserSerializer diff --git a/api/settings.py b/api/settings.py index 5bd9e69..978bc4e 100644 --- a/api/settings.py +++ b/api/settings.py @@ -1,13 +1,13 @@ """ Django settings for api project. -Generated by 'django-admin startproject' using Django 2.1.2. +Generated by 'django-admin startproject' using Django 1.11.15. For more information on this file, see -https://docs.djangoproject.com/en/2.1/topics/settings/ +https://docs.djangoproject.com/en/1.11/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/2.1/ref/settings/ +https://docs.djangoproject.com/en/1.11/ref/settings/ """ import os @@ -17,16 +17,17 @@ # Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') +SECRET_KEY = '(@n$7n&3l_9-_j1cv$_-ao!=4$ug-s2=+)cmb51n0ue5246_8e' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True if os.getenv('DJANGO_DEBUG') == 'True' else False +DEBUG = True ALLOWED_HOSTS = ['*'] + # Application definition INSTALLED_APPS = [ @@ -36,10 +37,12 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'accounts.apps.AccountsConfig', - 'profile.apps.ProfileConfig', - 'challenges.apps.ChallengesConfig', + 'safedelete', + 'adminplus', 'rest_framework', + 'usermanagement.apps.UsermanagementConfig', + 'challenges.apps.ChallengesConfig', + 'profile.apps.ProfileConfig', ] MIDDLEWARE = [ @@ -74,22 +77,21 @@ # Database -# https://docs.djangoproject.com/en/2.1/ref/settings/#databases +# https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('DB_POSTGRES_DATABASE_NAME'), - 'USER': os.getenv('DB_POSTGRES_USERNAME'), - 'PASSWORD': os.getenv('DB_POSTGRES_PASSWORD'), - 'HOST': os.getenv('DB_POSTGRES_HOSTNAME'), - 'PORT': os.getenv('DB_POSTGRES_PORT'), + 'NAME': 'dbName', + 'PASSWORD': 'dbPassword', + 'USER': 'dbUsername', + 'HOST': 'db', + 'PORT': 5432, } } - # Password validation -# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators +# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { @@ -106,33 +108,9 @@ }, ] -REST_FRAMEWORK = { - 'PAGE_SIZE': 100, - 'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler', - 'DEFAULT_PARSER_CLASSES': ( - 'rest_framework_json_api.parsers.JSONParser', - 'rest_framework.parsers.FormParser', - 'rest_framework.parsers.MultiPartParser', - 'rest_framework.parsers.JSONParser', - ), - 'DEFAULT_RENDERER_CLASSES': ( - 'rest_framework_json_api.renderers.JSONRenderer', - 'rest_framework.renderers.BrowsableAPIRenderer', - ), - 'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata', - 'TEST_REQUEST_DEFAULT_FORMAT': 'json', - 'DEFAULT_PAGINATION_CLASS': - 'rest_framework_json_api.pagination.JsonApiPageNumberPagination', -} - -JSON_API_FORMAT_KEYS = 'underscore' -JSON_API_FORMAT_TYPES = 'underscore' -JSON_API_PLURALIZE_TYPES = True -JSON_API_PLURALIZE_RELATION_TYPE = True -JSON_API_FORMAT_RELATION_KEYS = True # Internationalization -# https://docs.djangoproject.com/en/2.1/topics/i18n/ +# https://docs.djangoproject.com/en/1.11/topics/i18n/ LANGUAGE_CODE = 'en-us' @@ -146,8 +124,8 @@ # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.1/howto/static-files/ +# https://docs.djangoproject.com/en/1.11/howto/static-files/ STATIC_URL = '/static/' -AUTH_USER_MODEL = 'accounts.User' +AUTH_USER_MODEL = 'usermanagement.User' diff --git a/api/urls.py b/api/urls.py index 89a6a89..b199a00 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,11 +1,18 @@ from django.conf.urls import url, include from django.contrib import admin +urlpatterns = [ + url(r'^admin/', admin.site.urls), +] + +admin.autodiscover() + +admin.site.site_header = 'codecorgi Admin' + admin_root_url = r'^services/admin/' urlpatterns = [ - 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')), + url(admin_root_url, include(admin.site.urls)), + # url(r'^services/api/', include('usermanagement.urls')), + # url(r'^services/api/', include('challenges.urls')), ] diff --git a/api/wsgi.py b/api/wsgi.py index 462e4d0..925af73 100644 --- a/api/wsgi.py +++ b/api/wsgi.py @@ -4,13 +4,13 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings") application = get_wsgi_application() diff --git a/accounts/__init__.py b/challenges/__init.py__ similarity index 100% rename from accounts/__init__.py rename to challenges/__init.py__ diff --git a/challenges/__init__.py b/challenges/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/challenges/admin.py b/challenges/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/challenges/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/challenges/apps.py b/challenges/apps.py index 621ad57..13db77d 100644 --- a/challenges/apps.py +++ b/challenges/apps.py @@ -1,5 +1,6 @@ +from __future__ import unicode_literals from django.apps import AppConfig - class ChallengesConfig(AppConfig): name = 'challenges' + verbose_name = 'Challenge Management' diff --git a/challenges/migrations/0001_initial.py b/challenges/migrations/0001_initial.py index c13ee9b..20758c9 100644 --- a/challenges/migrations/0001_initial.py +++ b/challenges/migrations/0001_initial.py @@ -1,4 +1,6 @@ -# Generated by Django 2.1.2 on 2018-10-07 00:29 +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2018-10-02 05:14 +from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models @@ -29,14 +31,16 @@ class Migration(migrations.Migration): name='Challenge', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deleted', models.DateTimeField(editable=False, null=True)), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('is_visible', models.BooleanField(default=True)), + ('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)), + ('challenge_type', models.CharField(max_length=250)), + ('date_created', models.DateTimeField()), ('priority', models.CharField(max_length=250)), ('description', models.TextField()), ('short_description', models.TextField()), @@ -44,8 +48,10 @@ class Migration(migrations.Migration): ('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)), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='Source', @@ -68,6 +74,16 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=250)), ], ), + migrations.AddField( + model_name='challenge', + name='tags', + field=models.ManyToManyField(to='challenges.Tag'), + ), + migrations.AddField( + model_name='challenge', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL), + ), migrations.AddField( model_name='attachment', name='challenge', diff --git a/challenges/migrations/0002_challenge_tags.py b/challenges/migrations/0002_challenge_tags.py deleted file mode 100644 index 7459e14..0000000 --- a/challenges/migrations/0002_challenge_tags.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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 abec8b3..0de95b0 100644 --- a/challenges/models.py +++ b/challenges/models.py @@ -1,5 +1,6 @@ from django.db import models -from accounts.models import User +from safedelete.models import SafeDeleteModel, SOFT_DELETE +from usermanagement.models import User class Tag(models.Model): def __str__(self): @@ -9,25 +10,20 @@ def __str__(self): updated_at = models.DateTimeField(auto_now=True) name = models.CharField(max_length=250,) -class Challenge(models.Model): +class Challenge(SafeDeleteModel): - CHALLENGE_TYPES = ( - ('feature', 'Feature'), - ('bug', 'Bug'), - ('improvement', 'Improvement'), - ('task', 'Task'), - ('subtask', 'Sub Task'), - ) + _safedelete_policy = SOFT_DELETE 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) + 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) + challenge_type = models.CharField(max_length=250,) + date_created = models.DateTimeField() priority = models.CharField(max_length=250) description = models.TextField() short_description = models.TextField() @@ -35,7 +31,7 @@ class Challenge(models.Model): technical_notes = models.TextField() procedure = models.TextField() code_tips = models.TextField() - tags = models.ManyToManyField(Tag, related_name='challenges') + tags = models.ManyToManyField(Tag) class Attachment(models.Model): def __str__(self): @@ -57,4 +53,4 @@ def __str__(self): 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) + challenge = models.ForeignKey(Challenge, on_delete=models.DO_NOTHING) diff --git a/challenges/serializers.py b/challenges/serializers.py deleted file mode 100644 index 37b91a8..0000000 --- a/challenges/serializers.py +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index e69de29..0000000 diff --git a/challenges/tests/test_requests/__init__.py b/challenges/tests/test_requests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/challenges/tests/test_requests/test_challenges.py b/challenges/tests/test_requests/test_challenges.py deleted file mode 100644 index 2aaa0b6..0000000 --- a/challenges/tests/test_requests/test_challenges.py +++ /dev/null @@ -1,117 +0,0 @@ -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 deleted file mode 100644 index 29f4b9c..0000000 --- a/challenges/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 6104289..0000000 --- a/challenges/views.py +++ /dev/null @@ -1,25 +0,0 @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index ae83052..4f2f8b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: image: postgres ports: - "5432:5432" + expose: + - "5432" volumes: - ./backups:/home/backups - ./data/postgres:/var/lib/postgresql/data @@ -27,13 +29,3 @@ services: - db volumes: - .:/code - environment: - - DJANGO_SECRET_KEY=qAhCPjJP5wrixDBf0qhQd62TxJ0y9dtz - - DJANGO_DEBUG=True - - DB_POSTGRES_DATABASE_NAME=dbName - - DB_POSTGRES_USERNAME=dbUsername - - DB_POSTGRES_PASSWORD=dbPassword - - DB_POSTGRES_HOSTNAME=db - - DB_POSTGRES_PORT=5432 - - API_SERVICES_URL=http://localhost:9000/services/api/ - - FRONTEND_APP_URL=http://localhost:3000/ diff --git a/entrypoint.sh b/entrypoint.sh index 0d4cc48..bcf162a 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,7 +1,3 @@ #!/usr/bin/env bash -sleep 6s - -echo 'Starting Django-API application, with db:' -echo $DB_POSTGRES_DATABASE_NAME - +sleep 10s python3 manage.py runserver 0.0.0.0:9000 diff --git a/manage.py b/manage.py index 0baf6d3..2385de7 100755 --- a/manage.py +++ b/manage.py @@ -2,14 +2,21 @@ import os import sys -if __name__ == '__main__': - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api.settings') +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings") try: from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise execute_from_command_line(sys.argv) diff --git a/profile/admin.py b/profile/admin.py new file mode 100644 index 0000000..a597433 --- /dev/null +++ b/profile/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin +from .models import Profile, ProfileURL + +class ProfileAdmin(admin.ModelAdmin): + list_display = ('user', 'location', 'company') + + pass + +class ProfileURLAdmin(admin.ModelAdmin): + list_display = ('user', 'name') + + pass + +admin.site.register(Profile, ProfileAdmin) +admin.site.register(ProfileURL, ProfileURLAdmin) diff --git a/profile/apps.py b/profile/apps.py index d9bf26b..89ddf62 100644 --- a/profile/apps.py +++ b/profile/apps.py @@ -1,4 +1,6 @@ +from __future__ import unicode_literals from django.apps import AppConfig class ProfileConfig(AppConfig): name = 'profile' + verbose_name = 'Profile Management' diff --git a/profile/migrations/0001_initial.py b/profile/migrations/0001_initial.py index 18c5a08..76462ed 100644 --- a/profile/migrations/0001_initial.py +++ b/profile/migrations/0001_initial.py @@ -1,4 +1,6 @@ -# Generated by Django 2.1.2 on 2018-10-06 22:16 +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2018-10-02 05:14 +from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models @@ -30,17 +32,8 @@ class Migration(migrations.Migration): ('blog', models.CharField(blank=True, max_length=255)), ('public_repos', models.CharField(blank=True, max_length=255)), ('hireable', models.BooleanField(default=False)), - ('name', models.CharField(blank=True, max_length=255)), - ('avatar_url', models.TextField(null=True)), - ('hero_image_url', models.TextField(null=True)), - ('private', models.BooleanField(default=False)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.DO_NOTHING, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='user')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='user', to=settings.AUTH_USER_MODEL)), ], - options={ - 'verbose_name': 'Profile', - 'verbose_name_plural': 'Profiles', - 'ordering': ('user',), - }, ), migrations.CreateModel( name='ProfileURL', @@ -50,12 +43,7 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(auto_now=True)), ('name', models.CharField(max_length=250)), ('description', models.TextField(blank=True)), - ('url', models.TextField(null=True)), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='urls', to='profile.Profile')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), ], - options={ - 'verbose_name': 'Profile URLs', - 'verbose_name_plural': 'Profile URLs', - }, ), ] diff --git a/profile/migrations/0002_create_cc_profile.py b/profile/migrations/0002_codecorgi_profile.py similarity index 75% rename from profile/migrations/0002_create_cc_profile.py rename to profile/migrations/0002_codecorgi_profile.py index 6c5c198..0c59507 100644 --- a/profile/migrations/0002_create_cc_profile.py +++ b/profile/migrations/0002_codecorgi_profile.py @@ -2,7 +2,7 @@ from api.utils import id_generator def create_codecorgi_profile(apps, schema_editor): - User = apps.get_model('accounts', 'User') + User = apps.get_model('usermanagement', 'User') Profile = apps.get_model('profile', 'Profile') corgiUser = User.objects.create( @@ -10,7 +10,10 @@ def create_codecorgi_profile(apps, schema_editor): email='woof@codecorgi.co', is_verified=True, is_admin=True, + name='corginson', username='codecorgi', + avatar='https://raw.githubusercontent.com/corgicode/frontend-react/dev/src/assets/images/logo-square-hover.png', + heroImage='https://raw.githubusercontent.com/corgicode/frontend-react/dev/src/assets/images/hero-image.jpg', ) Profile.objects.create( @@ -22,16 +25,13 @@ def create_codecorgi_profile(apps, schema_editor): github_url = 'https://github.com/corgicode', company = 'codecorgi', blog = 'https://medium.com/@codecorgi', - public_repos = 'https://github.com/code-corgi', + public_repos = '', hireable = False, user = corgiUser, - name = 'Corginson', - avatar_url = 'https://raw.githubusercontent.com/corgicode/frontend-react/dev/src/assets/images/logo-square-hover.png', - hero_image_url = 'https://raw.githubusercontent.com/corgicode/frontend-react/dev/src/assets/images/hero-image.jpg', ) def delete_codecorgi_profile(apps, schema_editor): - User = apps.get_model('accounts', 'User') + User = apps.get_model('usermanagement', 'User') Profile = apps.get_model('profile', 'Profile') corgiUser = User.objects.filter(email='woof@codecorgi.co').first() diff --git a/profile/models.py b/profile/models.py index e927771..ad2b845 100644 --- a/profile/models.py +++ b/profile/models.py @@ -1,15 +1,28 @@ +from __future__ import unicode_literals from django.db import models -from accounts.models import User -from api import settings +from django.core.exceptions import ValidationError +from django.utils import timezone as tz +from django.apps import apps +import pytz +from django.conf import settings +from safedelete.models import SafeDeleteModel, SOFT_DELETE +from usermanagement.models import User + +class ProfileURL(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,) + description = models.TextField(blank=True) + user = models.ForeignKey(User, on_delete=models.DO_NOTHING) class Profile(models.Model): + def __str__(self): + return self.user.username - user = models.OneToOneField( - settings.AUTH_USER_MODEL, - related_name="profile", - verbose_name="user", - on_delete=models.DO_NOTHING - ) + user = models.ForeignKey(User, related_name='user', on_delete=models.DO_NOTHING) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) tagline = models.CharField(max_length=255, blank=True) @@ -22,30 +35,3 @@ class Profile(models.Model): blog = models.CharField(max_length=255, blank=True) public_repos = models.CharField(max_length=255, blank=True) hireable = models.BooleanField(default=False) - name = models.CharField(max_length=255, blank=True) - avatar_url = models.TextField(null=True) - hero_image_url = models.TextField(null=True) - private = models.BooleanField(default=False) - - class Meta: - verbose_name = "Profile" - verbose_name_plural = "Profiles" - ordering = ("user",) - - def __str__(self): - return self.user.username - -class ProfileURL(models.Model): - def __str__(self): - return self.name - - class Meta: - verbose_name = "Profile URLs" - verbose_name_plural = "Profile URLs" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - name = models.CharField(max_length=250,) - description = models.TextField(blank=True) - url = models.TextField(null=True,) - profile = models.ForeignKey(Profile, on_delete=models.DO_NOTHING, related_name='urls') \ No newline at end of file diff --git a/profile/serializers.py b/profile/serializers.py deleted file mode 100644 index a13fc15..0000000 --- a/profile/serializers.py +++ /dev/null @@ -1,34 +0,0 @@ -from rest_framework_json_api import serializers -from .models import Profile, ProfileURL -from accounts.serializers import UserSerializer -from accounts.models import User -from rest_framework_json_api.relations import ResourceRelatedField - -class ProfileURLSerializer(serializers.ModelSerializer): - class Meta: - model = ProfileURL - fields = ('name', 'description', 'url',) - -class ProfileSerializer(serializers.ModelSerializer): - class Meta: - model = Profile - fields = ('user', 'urls', 'bio', 'tagline', 'location', 'linkedin_url', 'hero_image_url', - 'name', 'avatar_url', 'twitter_url', 'github_url', 'company', 'blog', 'public_repos', 'hireable') - - included_serializers = { - 'user': UserSerializer, - 'urls': ProfileURLSerializer, - } - - user = ResourceRelatedField( - queryset=User.objects - ) - - urls = ResourceRelatedField( - queryset=ProfileURL.objects, - many=True, - ) - - class JSONAPIMeta: - included_resources = ['user', 'urls'] - diff --git a/profile/tests/__init__.py b/profile/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/profile/tests/test_requests/__init__.py b/profile/tests/test_requests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/profile/tests/test_requests/test_profile.py b/profile/tests/test_requests/test_profile.py deleted file mode 100644 index 5c0f12f..0000000 --- a/profile/tests/test_requests/test_profile.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.test import TestCase -from rest_framework.test import APIClient, APITestCase -from profile.models import Profile, ProfileURL -import sure -import json - -class ProfileApiTests(APITestCase): - def setUp(self): - self.client = APIClient() - self.content_type = 'application/vnd.api+json' - - def test_profile_create_with_user(self): - """ - Ensure we get the correct profile - """ - - profile = Profile.objects.filter().last() - - response = self.client.get("/services/api/profiles", - 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['bio'].should.equal(profile.bio) - relationships['user']['data']['id'].should.equal(str(profile.user.id)) - - def test_profile_create_with_urls(self): - """ - Ensure we get the correct profile and project urls - """ - - profile = Profile.objects.filter().last() - - url1 = ProfileURL.objects.create(profile=profile, name='codecorgi', url='https://codecorgi.co') - url2 = ProfileURL.objects.create(profile=profile, name='codecorgi gh', url=None) - - response = self.client.get("/services/api/profiles", - 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['bio'].should.equal(profile.bio) - - relationships['urls']['meta']['count'].should.equal(2) - relationships['urls']['data'][0]['id'].should.equal(str(url2.id)) - relationships['urls']['data'][1]['id'].should.equal(str(url1.id)) diff --git a/profile/urls.py b/profile/urls.py deleted file mode 100644 index 49d4774..0000000 --- a/profile/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.conf.urls import url, include -from . import views -from rest_framework import routers - -router = routers.DefaultRouter(trailing_slash=False) -router.register(r'profiles', views.ProfileViewSet, base_name='Profile') - -urlpatterns = [ - url(r'^', include(router.urls)), -] diff --git a/profile/views.py b/profile/views.py deleted file mode 100644 index d00adce..0000000 --- a/profile/views.py +++ /dev/null @@ -1,20 +0,0 @@ -from rest_framework import viewsets -from .models import Profile, ProfileURL -from .serializers import ProfileSerializer, ProfileURLSerializer - -class ProfileViewSet(viewsets.ModelViewSet): - queryset = Profile.objects.all() - serializer_class = ProfileSerializer - -class ProfileURLsViewSet(viewsets.ModelViewSet): - queryset = ProfileURL.objects - serializer_class = ProfileURLSerializer - - def get_queryset(self): - queryset = super(ProfileURL, self).get_queryset() - - if 'profile_pk' in self.kwargs: - order_pk = self.kwargs['profile_pk'] - queryset = queryset.filter(profile__pk=order_pk) - - return queryset diff --git a/requirements.txt b/requirements.txt index 7957123..1cd488c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,10 @@ -Django==2.1.2 -psycopg2-binary==2.7.5 -djangorestframework==3.8.2 -djangorestframework-jsonapi==2.6.0 -sure==1.4.11 \ No newline at end of file +Django==1.11.3 +psycopg2==2.7.1 +Django==1.11.3 +djangorestframework==3.6.2 +djangorestframework-jsonapi==2.2.0 +django-safedelete==0.4.5 +django-adminplus==0.5 +Markdown==2.6.8 +pyjwt==1.5.2 +django-model-utils==3.0.0 diff --git a/accounts/migrations/__init__.py b/usermanagement/__init__.py similarity index 100% rename from accounts/migrations/__init__.py rename to usermanagement/__init__.py diff --git a/usermanagement/admin.py b/usermanagement/admin.py new file mode 100644 index 0000000..f76ee48 --- /dev/null +++ b/usermanagement/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from .models import User + +class UserAdmin(admin.ModelAdmin): + list_display = ('email',) + + pass + +admin.site.register(User, UserAdmin) diff --git a/usermanagement/apps.py b/usermanagement/apps.py new file mode 100644 index 0000000..ba77233 --- /dev/null +++ b/usermanagement/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +from django.apps import AppConfig + + +class UsermanagementConfig(AppConfig): + name = 'usermanagement' + verbose_name = 'User Management' diff --git a/accounts/migrations/0001_initial.py b/usermanagement/migrations/0001_initial.py similarity index 64% rename from accounts/migrations/0001_initial.py rename to usermanagement/migrations/0001_initial.py index 9149725..f92e5bb 100644 --- a/accounts/migrations/0001_initial.py +++ b/usermanagement/migrations/0001_initial.py @@ -1,8 +1,10 @@ -# Generated by Django 2.1.2 on 2018-10-06 15:20 +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2018-10-02 05:14 +from __future__ import unicode_literals -import accounts.models import django.contrib.auth.password_validation from django.db import migrations, models +import usermanagement.models class Migration(migrations.Migration): @@ -20,6 +22,7 @@ class Migration(migrations.Migration): ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted', models.DateTimeField(editable=False, null=True)), ('password', models.CharField(max_length=128, validators=[django.contrib.auth.password_validation.validate_password], verbose_name='password')), ('email', models.EmailField(max_length=255, unique=True, verbose_name='email address')), ('username', models.CharField(max_length=100, unique=True)), @@ -27,12 +30,18 @@ class Migration(migrations.Migration): ('is_verified', models.BooleanField(default=False)), ('is_admin', models.BooleanField(default=False)), ('flagged', models.BooleanField(default=False)), + ('private', models.BooleanField(default=False)), + ('name', models.CharField(blank=True, max_length=50, null=True)), + ('avatar', models.TextField(blank=True, null=True)), + ('heroImage', models.TextField(blank=True, null=True)), + ('github_id', models.TextField(blank=True, max_length=255)), ], options={ - 'abstract': False, + 'verbose_name': 'user', + 'verbose_name_plural': 'users', }, managers=[ - ('objects', accounts.models.UserManager()), + ('objects', usermanagement.models.UserManager()), ], ), ] diff --git a/accounts/tests/__init__.py b/usermanagement/migrations/__init__.py similarity index 100% rename from accounts/tests/__init__.py rename to usermanagement/migrations/__init__.py diff --git a/usermanagement/models.py b/usermanagement/models.py new file mode 100644 index 0000000..43e7917 --- /dev/null +++ b/usermanagement/models.py @@ -0,0 +1,135 @@ +from __future__ import unicode_literals +from django.db import models +from django.contrib.auth.models import BaseUserManager, AbstractBaseUser +from django.core.exceptions import ValidationError +from django.contrib.auth import password_validation +from django.utils import timezone as tz +from django.apps import apps +from django.contrib.auth.tokens import default_token_generator +from django.conf import settings +from safedelete.models import SafeDeleteModel, SOFT_DELETE +from safedelete.managers import SafeDeleteManager +from model_utils import FieldTracker +import jwt + +class JWT: + + @staticmethod + def encode(payload): + return jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256') + + @staticmethod + def decode(encoded): + return jwt.decode(encoded, settings.SECRET_KEY) + +class UserManager(SafeDeleteManager, BaseUserManager): + use_in_migrations = True + + def create_user(self, email, password=None, unusable_password=False, **kwargs): + user = self.model(email=email, password=password, **kwargs) + + user.save(using=self._db, unusable_password=unusable_password) + return user + + def create_superuser(self, email, username, password): + user = self.create_user(email, password=password, username=username) + user.is_admin = True + user.save(using=self._db) + return user + + def get_by_natural_key(self, username): + case_insensitive_username_field = '{}__iexact'.format(self.model.USERNAME_FIELD) + return self.get(**{case_insensitive_username_field: username}) + +class User(AbstractBaseUser, SafeDeleteModel): + + _safedelete_policy = SOFT_DELETE + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username'] + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + password = models.CharField( + max_length=128, + verbose_name='password', + validators=[password_validation.validate_password] + ) + email = models.EmailField( + verbose_name='email address', + max_length=255, + unique=True, + ) + username = models.CharField(max_length=100, unique=True) + is_active = models.BooleanField(default=True) + is_verified = models.BooleanField(default=False) + is_admin = models.BooleanField(default=False) + flagged = models.BooleanField(default=False) + private = models.BooleanField(default=False) + name = models.CharField(max_length=50, null=True, blank=True) + avatar = models.TextField(blank=True, null=True) + heroImage = models.TextField(blank=True, null=True) + github_id = models.TextField(max_length=255, blank=True) + + tracker = FieldTracker() + + objects = UserManager() + + + class Meta: + verbose_name = 'user' + verbose_name_plural = 'users' + + + def __str__(self): + return self.username + + def save(self, *args, **kwargs): + + if self.tracker.has_changed('email'): + self.email = self.email.lower() + + if kwargs.get("unusable_password"): + self.set_unusable_password() + + self.full_clean(exclude=["id"]) + + if self.tracker.has_changed('password') and not kwargs.get("unusable_password"): + self.set_password(self.password) + + kwargs.pop("unusable_password", None) + + self.full_clean() + super(User, self).save(*args, **kwargs) + + def generate_profile_edit_token(self): + return JWT.encode({'user_id': self.id, + 'token': default_token_generator.make_token(self)}) + + def get_full_name(self): + return self.name + + def get_short_name(self): + return self.name + + def has_perm(self, perm, obj=None): + return True + + def has_module_perms(self, app_label): + "Does the user have permissions to view the app `app_label`?" + return True + + def profile_url(self): + return f'{settings.FRONTEND_DOMAIN}/profile/{ self.username }' + + @property + def is_staff(self): + return self.is_admin + + @property + def is_superuser(self): + return self.is_admin + + @property + def old_password(self): + pass diff --git a/accounts/urls.py b/usermanagement/urls.py similarity index 56% rename from accounts/urls.py rename to usermanagement/urls.py index 4b3b390..2f4f101 100644 --- a/accounts/urls.py +++ b/usermanagement/urls.py @@ -1,10 +1,9 @@ from django.conf.urls import url, include -from accounts import views from rest_framework import routers - router = routers.DefaultRouter(trailing_slash=False) -router.register(r'users', views.UserViewSet, base_name='User') +# router.register(r'users', views.UserViewSet, base_name='User') +# router.register(r'currentuser', views.CurrentUserViewSet, base_name='User') urlpatterns = [ url(r'^', include(router.urls)), diff --git a/usermanagement/views.py b/usermanagement/views.py new file mode 100644 index 0000000..3711970 --- /dev/null +++ b/usermanagement/views.py @@ -0,0 +1,29 @@ +from rest_framework import viewsets, mixins, status +from rest_framework.views import APIView +from rest_framework.generics import GenericAPIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated, AllowAny +from usermanagement.models import User +from usermanagement.serializers import UserPracticeSerializer, SalutationSerializer, PasswordResetSerializer, TokenSetPasswordSerializer, TokenizedProfileSerializer, UserPatientSerializer, ProviderProfileSerializer +from usermanagement.permissions import IsCreationOrIsAuthenticatedOrHasValidToken +from django.contrib.auth.tokens import default_token_generator +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.encoding import force_bytes +from django.core.exceptions import ValidationError +from django.shortcuts import redirect +from django.conf import settings +from datetime import datetime + +class CurrentUserViewSet(viewsets.ReadOnlyModelViewSet): + permission_classes = [IsAuthenticated,] + + def get_queryset(self): + user = self.request.user + return User.objects.filter(pk=user.id) + + def get_serializer_class(self): + if self.request.user.is_patient: + return UserPatientSerializer + else: + return UserPracticeSerializer +