diff --git a/.env.default b/.env.default index 7b79825..4d85746 100644 --- a/.env.default +++ b/.env.default @@ -4,8 +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/.flake8 b/.flake8 new file mode 100644 index 0000000..f2170f2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,12 @@ +[flake8] +exclude = + build, + .git, + .tox, + ./django/conf/app_template/*, + ./tests/.env, + ./backend/users/migrations/* +ignore = W504, E501 +max-line-length = 79 +max-complexity = 10 +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..f1871a5 --- /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/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/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/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/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 7e4e1b2..fb9f12f 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,106 @@ # 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" +USE_S3 = os.getenv("USE_S3") == "TRUE" -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", + "storages", + "media_upload", ] 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 +129,54 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "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.CommonPasswordValidator" }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "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 +193,62 @@ # 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 = {} + +# 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/backend/urls/main.py b/backend/urls/main.py index 1dbf7b6..0b04ed6 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,31 @@ 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..3d22868 100644 --- a/backend/users/admin.py +++ b/backend/users/admin.py @@ -8,44 +8,53 @@ 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..227c974 100644 --- a/backend/users/migrations/0001_initial.py +++ b/backend/users/migrations/0001_initial.py @@ -11,37 +11,131 @@ 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..281612f 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -11,78 +11,67 @@ 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 + 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 + 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 +80,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 +88,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..2984544 100644 --- a/backend/users/rest_urls/auth.py +++ b/backend/users/rest_urls/auth.py @@ -1,20 +1,31 @@ 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..24277e7 100644 --- a/backend/users/rest_urls/users.py +++ b/backend/users/rest_urls/users.py @@ -1,10 +1,15 @@ 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"), - 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 c56d1f5..f32e1af 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 @@ -55,8 +63,13 @@ 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")): - return Response({"old_password": ["Wrong password."]}, status=status.HTTP_400_BAD_REQUEST) + if not self.object.check_password( + serializer.data.get("old_password") + ): + 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 +79,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): @@ -78,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" % ( @@ -104,11 +120,14 @@ def post(self, request, *args, **kwargs): 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): """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 @@ -117,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): @@ -127,12 +148,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: @@ -140,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 00d7a56..967e354 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -5,24 +5,30 @@ 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) @@ -44,13 +50,12 @@ def validate(self, 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/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/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/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bc124cf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[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..abce667 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 @@ -21,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/requirements/dev.txt b/requirements/dev.txt index 6624a30..a74f1d6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1 +1,4 @@ -r common.txt +#Pre-commit +pre-commit==1.18.3 +black==19.3b0 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 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