From 8c48bc7d464934a05a9e806fd5ecd3bb16857f46 Mon Sep 17 00:00:00 2001 From: Marta_BM Date: Thu, 5 Sep 2019 12:13:17 +0200 Subject: [PATCH 1/4] camelCase and reset script. Pending flake, E501 --- .env.default | 1 + .flake8 | 6 + .pre-commit-config.yaml | 18 +++ backend/celery.py | 8 +- backend/manage.py | 5 +- backend/settings.py | 169 +++++++++++++---------- backend/urls/main.py | 21 +-- backend/users/admin.py | 48 ++++--- backend/users/apps.py | 2 +- backend/users/managers.py | 23 ++- backend/users/migrations/0001_initial.py | 144 +++++++++++++++---- backend/users/models.py | 59 +++----- backend/users/rest_urls/auth.py | 27 ++-- backend/users/rest_urls/users.py | 7 +- backend/users/rest_views.py | 35 +++-- backend/users/serializers.py | 20 ++- backend/wsgi.py | 2 +- django_vagrant.toml | 22 +++ requirements/common.txt | 3 + requirements/dev.txt | 2 + scripts/reset.sh | 9 ++ 21 files changed, 404 insertions(+), 227 deletions(-) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 django_vagrant.toml create mode 100755 scripts/reset.sh diff --git a/.env.default b/.env.default index 7b79825..5cd2279 100644 --- a/.env.default +++ b/.env.default @@ -9,3 +9,4 @@ DATABASE_USER=django DATABASE_PASSWORD=django DATABASE_HOST=localhost DATABASE_PORT=5432 +CORS_ORIGIN_WHITELIST= diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..55c9f88 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +exclude = build,.git,.tox,./django/conf/app_template/*,./tests/.env, backend/users/migrations/ +ignore = W504, E501 +max-line-length = 79 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..26bdb4f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/ambv/black + rev: 19.3b0 + hooks: + - id: black + language_version: python3.7 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.1.0 + hooks: + - id: flake8 + language_version: python3.7 diff --git a/backend/celery.py b/backend/celery.py index 950e250..012537d 100644 --- a/backend/celery.py +++ b/backend/celery.py @@ -7,12 +7,12 @@ from celery import Celery -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') -app = Celery('django-vagrant') -app.config_from_object('django.conf:settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") +app = Celery("django-vagrant") +app.config_from_object("django.conf:settings") app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) @app.task(bind=True) def debug_task(self): - print('Request: {0!r}'.format(self.request)) + print("Request: {0!r}".format(self.request)) diff --git a/backend/manage.py b/backend/manage.py index b1b48c0..99b49de 100644 --- a/backend/manage.py +++ b/backend/manage.py @@ -3,8 +3,9 @@ import os import sys + def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -16,5 +17,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/backend/settings.py b/backend/settings.py index 7e4e1b2..7f33e40 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -11,7 +11,8 @@ """ import os -from os.path import join, dirname +from os.path import join +from corsheaders.defaults import default_methods from dotenv import load_dotenv from datetime import timedelta @@ -20,80 +21,101 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -dotenv_path = join(BASE_DIR, '.env') +dotenv_path = join(BASE_DIR, ".env") load_dotenv(dotenv_path) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') +SECRET_KEY = os.getenv("DJANGO_SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv('ENV', 'dev') == 'dev' +DEBUG = os.getenv("ENV", "dev") == "dev" -ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(':') if os.getenv('ALLOWED_HOSTS') else None +ALLOWED_HOSTS = ( + os.getenv("ALLOWED_HOSTS").split(":") if os.getenv("ALLOWED_HOSTS") else None +) -INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(':') if os.getenv('INTERNAL_IPS') else None +INTERNAL_IPS = ( + os.getenv("INTERNAL_IPS").split(":") if os.getenv("INTERNAL_IPS") else None +) # Application definition INSTALLED_APPS = [ - 'django_su', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'drf_yasg', - 'users.apps.UsersConfig', - 'rest_framework', - 'rest_framework.authtoken', + "django_su", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "drf_yasg", + "users.apps.UsersConfig", + "rest_framework", + "rest_framework.authtoken", + "corsheaders", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'urls.main' +# CORS HEADERS +CORS_ALLOW_METHODS = default_methods +# default_headers is also possible +CORS_ALLOW_HEADERS = ("Authorization",) +if DEBUG: + CORS_ORIGIN_ALLOW_ALL = True + CORS_ALLOW_CREDENTIALS = True + +if not DEBUG: + CORS_ORIGIN_WHITELIST = ( + os.getenv("CORS_ORIGIN_WHITELIST").split("-:-") + if os.getenv("CORS_ORIGIN_WHITELIST") + else None + ) + +ROOT_URLCONF = "urls.main" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] }, - }, + } ] -WSGI_APPLICATION = 'wsgi.application' +WSGI_APPLICATION = "wsgi.application" # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases DATABASES = { - 'default': { - 'NAME': os.getenv('DATABASE_NAME'), - 'USER': os.getenv('DATABASE_USER'), - 'ENGINE': 'django.db.backends.postgresql', - 'PASSWORD': os.getenv('DATABASE_PASSWORD'), - 'HOST': os.getenv('DATABASE_HOST'), - 'PORT': os.getenv('DATABASE_PORT'), + "default": { + "NAME": os.getenv("DATABASE_NAME"), + "USER": os.getenv("DATABASE_USER"), + "ENGINE": "django.db.backends.postgresql", + "PASSWORD": os.getenv("DATABASE_PASSWORD"), + "HOST": os.getenv("DATABASE_HOST"), + "PORT": os.getenv("DATABASE_PORT"), } } @@ -102,47 +124,50 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Auth User Model -AUTH_USER_MODEL = 'users.User' +AUTH_USER_MODEL = "users.User" AUTHENTICATION_BACKENDS = ( - u'django.contrib.auth.backends.ModelBackend', - u'django_su.backends.SuBackend', + u"django.contrib.auth.backends.ModelBackend", + u"django_su.backends.SuBackend", ) REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'rest_framework_simplejwt.authentication.JWTAuthentication', + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework_simplejwt.authentication.JWTAuthentication" ], - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', - 'PAGE_SIZE': 100 + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 100, + "DEFAULT_RENDERER_CLASSES": ( + "djangorestframework_camel_case.render.CamelCaseJSONRenderer", + ), + "DEFAULT_PARSER_CLASSES": ( + "djangorestframework_camel_case.parser.CamelCaseFormParser", + "djangorestframework_camel_case.parser.CamelCaseMultiPartParser", + "djangorestframework_camel_case.parser.CamelCaseJSONParser", + ), + "JSON_UNDERSCOREIZE": {"no_underscore_before_number": True}, } SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(hours=1), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), + "ACCESS_TOKEN_LIFETIME": timedelta(hours=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=7), } # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -159,16 +184,14 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ -STATIC_URL = '/static/' -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, '../static'), -] -STATIC_ROOT = os.path.join(BASE_DIR, '../static_root') +STATIC_URL = "/static/" +STATICFILES_DIRS = [os.path.join(BASE_DIR, "../static")] +STATIC_ROOT = os.path.join(BASE_DIR, "../static_root") -MEDIA_URL = '/media/' -MEDIA_ROOT = os.path.join(BASE_DIR, '../media') +MEDIA_URL = "/media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "../media") # Celery -CELERY_TIMEZONE = 'US/Eastern' +CELERY_TIMEZONE = "US/Eastern" CELERYD_TASK_TIME_LIMIT = 700 CELERYBEAT_SCHEDULE = {} diff --git a/backend/urls/main.py b/backend/urls/main.py index 1dbf7b6..9d59f4a 100644 --- a/backend/urls/main.py +++ b/backend/urls/main.py @@ -23,7 +23,7 @@ schema_view = get_schema_view( openapi.Info( title="Django API", - default_version='v1', + default_version="v1", description="Test description", terms_of_service="https://www.google.com/policies/terms/", contact=openapi.Contact(email="admin@z1.digital"), @@ -35,17 +35,20 @@ required_urlpatterns = [ - path('admin/', admin.site.urls), - path('auth/', include(('users.rest_urls.auth', 'auth'), namespace='auth')), - path('users/', include(('users.rest_urls.users', 'users'), namespace='users')), + path("admin/", admin.site.urls), + path("auth/", include(("users.rest_urls.auth", "auth"), namespace="auth")), + path("users/", include(("users.rest_urls.users", "users"), namespace="users")), ] swagger_urlpatterns = [ - path('', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), - path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), - + path("", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), + re_path( + r"^swagger(?P\.json|\.yaml)$", + schema_view.without_ui(cache_timeout=0), + name="schema-json", + ), + path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), ] -urlpatterns = ([] + required_urlpatterns + swagger_urlpatterns) +urlpatterns = [] + required_urlpatterns + swagger_urlpatterns diff --git a/backend/users/admin.py b/backend/users/admin.py index e48044d..bc70f77 100644 --- a/backend/users/admin.py +++ b/backend/users/admin.py @@ -8,44 +8,50 @@ class UserAdmin(BaseUserAdmin): - list_display = ('email', 'is_active') - list_filter = ('is_staff', 'is_superuser') - search_fields = ('first_name', 'last_name', 'email') - ordering = ('email', ) + list_display = ("email", "is_active") + list_filter = ("is_staff", "is_superuser") + search_fields = ("first_name", "last_name", "email") + ordering = ("email",) fieldsets = ( - (None, {'fields': ( - 'email', 'email_confirmed', 'password')}), - (_('Personal info'), {'fields': ( - 'first_name', 'last_name', 'location')}), - (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', - 'groups', 'user_permissions')}), - (_('Important dates'), {'fields': ('last_login', 'date_joined')}), + (None, {"fields": ("email", "email_confirmed", "password")}), + (_("Personal info"), {"fields": ("first_name", "last_name", "location")}), + ( + _("Permissions"), + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "groups", + "user_permissions", + ) + }, + ), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), ) add_fieldsets = ( - (None, { - 'fields': ('email', 'password1', 'password2'), - }), - (_('Personal info'), {'fields': ('first_name', 'last_name')}), + (None, {"fields": ("email", "password1", "password2")}), + (_("Personal info"), {"fields": ("first_name", "last_name")}), ) - change_actions = ('login_as_user', ) + change_actions = ("login_as_user",) def get_change_actions(self, request, object_id, form_url): if request.users.is_superuser: return super(UserAdmin, self).get_change_actions( - request, object_id, form_url) + request, object_id, form_url + ) return [] def login_as_user(self, request, obj): from django_su.views import login_as_user - request.method = 'POST' + + request.method = "POST" return login_as_user(request, obj.id) login_as_user.label = "Login as user" - login_as_user.attrs = { - 'label_icon': "icon-user icon-alpha75", - } + login_as_user.attrs = {"label_icon": "icon-user icon-alpha75"} admin.site.register(User, UserAdmin) diff --git a/backend/users/apps.py b/backend/users/apps.py index 4ce1fab..3ef1284 100644 --- a/backend/users/apps.py +++ b/backend/users/apps.py @@ -2,4 +2,4 @@ class UsersConfig(AppConfig): - name = 'users' + name = "users" diff --git a/backend/users/managers.py b/backend/users/managers.py index f48617f..82fa589 100644 --- a/backend/users/managers.py +++ b/backend/users/managers.py @@ -3,7 +3,6 @@ class UserManager(DjangoUserManager): - def _create_user(self, email, password, **extra_fields): """ Creates and saves a User with the given email and password. @@ -16,27 +15,27 @@ def _create_user(self, email, password, **extra_fields): def create_user(self, email, password=None, **extra_fields): if not email: - raise ValueError('Users must have an email address') - extra_fields.setdefault('is_superuser', False) + raise ValueError("Users must have an email address") + extra_fields.setdefault("is_superuser", False) return self._create_user(email, password, **extra_fields) def create_user_random_password(self, email, **extra_fields): if not email: - raise ValueError('Users must have an email address') + raise ValueError("Users must have an email address") password = self.make_random_password() - extra_fields.setdefault('is_superuser', False) + extra_fields.setdefault("is_superuser", False) return self._create_user(email, password, **extra_fields) def create_superuser(self, email, password, **extra_fields): - extra_fields.setdefault('is_staff', True) - extra_fields.setdefault('is_superuser', True) + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) - if extra_fields.get('is_staff') is not True: - raise ValueError('Superuser must have is_staff=True.') - if extra_fields.get('is_superuser') is not True: - raise ValueError('Superuser must have is_superuser=True.') + if extra_fields.get("is_staff") is not True: + raise ValueError("Superuser must have is_staff=True.") + if extra_fields.get("is_superuser") is not True: + raise ValueError("Superuser must have is_superuser=True.") return self._create_user(email, password, **extra_fields) def get_by_natural_key(self, username): - return self.get(**{self.model.USERNAME_FIELD + '__iexact': username}) + return self.get(**{self.model.USERNAME_FIELD + "__iexact": username}) diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py index e8b434b..09dc3e1 100644 --- a/backend/users/migrations/0001_initial.py +++ b/backend/users/migrations/0001_initial.py @@ -11,37 +11,125 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ('auth', '0011_update_proxy_permissions'), - ] + dependencies = [("auth", "0011_update_proxy_permissions")] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('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')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('first_name', models.CharField(max_length=30, verbose_name='first name')), - ('last_name', models.CharField(max_length=30, verbose_name='last name')), - ('email', models.EmailField(max_length=254, null=True, unique=True, verbose_name='email address')), - ('email_confirmed', models.BooleanField(default=False, verbose_name='email confirmed')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('location', models.CharField(blank=True, max_length=255, verbose_name='location')), - ('company_name', models.CharField(blank=True, max_length=255, verbose_name='company name')), - ('avatar', models.ImageField(blank=True, max_length=4000, null=True, upload_to=users.models.upload_to_avatars, verbose_name='avatar')), - ('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={ - 'verbose_name': 'User', - 'verbose_name_plural': 'Users', - }, - managers=[ - ('objects', users.managers.UserManager()), + ("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", + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "first_name", + models.CharField(max_length=30, verbose_name="first name"), + ), + ( + "last_name", + models.CharField(max_length=30, verbose_name="last name"), + ), + ( + "email", + models.EmailField( + max_length=254, + null=True, + unique=True, + verbose_name="email address", + ), + ), + ( + "email_confirmed", + models.BooleanField(default=False, verbose_name="email confirmed"), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "location", + models.CharField( + blank=True, max_length=255, verbose_name="location" + ), + ), + ( + "company_name", + models.CharField( + blank=True, max_length=255, verbose_name="company name" + ), + ), + ( + "avatar", + models.ImageField( + blank=True, + max_length=4000, + null=True, + upload_to=users.models.upload_to_avatars, + verbose_name="avatar", + ), + ), + ( + "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={"verbose_name": "User", "verbose_name_plural": "Users"}, + managers=[("objects", users.managers.UserManager())], + ) ] diff --git a/backend/users/models.py b/backend/users/models.py index 5a05b34..ded5356 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -11,78 +11,61 @@ def get_slug_for_session(instance): - return instance.get_full_name() or 'Anonymous' + return instance.get_full_name() or "Anonymous" def upload_to_avatars(instance, filename): - return os.path.join('avatars', str(instance.id), filename) + return os.path.join("avatars", str(instance.id), filename) class User(AbstractBaseUser, PermissionsMixin): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - first_name = models.CharField( - verbose_name=_('first name'), - max_length=30, - ) + first_name = models.CharField(verbose_name=_("first name"), max_length=30) - last_name = models.CharField( - verbose_name=_('last name'), - max_length=30, - ) + last_name = models.CharField(verbose_name=_("last name"), max_length=30) - email = models.EmailField( - verbose_name=_('email address'), - null=True, - unique=True - ) + email = models.EmailField(verbose_name=_("email address"), null=True, unique=True) email_confirmed = models.BooleanField( - verbose_name=_('email confirmed'), - default=False, + verbose_name=_("email confirmed"), default=False ) is_staff = models.BooleanField( - verbose_name=_('staff status'), + verbose_name=_("staff status"), default=True, - help_text=_('Designates whether the user ' - 'can log into this admin site.') + help_text=_("Designates whether the user " "can log into this admin site."), ) is_active = models.BooleanField( - verbose_name=_('active'), + verbose_name=_("active"), default=True, - help_text=_('Designates whether this user should be treated as ' - 'active. Unselect this instead of deleting accounts.') + help_text=_( + "Designates whether this user should be treated as " + "active. Unselect this instead of deleting accounts." + ), ) date_joined = models.DateTimeField( - verbose_name=_('date joined'), - default=timezone.now + verbose_name=_("date joined"), default=timezone.now ) - location = models.CharField( - verbose_name=_('location'), - max_length=255, - blank=True - ) + location = models.CharField(verbose_name=_("location"), max_length=255, blank=True) company_name = models.CharField( - verbose_name=_('company name'), - max_length=255, - blank=True + verbose_name=_("company name"), max_length=255, blank=True ) avatar = models.ImageField( - verbose_name=_(u'avatar'), + verbose_name=_(u"avatar"), upload_to=upload_to_avatars, max_length=4000, blank=True, null=True, ) - USERNAME_FIELD = 'email' + USERNAME_FIELD = "email" REQUIRED_FIELDS = [] objects = UserManager() @@ -91,7 +74,7 @@ def get_full_name(self): """ Returns the first_name plus the last_name, with a space in between. """ - full_name = '%s %s' % (self.first_name, self.last_name) + full_name = "%s %s" % (self.first_name, self.last_name) return full_name.strip() def get_short_name(self): @@ -99,5 +82,5 @@ def get_short_name(self): return self.first_name class Meta: - verbose_name = _(u'User') - verbose_name_plural = _(u'Users') + verbose_name = _(u"User") + verbose_name_plural = _(u"Users") diff --git a/backend/users/rest_urls/auth.py b/backend/users/rest_urls/auth.py index 8785d68..9f48ac8 100644 --- a/backend/users/rest_urls/auth.py +++ b/backend/users/rest_urls/auth.py @@ -1,20 +1,23 @@ from django.urls import path -from rest_framework_simplejwt.views import (TokenRefreshView, TokenVerifyView) +from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView -from users.rest_views import (CustomTokenObtainPairView, - ChangePasswordRestView, ResetPasswordRestView, - ConfirmResetPasswordView) +from users.rest_views import ( + CustomTokenObtainPairView, + ChangePasswordRestView, + ResetPasswordRestView, + ConfirmResetPasswordView, +) urlpatterns = [ - path("login/", CustomTokenObtainPairView.as_view(), - name="token_obtain_pair"), - path("password/", ChangePasswordRestView.as_view(), - name="change_password"), - path("reset-password/", ResetPasswordRestView.as_view(), - name="reset_password"), - path("confirm-password//", ConfirmResetPasswordView.as_view(), - name="confirm_password"), + path("login/", CustomTokenObtainPairView.as_view(), name="token_obtain_pair"), + path("password/", ChangePasswordRestView.as_view(), name="change_password"), + path("reset-password/", ResetPasswordRestView.as_view(), name="reset_password"), + path( + "confirm-password//", + ConfirmResetPasswordView.as_view(), + name="confirm_password", + ), path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), path("token/verify/", TokenVerifyView.as_view(), name="token_verify"), ] diff --git a/backend/users/rest_urls/users.py b/backend/users/rest_urls/users.py index 56735d4..c17cc58 100644 --- a/backend/users/rest_urls/users.py +++ b/backend/users/rest_urls/users.py @@ -1,7 +1,10 @@ from django.urls import path -from users.rest_views import (UserListRestView, UserMeRetrieveRestView, - UserRetrieveUpdateRestView) +from users.rest_views import ( + UserListRestView, + UserMeRetrieveRestView, + UserRetrieveUpdateRestView, +) urlpatterns = [ path("", UserListRestView.as_view(), name="list_users"), diff --git a/backend/users/rest_views.py b/backend/users/rest_views.py index c56d1f5..6baced7 100644 --- a/backend/users/rest_views.py +++ b/backend/users/rest_views.py @@ -12,13 +12,18 @@ from rest_framework_simplejwt.views import TokenObtainPairView from users.models import User -from users.serializers import (UserProfileSerializer, ChangePasswordSerializer, - CustomTokenObtainPairView, EmailField, - ResetPasswordSerializer) +from users.serializers import ( + UserProfileSerializer, + ChangePasswordSerializer, + CustomTokenObtainPairView, + EmailField, + ResetPasswordSerializer, +) class UserListRestView(api_views.ListCreateAPIView): """Return a list of Users""" + permission_classes = (permissions.IsAuthenticated,) queryset = User.objects.all() serializer_class = UserProfileSerializer @@ -26,13 +31,15 @@ class UserListRestView(api_views.ListCreateAPIView): class UserRetrieveUpdateRestView(api_views.RetrieveUpdateAPIView): """Return an specific user""" - permission_classes = (permissions.IsAuthenticated, ) + + permission_classes = (permissions.IsAuthenticated,) queryset = User.objects.all() serializer_class = UserProfileSerializer class UserMeRetrieveRestView(api_views.RetrieveUpdateAPIView): """Return logged user""" + permission_classes = (permissions.IsAuthenticated,) serializer_class = UserProfileSerializer @@ -42,6 +49,7 @@ def get_object(self): class ChangePasswordRestView(api_views.UpdateAPIView): """Change logged user's password""" + permission_classes = (permissions.IsAuthenticated,) serializer_class = ChangePasswordSerializer model = User @@ -56,7 +64,10 @@ def update(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): if not self.object.check_password(serializer.data.get("old_password")): - return Response({"old_password": ["Wrong password."]}, status=status.HTTP_400_BAD_REQUEST) + return Response( + {"old_password": ["Wrong password."]}, + status=status.HTTP_400_BAD_REQUEST, + ) self.object.set_password(serializer.data.get("new_password")) self.object.save() return Response("Success.", status=status.HTTP_200_OK) @@ -66,6 +77,7 @@ def update(self, request, *args, **kwargs): class ResetPasswordRestView(api_views.GenericAPIView): """Send an email for reseting the password""" + serializer_class = EmailField def post(self, request, *args, **kwargs): @@ -93,11 +105,7 @@ def post(self, request, *args, **kwargs): print(message) try: send_mail( - subject, - message, - "z1@z1.digital", - [email], - fail_silently=False, + subject, message, "z1@z1.digital", [email], fail_silently=False ) except BadHeaderError: return HttpResponse("Invalid header found.") @@ -109,6 +117,7 @@ def post(self, request, *args, **kwargs): class ConfirmResetPasswordView(api_views.UpdateAPIView): """Change the password of the user using the token provided in the url of the email to reset the password""" + serializer_class = ResetPasswordSerializer queryset = User.objects.all() decoded_token = None @@ -127,12 +136,14 @@ def get_object(self, *args, **kwargs): def perform_update(self, serializer): user = self.get_object() serializer.is_valid() - user.set_password(serializer.validated_data['password']) + user.set_password(serializer.validated_data["password"]) user.save() def check_permission(self, request): try: - self.decoded_token = jwt.decode(self.kwargs.get("token"), "secret", algorithms=["HS256"]) + self.decoded_token = jwt.decode( + self.kwargs.get("token"), "secret", algorithms=["HS256"] + ) except jwt.ExpiredSignatureError: raise self.permission_denied(request, "The token has expired") except jwt.InvalidTokenError: diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 00d7a56..bf8f9b1 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -5,24 +5,23 @@ class UserProfileSerializer(serializers.ModelSerializer): - def create(self, validated_data): - user = User( - email=validated_data['email']) - user.set_password(validated_data['password']) + user = User(email=validated_data["email"]) + user.set_password(validated_data["password"]) user.save() return user class Meta: model = User - fields = ('id', 'first_name', 'last_name', 'email', 'location', 'avatar') - extra_kwargs = {'password': {'write_only': True}} + fields = ("id", "first_name", "last_name", "email", "location", "avatar") + extra_kwargs = {"password": {"write_only": True}} class ChangePasswordSerializer(serializers.Serializer): """ Serializer for password change endpoint. """ + old_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True) @@ -37,20 +36,17 @@ class ResetPasswordSerializer(serializers.Serializer): def validate(self, data): if data.get("password") != data.get("repeat_password"): - raise serializers.ValidationError( - {"password": ["Password must match"]} - ) + raise serializers.ValidationError({"password": ["Password must match"]}) return data class CustomTokenObtainPairView(TokenObtainPairSerializer): - def validate(self, attrs): data = super().validate(attrs) token = self.get_token(self.user) - data['user'] = token['user_id'] + data["user"] = token["user_id"] return data class Meta: - fields = ('first_name', 'last_name') + fields = ("first_name", "last_name") diff --git a/backend/wsgi.py b/backend/wsgi.py index a98bc18..4584c2b 100644 --- a/backend/wsgi.py +++ b/backend/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") application = get_wsgi_application() diff --git a/django_vagrant.toml b/django_vagrant.toml new file mode 100644 index 0000000..2c82974 --- /dev/null +++ b/django_vagrant.toml @@ -0,0 +1,22 @@ + +[tool.black] +line-length = 79 +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | \.vagrant + | \.idea + | _build + | buck-out + | build + | dist + | .env + | .env.default + | .bootstrap.sh +)/ +''' diff --git a/requirements/common.txt b/requirements/common.txt index 6de4978..9e3785a 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -5,7 +5,10 @@ django-su==0.8.0 django-filter==2.1.0 djangorestframework==3.9.4 djangorestframework-simplejwt==4.3.0 +djangorestframework-camel-case==1.0.3 +django-cors-headers==3.1.0 python-dotenv==0.10.3 + # posgres psycopg2==2.8.3 psycopg2-binary==2.8.3 diff --git a/requirements/dev.txt b/requirements/dev.txt index 6624a30..7248966 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1 +1,3 @@ -r common.txt +#Pre-commit +pre-commit=1.18.3 diff --git a/scripts/reset.sh b/scripts/reset.sh new file mode 100755 index 0000000..c4957e6 --- /dev/null +++ b/scripts/reset.sh @@ -0,0 +1,9 @@ + +db_name=django_db +db_user=django +db_password=django + +sudo -u postgres dropdb $db_name +sudo -u postgres dropuser $db_user +sudo -u postgres psql -c "CREATE USER $db_user WITH SUPERUSER PASSWORD '$db_password';" +sudo -u postgres createdb $db_name --owner=$db_user From 006d3382c0f1999acf97cb1fa4c326bd8c4b33c7 Mon Sep 17 00:00:00 2001 From: Marta_BM Date: Thu, 5 Sep 2019 12:54:35 +0200 Subject: [PATCH 2/4] Pending E501 --- .flake8 | 10 +++++++-- .pre-commit-config.yaml | 10 ++++----- README.md | 6 +++--- backend/settings.py | 12 ++++++++--- backend/urls/main.py | 17 +++++++++++++--- backend/users/admin.py | 5 ++++- backend/users/migrations/0001_initial.py | 12 ++++++++--- backend/users/models.py | 12 ++++++++--- backend/users/rest_urls/auth.py | 14 ++++++++++--- backend/users/rest_urls/users.py | 4 +++- backend/users/rest_views.py | 26 ++++++++++++++++++------ backend/users/serializers.py | 13 ++++++++++-- backend/users/tests.py | 3 --- backend/users/views.py | 3 --- django_vagrant.toml => pyproject.toml | 1 - requirements/dev.txt | 1 + 16 files changed, 107 insertions(+), 42 deletions(-) rename django_vagrant.toml => pyproject.toml (99%) diff --git a/.flake8 b/.flake8 index 55c9f88..f2170f2 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,12 @@ [flake8] -exclude = build,.git,.tox,./django/conf/app_template/*,./tests/.env, backend/users/migrations/ +exclude = + build, + .git, + .tox, + ./django/conf/app_template/*, + ./tests/.env, + ./backend/users/migrations/* ignore = W504, E501 max-line-length = 79 -max-complexity = 18 +max-complexity = 10 select = B,C,E,F,W,T4,B9 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26bdb4f..f1871a5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,13 +6,13 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files -- repo: https://github.com/ambv/black - rev: 19.3b0 - hooks: - - id: black - language_version: python3.7 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.1.0 hooks: - id: flake8 language_version: python3.7 +- repo: https://github.com/ambv/black + rev: 19.3b0 + hooks: + - id: black + language_version: python3.7 diff --git a/README.md b/README.md index 34d272d..c29a3bf 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,20 @@ * VirtualBox (https://www.virtualbox.org/wiki/Downloads) * Vagrant (https://www.vagrantup.com/downloads.html - + ## Installation $ git clone https://github.com/z1digitalstudio/django-vagrant $ cd django-vagrant $ vagrant up --provision - + ## Development ### Create user $ vagrant ssh $ python manage.py createsuperuser ### Create database tables $ python manage.py migrate - ### Start up + ### Start up $ ./run.sh ## Access from the browser diff --git a/backend/settings.py b/backend/settings.py index 7f33e40..59975df 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -34,7 +34,9 @@ DEBUG = os.getenv("ENV", "dev") == "dev" ALLOWED_HOSTS = ( - os.getenv("ALLOWED_HOSTS").split(":") if os.getenv("ALLOWED_HOSTS") else None + os.getenv("ALLOWED_HOSTS").split(":") + if os.getenv("ALLOWED_HOSTS") + else None ) INTERNAL_IPS = ( @@ -127,8 +129,12 @@ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, - {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, - {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator" + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator" + }, ] diff --git a/backend/urls/main.py b/backend/urls/main.py index 9d59f4a..0b04ed6 100644 --- a/backend/urls/main.py +++ b/backend/urls/main.py @@ -37,18 +37,29 @@ required_urlpatterns = [ path("admin/", admin.site.urls), path("auth/", include(("users.rest_urls.auth", "auth"), namespace="auth")), - path("users/", include(("users.rest_urls.users", "users"), namespace="users")), + path( + "users/", + include(("users.rest_urls.users", "users"), namespace="users"), + ), ] swagger_urlpatterns = [ - path("", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), + path( + "", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), re_path( r"^swagger(?P\.json|\.yaml)$", schema_view.without_ui(cache_timeout=0), name="schema-json", ), - path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), + path( + "redoc/", + schema_view.with_ui("redoc", cache_timeout=0), + name="schema-redoc", + ), ] urlpatterns = [] + required_urlpatterns + swagger_urlpatterns diff --git a/backend/users/admin.py b/backend/users/admin.py index bc70f77..3d22868 100644 --- a/backend/users/admin.py +++ b/backend/users/admin.py @@ -14,7 +14,10 @@ class UserAdmin(BaseUserAdmin): ordering = ("email",) fieldsets = ( (None, {"fields": ("email", "email_confirmed", "password")}), - (_("Personal info"), {"fields": ("first_name", "last_name", "location")}), + ( + _("Personal info"), + {"fields": ("first_name", "last_name", "location")}, + ), ( _("Permissions"), { diff --git a/backend/users/migrations/0001_initial.py b/backend/users/migrations/0001_initial.py index 09dc3e1..227c974 100644 --- a/backend/users/migrations/0001_initial.py +++ b/backend/users/migrations/0001_initial.py @@ -17,7 +17,10 @@ class Migration(migrations.Migration): migrations.CreateModel( name="User", fields=[ - ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "password", + models.CharField(max_length=128, verbose_name="password"), + ), ( "last_login", models.DateTimeField( @@ -60,7 +63,9 @@ class Migration(migrations.Migration): ), ( "email_confirmed", - models.BooleanField(default=False, verbose_name="email confirmed"), + models.BooleanField( + default=False, verbose_name="email confirmed" + ), ), ( "is_staff", @@ -81,7 +86,8 @@ class Migration(migrations.Migration): ( "date_joined", models.DateTimeField( - default=django.utils.timezone.now, verbose_name="date joined" + default=django.utils.timezone.now, + verbose_name="date joined", ), ), ( diff --git a/backend/users/models.py b/backend/users/models.py index ded5356..281612f 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -26,7 +26,9 @@ class User(AbstractBaseUser, PermissionsMixin): last_name = models.CharField(verbose_name=_("last name"), max_length=30) - email = models.EmailField(verbose_name=_("email address"), null=True, unique=True) + email = models.EmailField( + verbose_name=_("email address"), null=True, unique=True + ) email_confirmed = models.BooleanField( verbose_name=_("email confirmed"), default=False @@ -35,7 +37,9 @@ class User(AbstractBaseUser, PermissionsMixin): is_staff = models.BooleanField( verbose_name=_("staff status"), default=True, - help_text=_("Designates whether the user " "can log into this admin site."), + help_text=_( + "Designates whether the user " "can log into this admin site." + ), ) is_active = models.BooleanField( @@ -51,7 +55,9 @@ class User(AbstractBaseUser, PermissionsMixin): verbose_name=_("date joined"), default=timezone.now ) - location = models.CharField(verbose_name=_("location"), max_length=255, blank=True) + location = models.CharField( + verbose_name=_("location"), max_length=255, blank=True + ) company_name = models.CharField( verbose_name=_("company name"), max_length=255, blank=True diff --git a/backend/users/rest_urls/auth.py b/backend/users/rest_urls/auth.py index 9f48ac8..2984544 100644 --- a/backend/users/rest_urls/auth.py +++ b/backend/users/rest_urls/auth.py @@ -10,9 +10,17 @@ urlpatterns = [ - path("login/", CustomTokenObtainPairView.as_view(), name="token_obtain_pair"), - path("password/", ChangePasswordRestView.as_view(), name="change_password"), - path("reset-password/", ResetPasswordRestView.as_view(), name="reset_password"), + path( + "login/", CustomTokenObtainPairView.as_view(), name="token_obtain_pair" + ), + path( + "password/", ChangePasswordRestView.as_view(), name="change_password" + ), + path( + "reset-password/", + ResetPasswordRestView.as_view(), + name="reset_password", + ), path( "confirm-password//", ConfirmResetPasswordView.as_view(), diff --git a/backend/users/rest_urls/users.py b/backend/users/rest_urls/users.py index c17cc58..24277e7 100644 --- a/backend/users/rest_urls/users.py +++ b/backend/users/rest_urls/users.py @@ -8,6 +8,8 @@ urlpatterns = [ path("", UserListRestView.as_view(), name="list_users"), - path("/", UserRetrieveUpdateRestView.as_view(), name="get_profile"), + path( + "/", UserRetrieveUpdateRestView.as_view(), name="get_profile" + ), path("me/", UserMeRetrieveRestView.as_view(), name="me"), ] diff --git a/backend/users/rest_views.py b/backend/users/rest_views.py index 6baced7..f32e1af 100644 --- a/backend/users/rest_views.py +++ b/backend/users/rest_views.py @@ -63,7 +63,9 @@ def update(self, request, *args, **kwargs): self.object = self.get_object() serializer = self.get_serializer(data=request.data) if serializer.is_valid(): - if not self.object.check_password(serializer.data.get("old_password")): + if not self.object.check_password( + serializer.data.get("old_password") + ): return Response( {"old_password": ["Wrong password."]}, status=status.HTTP_400_BAD_REQUEST, @@ -90,7 +92,9 @@ def post(self, request, *args, **kwargs): past = now + datetime.timedelta(hours=10) encoded_token = jwt.encode( - {"user_id": str(user.id), "exp": past}, "secret", algorithm="HS256" + {"user_id": str(user.id), "exp": past}, + "secret", + algorithm="HS256", ) url = "%s/reset-password/%s" % ( @@ -105,14 +109,20 @@ def post(self, request, *args, **kwargs): print(message) try: send_mail( - subject, message, "z1@z1.digital", [email], fail_silently=False + subject, + message, + "z1@z1.digital", + [email], + fail_silently=False, ) except BadHeaderError: return HttpResponse("Invalid header found.") return Response(serializer.data, status=status.HTTP_201_CREATED) except User.DoesNotExist: - return Response("The user does not exist", status=status.HTTP_403_FORBIDDEN) + return Response( + "The user does not exist", status=status.HTTP_403_FORBIDDEN + ) class ConfirmResetPasswordView(api_views.UpdateAPIView): @@ -126,7 +136,9 @@ def _get_user(self): try: user = User.objects.get(id=self.decoded_token["user_id"]) except User.DoesNotExist: - return self.permission_denied(self.request, "The user does not exist") + return self.permission_denied( + self.request, "The user does not exist" + ) return user def get_object(self, *args, **kwargs): @@ -151,7 +163,9 @@ def check_permission(self, request): def update(self, request, *args, **kwargs): self.check_permission(request) - return super(ConfirmResetPasswordView, self).update(request, *args, **kwargs) + return super(ConfirmResetPasswordView, self).update( + request, *args, **kwargs + ) class CustomTokenObtainPairView(TokenObtainPairView): diff --git a/backend/users/serializers.py b/backend/users/serializers.py index bf8f9b1..967e354 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -13,7 +13,14 @@ def create(self, validated_data): class Meta: model = User - fields = ("id", "first_name", "last_name", "email", "location", "avatar") + fields = ( + "id", + "first_name", + "last_name", + "email", + "location", + "avatar", + ) extra_kwargs = {"password": {"write_only": True}} @@ -36,7 +43,9 @@ class ResetPasswordSerializer(serializers.Serializer): def validate(self, data): if data.get("password") != data.get("repeat_password"): - raise serializers.ValidationError({"password": ["Password must match"]}) + raise serializers.ValidationError( + {"password": ["Password must match"]} + ) return data diff --git a/backend/users/tests.py b/backend/users/tests.py index 7ce503c..e69de29 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/backend/users/views.py b/backend/users/views.py index 91ea44a..e69de29 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/django_vagrant.toml b/pyproject.toml similarity index 99% rename from django_vagrant.toml rename to pyproject.toml index 2c82974..bc124cf 100644 --- a/django_vagrant.toml +++ b/pyproject.toml @@ -1,4 +1,3 @@ - [tool.black] line-length = 79 include = '\.pyi?$' diff --git a/requirements/dev.txt b/requirements/dev.txt index 7248966..b5820b6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,3 +1,4 @@ -r common.txt #Pre-commit pre-commit=1.18.3 +black==19.3b0 From 6582d7d2b77218e0f6bc1b700a158913ea8a8ea8 Mon Sep 17 00:00:00 2001 From: Marta_BM Date: Mon, 9 Sep 2019 07:44:41 +0200 Subject: [PATCH 3/4] global config --- .env.default | 8 +++ backend/base/models.py | 44 +++++++++++++ backend/media_upload/__init__.py | 0 backend/media_upload/admin.py | 0 backend/media_upload/apps.py | 5 ++ backend/media_upload/backends/base.py | 29 +++++++++ backend/media_upload/backends/local.py | 70 +++++++++++++++++++++ backend/media_upload/backends/s3.py | 35 +++++++++++ backend/media_upload/migrations/__init__.py | 0 backend/media_upload/models.py | 7 +++ backend/media_upload/rest_views.py | 33 ++++++++++ backend/media_upload/tests.py | 0 backend/media_upload/urls/files.py | 11 ++++ backend/settings.py | 51 +++++++++++++++ backend/storages_backends.py | 19 ++++++ requirements/common.txt | 4 ++ scripts/update.sh | 7 +++ 17 files changed, 323 insertions(+) create mode 100644 backend/base/models.py create mode 100644 backend/media_upload/__init__.py create mode 100644 backend/media_upload/admin.py create mode 100644 backend/media_upload/apps.py create mode 100644 backend/media_upload/backends/base.py create mode 100644 backend/media_upload/backends/local.py create mode 100644 backend/media_upload/backends/s3.py create mode 100644 backend/media_upload/migrations/__init__.py create mode 100644 backend/media_upload/models.py create mode 100644 backend/media_upload/rest_views.py create mode 100644 backend/media_upload/tests.py create mode 100644 backend/media_upload/urls/files.py create mode 100644 backend/storages_backends.py create mode 100644 scripts/update.sh diff --git a/.env.default b/.env.default index 5cd2279..4d85746 100644 --- a/.env.default +++ b/.env.default @@ -4,9 +4,17 @@ REQUIREMENTS_FILE=dev DJANGO_SECRET_KEY='12b06mtny_e^*(*7&3wy14i26jk=71azifld4+ky_wdsu%qx6m' ALLOWED_HOSTS=localhost:127.0.0.1 INTERNAL_IPS=127.0.0.1:10.0.2.2 + DATABASE_NAME=django_db DATABASE_USER=django DATABASE_PASSWORD=django DATABASE_HOST=localhost DATABASE_PORT=5432 + CORS_ORIGIN_WHITELIST= + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_STORAGE_BUCKET_NAME= +AWS_S3_REGION_NAME= +USE_S3=FALSE diff --git a/backend/base/models.py b/backend/base/models.py new file mode 100644 index 0000000..42c038f --- /dev/null +++ b/backend/base/models.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +import uuid + +from django.db import models + +from django.utils.translation import ugettext_lazy as _ + + +class SimpleModel(models.Model): + """ + An abstract base class model that provides: + self-updating 'created' and 'modified' fields. + """ + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + created = models.DateTimeField( + verbose_name=_("created date"), null=True, auto_now_add=True + ) + modified = models.DateTimeField( + verbose_name=_("modified date"), null=True, auto_now=True + ) + + class Meta: + abstract = True + ordering = ("-created",) + + +class NamedModel(SimpleModel): + """ + An abstract base class model that provides 'name' and + autogenerated 'slug' fields. + """ + + name = models.CharField( + verbose_name=_("Name"), max_length=255, null=True, blank=True + ) + + class Meta: + abstract = True + ordering = ("name",) + + def __str__(self): + return self.name diff --git a/backend/media_upload/__init__.py b/backend/media_upload/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/media_upload/admin.py b/backend/media_upload/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/media_upload/apps.py b/backend/media_upload/apps.py new file mode 100644 index 0000000..9e04c07 --- /dev/null +++ b/backend/media_upload/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MediaUploadConfig(AppConfig): + name = "media_upload" diff --git a/backend/media_upload/backends/base.py b/backend/media_upload/backends/base.py new file mode 100644 index 0000000..ca76d82 --- /dev/null +++ b/backend/media_upload/backends/base.py @@ -0,0 +1,29 @@ +import mimetypes + +from django.http import Http404 + + +class BaseMediaUploadBackend(object): + def get_presigned_url(self): + # Subclasses must implement this + raise NotImplementedError + + def process_upload(self, *args, **kwargs): + # By default the upload is done outside our system + raise Http404 + + def __init__(self, request, *args, **kwargs): + self.request = request + self.args = args + self.kwargs = kwargs + + def _get_filename(self): + return self.request.GET.get("filename", "data") + + def _get_content_type(self): + filename = self._get_filename() + content_type = self.request.GET.get( + "contentType", + mimetypes.guess_type(filename)[0] or "application/octet-stream", + ) + return content_type diff --git a/backend/media_upload/backends/local.py b/backend/media_upload/backends/local.py new file mode 100644 index 0000000..2f60917 --- /dev/null +++ b/backend/media_upload/backends/local.py @@ -0,0 +1,70 @@ +import os +import datetime + +from django.conf import settings +from django.core import signing +from django.core.signing import BadSignature +from django.core.files.storage import default_storage + +from media_upload.backends.base import BaseMediaUploadBackend +from media_upload.models import UploadToken + +from rest_framework.reverse import reverse +from rest_framework.response import Response + + +class LocalMediaUploadBackend(BaseMediaUploadBackend): + def get_presigned_url(self): + filename = self._get_filename() + mimetype = self._get_content_type() + + name = default_storage.get_available_name( + self._full_path(filename, self.request.user) + ) + + timestamp = datetime.datetime.now().timestamp() + token = signing.dumps({"date": timestamp, "full_path": name}) + + UploadToken.objects.create(token=token) + result = reverse( + "upload_file", kwargs={"token": token, "filename": filename} + ) + + return { + "uploadUrl": self.request.build_absolute_uri(result), + "contentType": mimetype, + "retrieveUrl": self.request.build_absolute_uri( + default_storage.url(name) + ), + } + + def _full_path(self, filename, user): + if user and user.is_authenticated: + return os.path.join(str(user.id), filename) + return os.path.join("anon", filename) + + def _invalid_token_response(self): + return Response("The token is invalid or has expired", status=401) + + def process_upload(self): + try: + actual_date = datetime.datetime.now().timestamp() + limit_date = actual_date - int(settings.UPLOAD_TOKEN_EXPIRE_TIME) + + token = UploadToken.objects.get(token=self.kwargs["token"]) + token_dict = signing.loads(self.kwargs["token"]) + token_date = token_dict.get("date") + full_path = token_dict.get("full_path") + + is_valid = all([limit_date < token_date]) + + if is_valid: + uploaded_file = self.request.data["file"] + default_storage.save(full_path, uploaded_file, max_length=100) + token.delete() + else: + token.delete() + return self._invalid_token_response() + except (UploadToken.DoesNotExist, BadSignature): + return self._invalid_token_response() + return Response(None, status=200) diff --git a/backend/media_upload/backends/s3.py b/backend/media_upload/backends/s3.py new file mode 100644 index 0000000..8cb20e6 --- /dev/null +++ b/backend/media_upload/backends/s3.py @@ -0,0 +1,35 @@ +import boto3 +from botocore.exceptions import ClientError + +from django.conf import settings +from media_upload.backends.base import BaseMediaUploadBackend + + +class S3MediaUploadBackend(BaseMediaUploadBackend): + def get_presigned_url(self): + s3_client = boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + + filename = self._get_filename() + mimetype = self._get_content_type() + try: + response = s3_client.generate_presigned_url( + "put_object", + Params={ + "Bucket": settings.AWS_BUCKET_NAME, + "Key": filename, + "ContentType": mimetype, + }, + ExpiresIn=int(settings.UPLOAD_TOKEN_EXPIRE_TIME), + HttpMethod="PUT", + ) + except ClientError: + return + return { + "uploadUrl": response, + "contentType": mimetype, + "retrieveUrl": response.split("?")[0], + } diff --git a/backend/media_upload/migrations/__init__.py b/backend/media_upload/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/media_upload/models.py b/backend/media_upload/models.py new file mode 100644 index 0000000..f53cdce --- /dev/null +++ b/backend/media_upload/models.py @@ -0,0 +1,7 @@ +from django.db import models +from base.models import SimpleModel + + +class UploadToken(SimpleModel): + + token = models.CharField(max_length=255, null=True) diff --git a/backend/media_upload/rest_views.py b/backend/media_upload/rest_views.py new file mode 100644 index 0000000..dcfffc9 --- /dev/null +++ b/backend/media_upload/rest_views.py @@ -0,0 +1,33 @@ +from django.conf import settings +from django.utils.module_loading import import_string + +from rest_framework import views, permissions +from rest_framework.parsers import FileUploadParser +from rest_framework.response import Response + + +class MediaUploadBackendMixin(object): + def get_backend(self, request, *args, **kwargs): + backend_class = import_string(settings.MEDIA_UPLOAD_BACKEND) + return backend_class(request, *args, **kwargs) + + +class GetFilesView(MediaUploadBackendMixin, views.APIView): + permission_classes = (permissions.IsAuthenticated,) + permission_classes = (permissions.AllowAny,) + + def get(self, request, *args, **kwargs): + presigned_url_data = self.get_backend( + request, *args, **kwargs + ).get_presigned_url() + if presigned_url_data is None: + return Response({"error": "Invalid data"}, status=400) + return Response(presigned_url_data) + + +class UploadFileView(MediaUploadBackendMixin, views.APIView): + permission_classes = (permissions.AllowAny,) + parser_classes = (FileUploadParser,) + + def put(self, request, *args, **kwargs): + return self.get_backend(request, *args, **kwargs).process_upload() diff --git a/backend/media_upload/tests.py b/backend/media_upload/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/media_upload/urls/files.py b/backend/media_upload/urls/files.py new file mode 100644 index 0000000..d298528 --- /dev/null +++ b/backend/media_upload/urls/files.py @@ -0,0 +1,11 @@ +from django.urls import path +from media_upload.rest_views import GetFilesView, UploadFileView + +urlpatterns = [ + path("signed_url/", GetFilesView.as_view(), name="get_files"), + path( + "upload_file///", + UploadFileView.as_view(), + name="upload_file", + ), +] diff --git a/backend/settings.py b/backend/settings.py index 59975df..fb9f12f 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -32,6 +32,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv("ENV", "dev") == "dev" +USE_S3 = os.getenv("USE_S3") == "TRUE" ALLOWED_HOSTS = ( os.getenv("ALLOWED_HOSTS").split(":") @@ -58,6 +59,8 @@ "rest_framework", "rest_framework.authtoken", "corsheaders", + "storages", + "media_upload", ] MIDDLEWARE = [ @@ -201,3 +204,51 @@ CELERY_TIMEZONE = "US/Eastern" CELERYD_TASK_TIME_LIMIT = 700 CELERYBEAT_SCHEDULE = {} + +# storages +UPLOAD_TOKEN_EXPIRE_TIME = os.getenv("UPLOAD_TOKEN_EXPIRE_TIME") + + +MEDIA_UPLOAD_BACKEND = os.getenv( + "MEDIA_UPLOAD_BACKEND", + "media_upload.backends.local.LocalMediaUploadBackend", +) + +if USE_S3: + AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") + AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") + AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_BUCKET_NAME") + AWS_S3_REGION_NAME = os.getenv("AWS_S3_REGION_NAME") + AWS_S3_CUSTOM_DOMAIN = "%s.s3.amazonaws.com" % AWS_STORAGE_BUCKET_NAME + AWS_DEFAULT_ACL = "public-read" + AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"} + # s3 static settings + AWS_LOCATION = "static" + STATIC_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, AWS_LOCATION) + STATICFILES_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" + + # s3 public media settings + PUBLIC_MEDIA_LOCATION = "media" + MEDIA_URL = "https://%s/%s/" % ( + AWS_S3_CUSTOM_DOMAIN, + PUBLIC_MEDIA_LOCATION, + ) + DEFAULT_FILE_STORAGE = "storage_backends.PublicMediaStorage" + + # s3 private media settings + PRIVATE_MEDIA_LOCATION = "private" + PRIVATE_FILE_STORAGE = "storage_backends.PrivateMediaStorage" + +else: + # Static files (CSS, JavaScript, Images) + # https://docs.djangoproject.com/en/2.2/howto/static-files/ + + STATIC_URL = "/static/" + STATIC_ROOT = os.path.join(BASE_DIR, "static_root") + MEDIA_URL = "/media/" + MEDIA_ROOT = os.path.join(BASE_DIR, "../media") + + +STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] + +FEEDBACK_EXPIRATION_TIME = 9 * 3600 # 9 horas diff --git a/backend/storages_backends.py b/backend/storages_backends.py new file mode 100644 index 0000000..09b45b9 --- /dev/null +++ b/backend/storages_backends.py @@ -0,0 +1,19 @@ +from storages.backends.s3boto3 import S3Boto3Storage + + +class StaticStorage(S3Boto3Storage): + location = "static" + default_acl = "public-read" + + +class PublicMediaStorage(S3Boto3Storage): + location = "media" + default_acl = "public-read" + file_overwrite = False + + +class PrivateMediaStorage(S3Boto3Storage): + location = "private" + default_acl = "private" + file_overwrite = False + custom_domain = False diff --git a/requirements/common.txt b/requirements/common.txt index 9e3785a..abce667 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -24,3 +24,7 @@ Markdown==3.1.1 #Swagger drf-yasg==1.16.0 + +#django-storages +boto3==1.9.223 +django-storages==1.7.1 diff --git a/scripts/update.sh b/scripts/update.sh new file mode 100644 index 0000000..76ca60a --- /dev/null +++ b/scripts/update.sh @@ -0,0 +1,7 @@ + +source /home/ubuntu/venvs/django-vagrant/activate +git pull +pip install -r requirements/prod.txt +cd django-vagrant +./manage.py migrate +sudo supervisorctl -c /etc/supervisor/supervisord.conf restart all From d1e8fbf9a1664f3dab0c238cbef1c3d632f32444 Mon Sep 17 00:00:00 2001 From: Marta_BM Date: Mon, 9 Sep 2019 08:03:36 +0200 Subject: [PATCH 4/4] config --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index b5820b6..a74f1d6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,4 +1,4 @@ -r common.txt #Pre-commit -pre-commit=1.18.3 +pre-commit==1.18.3 black==19.3b0