From ac02e035eeac5225ebbc1641855a2c4d932b2207 Mon Sep 17 00:00:00 2001 From: Elias Date: Tue, 30 Apr 2019 22:16:12 +0300 Subject: [PATCH 01/12] Worked on creation/update project data via API. --- projectApp/serializers.py | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/projectApp/serializers.py b/projectApp/serializers.py index 8731034..80d38cd 100644 --- a/projectApp/serializers.py +++ b/projectApp/serializers.py @@ -6,11 +6,16 @@ class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag fields = ('title', 'slug') + extra_kwargs = { + 'title': {'validators': []}, + 'slug': {'validators': []} + } class StatusSerializer(serializers.ModelSerializer): class Meta: model = Status fields = ('title', ) + extra_kwargs = {'title': {'validators': []}} class ProjectSerializer(serializers.ModelSerializer): status = StatusSerializer() @@ -19,3 +24,56 @@ class ProjectSerializer(serializers.ModelSerializer): class Meta: model = Project fields = '__all__' + + # Validation of relational fields: + + def validate_status(self, status_data): + title = status_data['title'] + if not Status.objects.filter(title=title).exists(): + issue = "Status '%s' doesn't exist." % title + raise serializers.ValidationError(issue) + + return status_data + + def validate_tags(self, tag_data_list): + wrong_tags = [] + + for tag_data in tag_data_list: + title, slug = tag_data.values() + if not Tag.objects.filter(title=title, slug=slug).exists(): + wrong_tags.append(title) + + if not wrong_tags: + return tag_data_list + + issue = "Problem with next tags: '%s'" % ", ".join(wrong_tags) + raise serializers.ValidationError(issue) + + # Setting write methods explicitly: + + def create(self, validated_data): + status_data = validated_data.pop('status') + tags_data = validated_data.pop('tags') + + status = Status.objects.get(**status_data) + tags = [Tag.objects.get(**td) for td in tags_data] + + project = Project.objects.create(**validated_data, status=status) + project.tags.set(tags) + return project + + def update(self, instance, validated_data): + status_data = validated_data.pop('status') + tags_data = validated_data.pop('tags') + + status = Status.objects.get(**status_data) + tags = [Tag.objects.get(**td) for td in tags_data] + + instance.status = status + instance.tags.set(tags) + for key, value in validated_data.items(): + setattr(instance, key, value) + + updated_fields = ['status'] + list(validated_data.keys()) + instance.save(update_fields=updated_fields) + return instance From 0f767f49d5d07d32da4659872c49f8262c74bfaf Mon Sep 17 00:00:00 2001 From: Elias Date: Sat, 4 May 2019 16:07:25 +0300 Subject: [PATCH 02/12] Worked on userApp models. --- mlp_api/settings.py | 3 +- projectApp/admin.py | 6 +- projectApp/models.py | 20 ++--- projectApp/serializers.py | 38 ++++---- userApp/__init__.py | 0 userApp/admin.py | 14 +++ userApp/apps.py | 5 ++ userApp/models.py | 178 ++++++++++++++++++++++++++++++++++++++ userApp/tests.py | 3 + userApp/utils.py | 18 ++++ userApp/validators.py | 86 ++++++++++++++++++ userApp/views.py | 3 + 12 files changed, 341 insertions(+), 33 deletions(-) create mode 100644 userApp/__init__.py create mode 100644 userApp/admin.py create mode 100644 userApp/apps.py create mode 100644 userApp/models.py create mode 100644 userApp/tests.py create mode 100644 userApp/utils.py create mode 100644 userApp/validators.py create mode 100644 userApp/views.py diff --git a/mlp_api/settings.py b/mlp_api/settings.py index 0f85435..6a6117e 100644 --- a/mlp_api/settings.py +++ b/mlp_api/settings.py @@ -41,6 +41,7 @@ 'tinymce', 'projectApp', 'staticPageApp', + 'userApp', ] MIDDLEWARE = [ @@ -138,4 +139,4 @@ try: from .settings_dev import * except ImportError: - pass \ No newline at end of file + pass diff --git a/projectApp/admin.py b/projectApp/admin.py index c970f78..abcdacb 100644 --- a/projectApp/admin.py +++ b/projectApp/admin.py @@ -1,12 +1,12 @@ from django.contrib import admin -from projectApp.models import Project, Tag, Status +from projectApp.models import Project, Technology, Status @admin.register(Project) class ProjectAdmin(admin.ModelAdmin): pass -@admin.register(Tag) -class TagAdmin(admin.ModelAdmin): +@admin.register(Technology) +class TechnologyAdmin(admin.ModelAdmin): list_display = ('id', 'title', 'slug') list_display_links = ('title',) diff --git a/projectApp/models.py b/projectApp/models.py index 91dd85d..ef7c0c6 100644 --- a/projectApp/models.py +++ b/projectApp/models.py @@ -65,10 +65,10 @@ class Project(models.Model): on_delete=models.SET_NULL, verbose_name=_('status') ) - tags = models.ManyToManyField( - 'Tag', + technologies = models.ManyToManyField( + 'Technology', related_name='projects', - verbose_name=_('tags') + verbose_name=_('programming languages/technologies') ) class Meta: @@ -93,11 +93,11 @@ def save(self, *args, **kwargs): def __str__(self): return "'%s' project." % self.title -class Tag(models.Model): +class Technology(models.Model): """ - Represents tag which marks programming language or technology, - used in some project. Project can have many tags, so it has - one to many relation with Project model. + Represents programming language or technology, + used in some project. Many technologies can be attached to a project, + so it has many to many relationship with Project model. """ title = models.CharField( @@ -115,8 +115,8 @@ class Tag(models.Model): ) class Meta: - verbose_name = _("project's tag") - verbose_name_plural = _("project's tags") + verbose_name = _("project's technology") + verbose_name_plural = _("project's technologies") def save(self, *args, **kwargs): """ @@ -148,4 +148,4 @@ class Meta: verbose_name_plural = _("project's statuses") def __str__(self): - return "Status: '%s'." % self.title \ No newline at end of file + return "Status: '%s'." % self.title diff --git a/projectApp/serializers.py b/projectApp/serializers.py index 80d38cd..03c311a 100644 --- a/projectApp/serializers.py +++ b/projectApp/serializers.py @@ -1,10 +1,10 @@ from rest_framework import serializers -from projectApp.models import Project, Tag, Status +from projectApp.models import Project, Technology, Status -class TagSerializer(serializers.ModelSerializer): +class TechnologySerializer(serializers.ModelSerializer): class Meta: - model = Tag + model = Technology fields = ('title', 'slug') extra_kwargs = { 'title': {'validators': []}, @@ -19,7 +19,7 @@ class Meta: class ProjectSerializer(serializers.ModelSerializer): status = StatusSerializer() - tags = TagSerializer(many=True) + technologies = TechnologySerializer(many=True) class Meta: model = Project @@ -35,42 +35,42 @@ def validate_status(self, status_data): return status_data - def validate_tags(self, tag_data_list): - wrong_tags = [] + def validate_technologies(self, tech_data_list): + wrong_techs = [] - for tag_data in tag_data_list: - title, slug = tag_data.values() - if not Tag.objects.filter(title=title, slug=slug).exists(): - wrong_tags.append(title) + for tech_data in tech_data_list: + title, slug = tech_data.values() + if not Technology.objects.filter(title=title, slug=slug).exists(): + wrong_techs.append(title) - if not wrong_tags: - return tag_data_list + if not wrong_techs: + return tech_data_list - issue = "Problem with next tags: '%s'" % ", ".join(wrong_tags) + issue = "Problem with next techs: '%s'" % ", ".join(wrong_techs) raise serializers.ValidationError(issue) # Setting write methods explicitly: def create(self, validated_data): status_data = validated_data.pop('status') - tags_data = validated_data.pop('tags') + techs_data = validated_data.pop('technologies') status = Status.objects.get(**status_data) - tags = [Tag.objects.get(**td) for td in tags_data] + techs = [Technology.objects.get(**td) for td in techs_data] project = Project.objects.create(**validated_data, status=status) - project.tags.set(tags) + project.technologies.set(techs) return project def update(self, instance, validated_data): status_data = validated_data.pop('status') - tags_data = validated_data.pop('tags') + techs_data = validated_data.pop('technologies') status = Status.objects.get(**status_data) - tags = [Tag.objects.get(**td) for td in tags_data] + techs = [Technology.objects.get(**td) for td in techs_data] instance.status = status - instance.tags.set(tags) + instance.technologies.set(techs) for key, value in validated_data.items(): setattr(instance, key, value) diff --git a/userApp/__init__.py b/userApp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/userApp/admin.py b/userApp/admin.py new file mode 100644 index 0000000..b5ae78f --- /dev/null +++ b/userApp/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from userApp.models import UserAuthorizingData, UserPersonalData, UserProgress + +@admin.register(UserAuthorizingData) +class UserAuthorizingAdmin(admin.ModelAdmin): + pass + +@admin.register(UserPersonalData) +class UserProfileAdmin(admin.ModelAdmin): + pass + +@admin.register(UserProgress) +class UserProgressAdmin(admin.ModelAdmin): + pass diff --git a/userApp/apps.py b/userApp/apps.py new file mode 100644 index 0000000..dc98544 --- /dev/null +++ b/userApp/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class UserappConfig(AppConfig): + name = 'userApp' diff --git a/userApp/models.py b/userApp/models.py new file mode 100644 index 0000000..29bf903 --- /dev/null +++ b/userApp/models.py @@ -0,0 +1,178 @@ +import os +import io +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.core.validators import MinLengthValidator, FileExtensionValidator +from PIL import Image +from mlp_api.settings import MEDIA_ROOT +from userApp.utils import get_userpic_path, get_thumbnail_name +from userApp.validators import MinImageSizeValidator + +class UserAuthorizingData(models.Model): + login = models.CharField( + _('login'), + max_length=255, + unique=True, + help_text="At most 255 characters.", + validators=[MinLengthValidator(4)] + ) + email = models.EmailField(_('email'), unique=True) + phone = models.DecimalField( + _('phone number'), + max_digits=15, + decimal_places=0, + unique=True, + help_text="Phone number in international format without '+','(',')','-'." + ) + password = models.TextField(_('password')) + email_confirmed = models.BooleanField(_('email confirmed'), default=False) + date_of_creation = models.DateTimeField( + _('date of creation'), + auto_now_add = True, + auto_now = False + ) + date_of_update = models.DateTimeField( + _('date of update'), + auto_now_add = False, + auto_now = True + ) + + class Meta: + verbose_name = _("authorizing user's data") + verbose_name_plural = _("authorizing data of users") + + def __str__(self): + return self.login + ": " + self.email + +class UserPersonalData(models.Model): + first_name = models.CharField( + _('first name'), + max_length=255, + null=True, + help_text="At most 255 characters." + ) + last_name = models.CharField( + _('last name'), + max_length=255, + null=True, + blank=True, + help_text="At most 255 characters." + ) + avatar = models.ImageField( + _('avatar'), + upload_to=get_userpic_path, + validators=[ + FileExtensionValidator(allowed_extensions=('jpg','jpeg')), + MinImageSizeValidator(250, 250) + ], + null=True, + blank=True + ) + date_of_birth = models.DateField(_('date of birth'), null=True, blank=True) + about_myself = models.TextField(_('about myself'), null=True, blank=True) + technologies = models.ManyToManyField( + 'projectApp.Technology', + related_name='users', + verbose_name=_('programming languages/technologies'), + blank=True + ) + + auth_data = models.OneToOneField( + 'UserAuthorizingData', + related_name='personal_data', + on_delete=models.CASCADE, + verbose_name=_('user\'s authorizing data') + ) + + class Meta: + verbose_name = _("personal data of a user") + verbose_name_plural = _("personal data of users") + + def __str__(self): + names = (self.first_name, self.last_name, self.auth_data.login) + return "%s %s (%s)" % names + + # Handling user's profile picture: + avatar_size = (460, 460) # size in pixels + avatar_thumbnail_size = (150, 150) # size in pixels + + def _get_avatar_dir(self): + """ Returns path to directory for user's avatar. """ + userpic_dirname = "user_%s" % self.auth_data.id + return os.path.join(MEDIA_ROOT, 'userApp', 'avatar', userpic_dirname) + + def _make_avatar_from_field(self): + if not self.avatar: return None + + # Verify if there user picture's directory exists: + image_dir = self._get_avatar_dir() + if not os.path.exists(image_dir): + os.makedirs(image_dir) + + # Resizing given user's picture: + correct_image = Image.open(self.avatar.file).resize(self.avatar_size) + image_bytestream = io.BytesIO() + correct_image.save(image_bytestream, 'JPEG') + self.avatar.file = image_bytestream + + # Creating thumbnail: + correct_image.thumbnail(self.avatar_thumbnail_size) + thumbnail_name = get_thumbnail_name(self.avatar.name) + thumbnail_path = os.path.join(image_dir, thumbnail_name) + correct_image.save(thumbnail_path, 'JPEG') + + def _update_avatar(self): + # Check avatar, if it is updated or removed, delete old image: + try: + old_avatar = UserPersonalData.objects.get(id=self.id).avatar + # In case of changes of avatar: + if self.avatar != old_avatar: + # If old avatar is present, it should be removed: + if old_avatar: + # Remove avatar thumbnail: + thumbnail_path = os.path.join( + MEDIA_ROOT, + get_thumbnail_name(old_avatar.name) + ) + os.remove(thumbnail_path) + # Remove avatar: + old_avatar.delete(save=False) + + # If there is a new avatar, it should be stored: + self._make_avatar_from_field() + + except UserPersonalData.DoesNotExist: + self._make_avatar_from_field() + + def save(self, *args, **kwargs): + self._update_avatar() + super().save(*args, **kwargs) + +class UserProgress(models.Model): + level = models.PositiveSmallIntegerField( + _('level'), + default=0 + ) + amount_tasks = models.PositiveSmallIntegerField( + _('amount tasks'), + default=0 + ) + reputation_point = models.PositiveIntegerField( + _('reputation point'), + default=0 + ) + + user = models.OneToOneField( + 'UserAuthorizingData', + related_name='progress', + on_delete=models.CASCADE, + verbose_name=_('user\'s progress') + ) + + class Meta: + verbose_name = _("user's progress") + verbose_name_plural = _("progress of users") + + def __str__(self): + line = "%s: level = %d, rating = %d" + return line % (self.user.login, self.level, self.reputation_point) diff --git a/userApp/tests.py b/userApp/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/userApp/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/userApp/utils.py b/userApp/utils.py new file mode 100644 index 0000000..60cefba --- /dev/null +++ b/userApp/utils.py @@ -0,0 +1,18 @@ +from os import path +import re +from mlp_api.settings import MEDIA_ROOT + +def get_userpic_path(instance, filename): + """ + Takes an instance of UserPersonalData model and name of profile's picture. + Returns a path to profile's picture relative to MEDIA_ROOT. + """ + userpic_dirname = 'user_%s' % instance.auth_data.id + return path.join('userApp', 'avatar', userpic_dirname, filename) + +def get_thumbnail_name(avatar_name): + """ + Takes name of user's avatar file. + Return name of related thumbnail file. + """ + return re.sub(r'\.jpe?g$', lambda fnd: "_150x150"+fnd.group(0), avatar_name) diff --git a/userApp/validators.py b/userApp/validators.py new file mode 100644 index 0000000..c6eab9f --- /dev/null +++ b/userApp/validators.py @@ -0,0 +1,86 @@ +from django.utils.deconstruct import deconstructible +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError +from PIL import Image + +@deconstructible +class ImageSizeValidator: + """ + Takes width and height in pixels for comparing with image size. + Creates validator for an ImageField. + """ + message = _( + "Ensure that image size is equal to " + + "%(limit_width)sx%(limit_height)s pixels." + ) + code = "limit_image_size" + + def __init__(self, limit_width, limit_height, message=None, code=None): + self.limit_width = limit_width + self.limit_height = limit_height + + if message is not None: + self.message = message + if code is not None: + self.code = code + + def __call__(self, value): + try: + size = value.width, value.height + except ValueError: + # Image field with image type 'image/x-icon' produces an ValueError + # with message: 'buffer is not large enough' each time + # when you attempt to read its 'width/height' property. + size = Image.open(value.file).size + + limit_size = self.limit_width, self.limit_height + + if self.compare_size(size, limit_size): + raise ValidationError( + self.message, + code=self.code, + params={ + 'limit_width': self.limit_width, + 'limit_height': self.limit_height + } + ) + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) and + self.min_width == other.min_width and + self.min_height == other.min_height and + self.message == other.message and + self.code == other.code + ) + + def compare_size(self, current_size, limit_size): + width, height = current_size + limit_width, limit_height = limit_size + return not (width == limit_width and height == limit_height) + +@deconstructible +class MinImageSizeValidator(ImageSizeValidator): + message = _( + "Ensure that image size is not less than " + + "%(limit_width)sx%(limit_height)s pixels." + ) + code = "min_image_size" + + def compare_size(self, current_size, min_size): + width, height = current_size + min_width, min_height = min_size + return width < min_width or height < min_height + +@deconstructible +class MaxImageSizeValidator(ImageSizeValidator): + message = _( + "Ensure that image size is not more than " + + "%(limit_width)sx%(limit_height)s pixels." + ) + code = "max_image_size" + + def compare_size(self, current_size, max_size): + width, height = current_size + max_width, max_height = max_size + return width > max_width or height > max_height diff --git a/userApp/views.py b/userApp/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/userApp/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From e5189b2213a49d2af0e1ad529dc08bb4ca4212dd Mon Sep 17 00:00:00 2001 From: Elias Date: Mon, 6 May 2019 16:57:04 +0300 Subject: [PATCH 03/12] Extended AbstractBaseUser --- mlp_api/settings.py | 2 ++ userApp/admin.py | 38 ++++++++++++++++++++++++++++++++++---- userApp/forms.py | 20 ++++++++++++++++++++ userApp/manager.py | 34 ++++++++++++++++++++++++++++++++++ userApp/models.py | 29 +++++++++++++++++------------ userApp/utils.py | 1 - 6 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 userApp/forms.py create mode 100644 userApp/manager.py diff --git a/mlp_api/settings.py b/mlp_api/settings.py index 6a6117e..bb00a85 100644 --- a/mlp_api/settings.py +++ b/mlp_api/settings.py @@ -85,6 +85,8 @@ } } +# User model: +AUTH_USER_MODEL = 'userApp.User' # Password validation # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators diff --git a/userApp/admin.py b/userApp/admin.py index b5ae78f..5830dda 100644 --- a/userApp/admin.py +++ b/userApp/admin.py @@ -1,9 +1,39 @@ from django.contrib import admin -from userApp.models import UserAuthorizingData, UserPersonalData, UserProgress +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from userApp.models import User, UserPersonalData, UserProgress +from userApp.forms import UserCreationForm, UserChangeForm -@admin.register(UserAuthorizingData) -class UserAuthorizingAdmin(admin.ModelAdmin): - pass + +@admin.register(User) +class UserAdmin(BaseUserAdmin): + form = UserChangeForm + add_form = UserCreationForm + list_display = ('login', 'email', 'phone', 'is_staff') + list_display_links = ('login',) + list_filter = ('is_staff', 'is_active', 'is_superuser') + seach_fields = ('login', 'email') + ordering = ('login',) + readonly_fields = ('date_of_creation', 'date_of_update') + fieldsets = ( + (None, {'fields': ('login', 'password')}), + ('Personal info', {'fields': ('phone', 'email', 'email_confirmed')}), + ('Permissons', { + 'fields': ( + 'is_active', 'is_staff', 'is_superuser', + 'groups', 'user_permissions' + ) + }), + ('Important dates', {'fields': ('date_of_creation', 'date_of_update')}) + ) + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ( + 'login', 'email', 'phone', + 'password1', 'password2' + ) + }), + ) @admin.register(UserPersonalData) class UserProfileAdmin(admin.ModelAdmin): diff --git a/userApp/forms.py b/userApp/forms.py new file mode 100644 index 0000000..51ea79a --- /dev/null +++ b/userApp/forms.py @@ -0,0 +1,20 @@ +from django.contrib.auth.forms import ( + UserCreationForm as BaseUserCreationForm, + UserChangeForm as BaseUserChangeForm +) +from userApp.models import User + + +class UserCreationForm(BaseUserCreationForm): + class Meta: + model = User + fields = ('login', 'email', 'phone') + +class UserChangeForm(BaseUserChangeForm): + class Meta: + model = User + fields = ( + 'login', 'email', 'phone', + 'email_confirmed', + 'is_active', 'is_staff', 'is_superuser' + ) diff --git a/userApp/manager.py b/userApp/manager.py new file mode 100644 index 0000000..8b10d80 --- /dev/null +++ b/userApp/manager.py @@ -0,0 +1,34 @@ +from django.contrib.auth.base_user import BaseUserManager + +class UserManager(BaseUserManager): + use_in_migrations = True + + def _create_user(self, login, email, phone, password, **extra_fields): + if not email: + raise ValueError("User must have an email address.") + if not phone: + raise ValueError("User must have a phone number.") + + user = self.model( + login=login, + email=self.normalize_email(email), + phone=phone, + **extra_fields + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_user(self, login, email, phone, password=None, **extra_fields): + extra_fields.setdefault('is_superuser', False) + return self._create_user(login, email, phone, password, **extra_fields) + + def create_superuser(self, login, email, phone, password, **extra_fields): + extra_fields.setdefault('is_staff', True) + is_superuser = extra_fields.setdefault('is_superuser', True) + + if is_superuser is not True: + raise ValueError("Superuser must have 'is_superuser = True'") + + return self._create_user(login, email, phone, password, **extra_fields) diff --git a/userApp/models.py b/userApp/models.py index 29bf903..5d3e809 100644 --- a/userApp/models.py +++ b/userApp/models.py @@ -3,12 +3,14 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinLengthValidator, FileExtensionValidator +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.conf import settings from PIL import Image -from mlp_api.settings import MEDIA_ROOT +from userApp.manager import UserManager from userApp.utils import get_userpic_path, get_thumbnail_name from userApp.validators import MinImageSizeValidator -class UserAuthorizingData(models.Model): +class User(AbstractBaseUser, PermissionsMixin): login = models.CharField( _('login'), max_length=255, @@ -24,7 +26,6 @@ class UserAuthorizingData(models.Model): unique=True, help_text="Phone number in international format without '+','(',')','-'." ) - password = models.TextField(_('password')) email_confirmed = models.BooleanField(_('email confirmed'), default=False) date_of_creation = models.DateTimeField( _('date of creation'), @@ -37,9 +38,13 @@ class UserAuthorizingData(models.Model): auto_now = True ) - class Meta: - verbose_name = _("authorizing user's data") - verbose_name_plural = _("authorizing data of users") + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + + objects = UserManager() + + USERNAME_FIELD = 'login' + REQUIRED_FIELDS = ('email', 'phone') def __str__(self): return self.login + ": " + self.email @@ -78,7 +83,7 @@ class UserPersonalData(models.Model): ) auth_data = models.OneToOneField( - 'UserAuthorizingData', + settings.AUTH_USER_MODEL, related_name='personal_data', on_delete=models.CASCADE, verbose_name=_('user\'s authorizing data') @@ -98,8 +103,8 @@ def __str__(self): def _get_avatar_dir(self): """ Returns path to directory for user's avatar. """ - userpic_dirname = "user_%s" % self.auth_data.id - return os.path.join(MEDIA_ROOT, 'userApp', 'avatar', userpic_dirname) + user_dir = "user_%s" % self.auth_data.id + return os.path.join(settings.MEDIA_ROOT, 'userApp', 'avatar', user_dir) def _make_avatar_from_field(self): if not self.avatar: return None @@ -131,7 +136,7 @@ def _update_avatar(self): if old_avatar: # Remove avatar thumbnail: thumbnail_path = os.path.join( - MEDIA_ROOT, + settings.MEDIA_ROOT, get_thumbnail_name(old_avatar.name) ) os.remove(thumbnail_path) @@ -163,10 +168,10 @@ class UserProgress(models.Model): ) user = models.OneToOneField( - 'UserAuthorizingData', + settings.AUTH_USER_MODEL, related_name='progress', on_delete=models.CASCADE, - verbose_name=_('user\'s progress') + verbose_name=_('user') ) class Meta: diff --git a/userApp/utils.py b/userApp/utils.py index 60cefba..32c5cd4 100644 --- a/userApp/utils.py +++ b/userApp/utils.py @@ -1,6 +1,5 @@ from os import path import re -from mlp_api.settings import MEDIA_ROOT def get_userpic_path(instance, filename): """ From 2ecdfa8cb2b904ea6ba9be93eea77121d379101b Mon Sep 17 00:00:00 2001 From: Elias Date: Mon, 6 May 2019 17:34:33 +0300 Subject: [PATCH 04/12] Restructured corresponding to mail develop branch --- .gitignore | 2 +- manage.py => mlp_api/manage.py | 0 mlp_api/{ => mlp_api}/__init__.py | 0 mlp_api/{ => mlp_api}/settings.py | 0 mlp_api/mlp_api/settings_dev.py | 18 ++++++++++++++++++ mlp_api/{ => mlp_api}/urls.py | 0 mlp_api/{ => mlp_api}/urls_api.py | 0 mlp_api/{ => mlp_api}/wsgi.py | 0 {projectApp => mlp_api/projectApp}/__init__.py | 0 {projectApp => mlp_api/projectApp}/admin.py | 0 {projectApp => mlp_api/projectApp}/apps.py | 0 {projectApp => mlp_api/projectApp}/models.py | 0 .../projectApp}/permissions.py | 0 .../projectApp}/serializers.py | 0 {projectApp => mlp_api/projectApp}/tests.py | 0 {projectApp => mlp_api/projectApp}/urls.py | 0 .../projectApp}/validators.py | 0 {projectApp => mlp_api/projectApp}/views.py | 0 requirements.txt => mlp_api/requirements.txt | 0 .../staticPageApp}/__init__.py | 0 .../staticPageApp}/admin.py | 0 .../staticPageApp}/apps.py | 0 .../staticPageApp}/models.py | 0 .../staticPageApp}/tests.py | 0 .../staticPageApp}/views.py | 0 {userApp => mlp_api/userApp}/__init__.py | 0 {userApp => mlp_api/userApp}/admin.py | 0 {userApp => mlp_api/userApp}/apps.py | 0 {userApp => mlp_api/userApp}/forms.py | 0 {userApp => mlp_api/userApp}/manager.py | 0 {userApp => mlp_api/userApp}/models.py | 0 {userApp => mlp_api/userApp}/tests.py | 0 {userApp => mlp_api/userApp}/utils.py | 0 {userApp => mlp_api/userApp}/validators.py | 0 {userApp => mlp_api/userApp}/views.py | 0 35 files changed, 19 insertions(+), 1 deletion(-) rename manage.py => mlp_api/manage.py (100%) rename mlp_api/{ => mlp_api}/__init__.py (100%) rename mlp_api/{ => mlp_api}/settings.py (100%) create mode 100644 mlp_api/mlp_api/settings_dev.py rename mlp_api/{ => mlp_api}/urls.py (100%) rename mlp_api/{ => mlp_api}/urls_api.py (100%) rename mlp_api/{ => mlp_api}/wsgi.py (100%) rename {projectApp => mlp_api/projectApp}/__init__.py (100%) rename {projectApp => mlp_api/projectApp}/admin.py (100%) rename {projectApp => mlp_api/projectApp}/apps.py (100%) rename {projectApp => mlp_api/projectApp}/models.py (100%) rename {projectApp => mlp_api/projectApp}/permissions.py (100%) rename {projectApp => mlp_api/projectApp}/serializers.py (100%) rename {projectApp => mlp_api/projectApp}/tests.py (100%) rename {projectApp => mlp_api/projectApp}/urls.py (100%) rename {projectApp => mlp_api/projectApp}/validators.py (100%) rename {projectApp => mlp_api/projectApp}/views.py (100%) rename requirements.txt => mlp_api/requirements.txt (100%) rename {staticPageApp => mlp_api/staticPageApp}/__init__.py (100%) rename {staticPageApp => mlp_api/staticPageApp}/admin.py (100%) rename {staticPageApp => mlp_api/staticPageApp}/apps.py (100%) rename {staticPageApp => mlp_api/staticPageApp}/models.py (100%) rename {staticPageApp => mlp_api/staticPageApp}/tests.py (100%) rename {staticPageApp => mlp_api/staticPageApp}/views.py (100%) rename {userApp => mlp_api/userApp}/__init__.py (100%) rename {userApp => mlp_api/userApp}/admin.py (100%) rename {userApp => mlp_api/userApp}/apps.py (100%) rename {userApp => mlp_api/userApp}/forms.py (100%) rename {userApp => mlp_api/userApp}/manager.py (100%) rename {userApp => mlp_api/userApp}/models.py (100%) rename {userApp => mlp_api/userApp}/tests.py (100%) rename {userApp => mlp_api/userApp}/utils.py (100%) rename {userApp => mlp_api/userApp}/validators.py (100%) rename {userApp => mlp_api/userApp}/views.py (100%) diff --git a/.gitignore b/.gitignore index a496d09..2e0ea0a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ migrations/ __pycache__/ *.pyc backend/mlp/settings_dev.py -mlp_api/settings_dev.py +mlp_api/mlp_api/settings_dev.py diff --git a/manage.py b/mlp_api/manage.py similarity index 100% rename from manage.py rename to mlp_api/manage.py diff --git a/mlp_api/__init__.py b/mlp_api/mlp_api/__init__.py similarity index 100% rename from mlp_api/__init__.py rename to mlp_api/mlp_api/__init__.py diff --git a/mlp_api/settings.py b/mlp_api/mlp_api/settings.py similarity index 100% rename from mlp_api/settings.py rename to mlp_api/mlp_api/settings.py diff --git a/mlp_api/mlp_api/settings_dev.py b/mlp_api/mlp_api/settings_dev.py new file mode 100644 index 0000000..d036ab2 --- /dev/null +++ b/mlp_api/mlp_api/settings_dev.py @@ -0,0 +1,18 @@ +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'mlpweb', + 'USER': 'mlp', + 'PASSWORD': '|c0#+R01U$0u1', + 'HOST': 'localhost', + 'PORT': 5432 + } +} + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } +} + +TIME_ZONE = 'Europe/Kiev' diff --git a/mlp_api/urls.py b/mlp_api/mlp_api/urls.py similarity index 100% rename from mlp_api/urls.py rename to mlp_api/mlp_api/urls.py diff --git a/mlp_api/urls_api.py b/mlp_api/mlp_api/urls_api.py similarity index 100% rename from mlp_api/urls_api.py rename to mlp_api/mlp_api/urls_api.py diff --git a/mlp_api/wsgi.py b/mlp_api/mlp_api/wsgi.py similarity index 100% rename from mlp_api/wsgi.py rename to mlp_api/mlp_api/wsgi.py diff --git a/projectApp/__init__.py b/mlp_api/projectApp/__init__.py similarity index 100% rename from projectApp/__init__.py rename to mlp_api/projectApp/__init__.py diff --git a/projectApp/admin.py b/mlp_api/projectApp/admin.py similarity index 100% rename from projectApp/admin.py rename to mlp_api/projectApp/admin.py diff --git a/projectApp/apps.py b/mlp_api/projectApp/apps.py similarity index 100% rename from projectApp/apps.py rename to mlp_api/projectApp/apps.py diff --git a/projectApp/models.py b/mlp_api/projectApp/models.py similarity index 100% rename from projectApp/models.py rename to mlp_api/projectApp/models.py diff --git a/projectApp/permissions.py b/mlp_api/projectApp/permissions.py similarity index 100% rename from projectApp/permissions.py rename to mlp_api/projectApp/permissions.py diff --git a/projectApp/serializers.py b/mlp_api/projectApp/serializers.py similarity index 100% rename from projectApp/serializers.py rename to mlp_api/projectApp/serializers.py diff --git a/projectApp/tests.py b/mlp_api/projectApp/tests.py similarity index 100% rename from projectApp/tests.py rename to mlp_api/projectApp/tests.py diff --git a/projectApp/urls.py b/mlp_api/projectApp/urls.py similarity index 100% rename from projectApp/urls.py rename to mlp_api/projectApp/urls.py diff --git a/projectApp/validators.py b/mlp_api/projectApp/validators.py similarity index 100% rename from projectApp/validators.py rename to mlp_api/projectApp/validators.py diff --git a/projectApp/views.py b/mlp_api/projectApp/views.py similarity index 100% rename from projectApp/views.py rename to mlp_api/projectApp/views.py diff --git a/requirements.txt b/mlp_api/requirements.txt similarity index 100% rename from requirements.txt rename to mlp_api/requirements.txt diff --git a/staticPageApp/__init__.py b/mlp_api/staticPageApp/__init__.py similarity index 100% rename from staticPageApp/__init__.py rename to mlp_api/staticPageApp/__init__.py diff --git a/staticPageApp/admin.py b/mlp_api/staticPageApp/admin.py similarity index 100% rename from staticPageApp/admin.py rename to mlp_api/staticPageApp/admin.py diff --git a/staticPageApp/apps.py b/mlp_api/staticPageApp/apps.py similarity index 100% rename from staticPageApp/apps.py rename to mlp_api/staticPageApp/apps.py diff --git a/staticPageApp/models.py b/mlp_api/staticPageApp/models.py similarity index 100% rename from staticPageApp/models.py rename to mlp_api/staticPageApp/models.py diff --git a/staticPageApp/tests.py b/mlp_api/staticPageApp/tests.py similarity index 100% rename from staticPageApp/tests.py rename to mlp_api/staticPageApp/tests.py diff --git a/staticPageApp/views.py b/mlp_api/staticPageApp/views.py similarity index 100% rename from staticPageApp/views.py rename to mlp_api/staticPageApp/views.py diff --git a/userApp/__init__.py b/mlp_api/userApp/__init__.py similarity index 100% rename from userApp/__init__.py rename to mlp_api/userApp/__init__.py diff --git a/userApp/admin.py b/mlp_api/userApp/admin.py similarity index 100% rename from userApp/admin.py rename to mlp_api/userApp/admin.py diff --git a/userApp/apps.py b/mlp_api/userApp/apps.py similarity index 100% rename from userApp/apps.py rename to mlp_api/userApp/apps.py diff --git a/userApp/forms.py b/mlp_api/userApp/forms.py similarity index 100% rename from userApp/forms.py rename to mlp_api/userApp/forms.py diff --git a/userApp/manager.py b/mlp_api/userApp/manager.py similarity index 100% rename from userApp/manager.py rename to mlp_api/userApp/manager.py diff --git a/userApp/models.py b/mlp_api/userApp/models.py similarity index 100% rename from userApp/models.py rename to mlp_api/userApp/models.py diff --git a/userApp/tests.py b/mlp_api/userApp/tests.py similarity index 100% rename from userApp/tests.py rename to mlp_api/userApp/tests.py diff --git a/userApp/utils.py b/mlp_api/userApp/utils.py similarity index 100% rename from userApp/utils.py rename to mlp_api/userApp/utils.py diff --git a/userApp/validators.py b/mlp_api/userApp/validators.py similarity index 100% rename from userApp/validators.py rename to mlp_api/userApp/validators.py diff --git a/userApp/views.py b/mlp_api/userApp/views.py similarity index 100% rename from userApp/views.py rename to mlp_api/userApp/views.py From 946e89cc9620305bdf66d71770e30090a6217c24 Mon Sep 17 00:00:00 2001 From: Elias Date: Mon, 6 May 2019 18:26:58 +0300 Subject: [PATCH 05/12] Extended base user model (with last changes) --- mlp_api/mlp_api/settings_dev.py | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 mlp_api/mlp_api/settings_dev.py diff --git a/mlp_api/mlp_api/settings_dev.py b/mlp_api/mlp_api/settings_dev.py deleted file mode 100644 index d036ab2..0000000 --- a/mlp_api/mlp_api/settings_dev.py +++ /dev/null @@ -1,18 +0,0 @@ -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'mlpweb', - 'USER': 'mlp', - 'PASSWORD': '|c0#+R01U$0u1', - 'HOST': 'localhost', - 'PORT': 5432 - } -} - -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', - } -} - -TIME_ZONE = 'Europe/Kiev' From d1ae5c688c7e708f5c09f58180c9f28ef173d99e Mon Sep 17 00:00:00 2001 From: Elias Date: Tue, 7 May 2019 15:08:57 +0300 Subject: [PATCH 06/12] Fixed bug with unexpected removed images. --- mlp_api/userApp/models.py | 2 +- mlp_api/userApp/validators.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mlp_api/userApp/models.py b/mlp_api/userApp/models.py index 5d3e809..5085c57 100644 --- a/mlp_api/userApp/models.py +++ b/mlp_api/userApp/models.py @@ -146,7 +146,7 @@ def _update_avatar(self): # If there is a new avatar, it should be stored: self._make_avatar_from_field() - except UserPersonalData.DoesNotExist: + except (UserPersonalData.DoesNotExist, FileNotFoundError): self._make_avatar_from_field() def save(self, *args, **kwargs): diff --git a/mlp_api/userApp/validators.py b/mlp_api/userApp/validators.py index c6eab9f..28c7631 100644 --- a/mlp_api/userApp/validators.py +++ b/mlp_api/userApp/validators.py @@ -32,6 +32,9 @@ def __call__(self, value): # with message: 'buffer is not large enough' each time # when you attempt to read its 'width/height' property. size = Image.open(value.file).size + except FileNotFoundError: + # In case if image was removed manually from user's directory: + raise ValidationError("Image is not present in user's directory.") limit_size = self.limit_width, self.limit_height From 6e53ad140b178cc958793b033082dc0aac059095 Mon Sep 17 00:00:00 2001 From: Elias Date: Fri, 10 May 2019 13:14:11 +0300 Subject: [PATCH 07/12] Worked on account activation implementation --- mlp_api/mlp_api/urls_api.py | 4 +- mlp_api/userApp/models.py | 79 +++++++++++++++++++++++++++++++++- mlp_api/userApp/serializers.py | 44 +++++++++++++++++++ mlp_api/userApp/urls.py | 10 +++++ mlp_api/userApp/utils.py | 10 +++++ mlp_api/userApp/views.py | 69 ++++++++++++++++++++++++++++- 6 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 mlp_api/userApp/serializers.py create mode 100644 mlp_api/userApp/urls.py diff --git a/mlp_api/mlp_api/urls_api.py b/mlp_api/mlp_api/urls_api.py index d968616..d296b14 100644 --- a/mlp_api/mlp_api/urls_api.py +++ b/mlp_api/mlp_api/urls_api.py @@ -2,6 +2,7 @@ from projectApp.urls import router as projectApp_router from staticPageApp.urls import router as staticPageApp_router +from userApp.urls import router as userApp_router class DefaultRouter(routers.DefaultRouter): """ @@ -20,4 +21,5 @@ def extend(self, router): # Include new routers from applications router = DefaultRouter() router.extend(projectApp_router) -router.extend(staticPageApp_router) \ No newline at end of file +router.extend(staticPageApp_router) +router.extend(userApp_router) diff --git a/mlp_api/userApp/models.py b/mlp_api/userApp/models.py index 5085c57..d7be397 100644 --- a/mlp_api/userApp/models.py +++ b/mlp_api/userApp/models.py @@ -1,13 +1,20 @@ import os import io +from datetime import timedelta from django.db import models +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from django.core.mail import send_mail from django.core.validators import MinLengthValidator, FileExtensionValidator from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin from django.conf import settings from PIL import Image from userApp.manager import UserManager -from userApp.utils import get_userpic_path, get_thumbnail_name +from userApp.utils import ( + get_userpic_path, + get_thumbnail_name, + generate_random_string +) from userApp.validators import MinImageSizeValidator class User(AbstractBaseUser, PermissionsMixin): @@ -49,6 +56,9 @@ class User(AbstractBaseUser, PermissionsMixin): def __str__(self): return self.login + ": " + self.email + def email_user(self, subject, message, from_email=None, **kwargs): + send_mail(subject, message, from_email, [self.email], **kwargs) + class UserPersonalData(models.Model): first_name = models.CharField( _('first name'), @@ -181,3 +191,70 @@ class Meta: def __str__(self): line = "%s: level = %d, rating = %d" return line % (self.user.login, self.level, self.reputation_point) + + +class AccountActivationCode(models.Model): + code = models.TextField(_('account activation code')) + date_of_creation = models.DateTimeField(auto_now_add=True, auto_now=False) + + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + related_name='activation_code', + on_delete=models.CASCADE, + verbose_name=_('user') + ) + + email_message_template = { + 'subject': _("MLP Account Activation"), + 'body': _( + "Hi, %(username)s!\n" + + "To activate your account, follow the next link:\n" + + "%(link)s" + ) + } + link_template = ( + "%(protocol)s://%(hostname)s:%(port)d" + "/api/v1/account-activation/%(code)s/" + ) + expiration_term = timedelta(hours=24) + + class Meta: + verbose_name = _("account activation code") + verbose_name_plural = _("account activation codes") + + @classmethod + def get_actuals(cls): + """ + Returns filtered queryset of model items, + which was created not later then expiration date. + """ + time_floor = timezone.now() - cls.expiration_term + return cls.objects.filter(date_of_creation__gte=time_floor) + + def __str__(self): + return self.code + + def _generate_link(self): + """ Returns reliable account activation link. """ + + # Like a temporary solution, we can user a pre-defined + # dictionary in settings_dev.py to configure an absolute url base + # depending on development server's parameters + return self.link_template % { + 'protocol': settings.MLP_WEBSITE['protocol'], + 'hostname': settings.MLP_WEBSITE['hostname'], + 'port': settings.MLP_WEBSITE['port'], + 'code': self.code + } + + def notificate_user(self): + msg_body = {'username': self.user.login, 'link': self._generate_link()} + self.user.email_user( + self.email_message_template['subject'], + self.email_message_template['body'] % msg_body, + ) + + def save(self, *args, **kwargs): + self.code = generate_random_string(30, 40) + self.notificate_user() + super().save(*args, **kwargs) diff --git a/mlp_api/userApp/serializers.py b/mlp_api/userApp/serializers.py new file mode 100644 index 0000000..41286a1 --- /dev/null +++ b/mlp_api/userApp/serializers.py @@ -0,0 +1,44 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.password_validation import ( + validate_password, + password_validators_help_texts as get_passw_helps +) +from userApp.models import User, AccountActivationCode + +class UserSerializer(serializers.ModelSerializer): + password1 = serializers.CharField( + label=_('password'), + write_only=True, + trim_whitespace=False, + validators=[validate_password], + help_text=_(' '.join(get_passw_helps())) + ) + password2 = serializers.CharField( + label=_('password confirmation'), + write_only=True, + trim_whitespace=False, + help_text=_('Enter the same password as before.') + ) + + class Meta: + model = User + fields = ('login', 'email', 'phone', 'password1', 'password2') + + def validate_password2(self, password2): + password1 = self.initial_data.get('password1') + if password1 and password2 and password1 != password2: + raise serializers.ValidationError("Passwords didn't match.") + + return password2 + + def create(self, validated_data): + user = User( + login=validated_data['login'], + email=validated_data['email'], + phone=validated_data['phone'] + ) + user.set_password(validated_data['password2']) + user.save() + AccountActivationCode.objects.create(user=user) + return user diff --git a/mlp_api/userApp/urls.py b/mlp_api/userApp/urls.py new file mode 100644 index 0000000..0f156c2 --- /dev/null +++ b/mlp_api/userApp/urls.py @@ -0,0 +1,10 @@ +from django.urls import include, path +from rest_framework import routers +from userApp.views import UserViewSet, AccountActivationViewSet + + +router = routers.SimpleRouter() +router.register(r'user', UserViewSet) +router.register(r'account-activation', AccountActivationViewSet, 'account-activation') + +urlpatterns = router.urls diff --git a/mlp_api/userApp/utils.py b/mlp_api/userApp/utils.py index 32c5cd4..6655a12 100644 --- a/mlp_api/userApp/utils.py +++ b/mlp_api/userApp/utils.py @@ -1,5 +1,7 @@ from os import path import re +import random +from string import ascii_letters, digits def get_userpic_path(instance, filename): """ @@ -15,3 +17,11 @@ def get_thumbnail_name(avatar_name): Return name of related thumbnail file. """ return re.sub(r'\.jpe?g$', lambda fnd: "_150x150"+fnd.group(0), avatar_name) + +def generate_random_string(min_len, max_len, chars=ascii_letters + digits): + """ + Takes minimum and maximum length of output string and one optional argument + - sequence of characters, on which a new random string will be based. + """ + randrange = range(random.randrange(min_len, max_len)) + return ''.join(random.choice(chars) for _ in randrange) diff --git a/mlp_api/userApp/views.py b/mlp_api/userApp/views.py index 91ea44a..06a553b 100644 --- a/mlp_api/userApp/views.py +++ b/mlp_api/userApp/views.py @@ -1,3 +1,68 @@ -from django.shortcuts import render +from django.shortcuts import redirect +from django.contrib.auth import authenticate +from rest_framework import status +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ( + CreateModelMixin, + RetrieveModelMixin, + ListModelMixin +) +from rest_framework.response import Response +from userApp.models import User, AccountActivationCode +from userApp.serializers import UserSerializer -# Create your views here. + +create_retrieve_list_interface = ( + CreateModelMixin, RetrieveModelMixin, ListModelMixin +) +class UserViewSet(*create_retrieve_list_interface, GenericViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + lookup_field = 'login' + +class AccountActivationViewSet(GenericViewSet): + lookup_field = 'code' + + def get_queryset(self): + return AccountActivationCode.get_actuals() + + def retrieve(self, request, code=None): + """ + Takes activation GET request (user has followed by activation link). + Activates user account if activation code is correct. + """ + code_obj = self.get_object() + code_obj.user.email_confirmed = True + code_obj.user.save() + code_obj.delete() + return redirect('/') + + def create(self, request): + """ + Takes POST request for update activation link. + """ + user = authenticate( + login=request.data.get('login'), + password=request.data.get('password') + ) + + if user is None: + response_status = status.HTTP_401_UNAUTHORIZED + response_text = "Authorization failed." + elif user.email_confirmed: + response_status = status.HTTP_400_BAD_REQUEST + response_text = "Your account is already activated." + else: + if self.get_queryset().filter(user=user).exists(): + # Activation link already exists, so just send it to user: + user.activation_code.notificate_user() + else: + # Remove expired link of the user: + AccountActivationCode.objects.filter(user=user).delete() + # Create new one: + AccountActivationCode.objects.create(user=user) + + response_status = status.HTTP_200_OK + response_text = "We have sent activation link on your email." + + return Response({'detail':response_text}, response_status) From d52b0df6a94dee090f5376d06c35fa4e69987187 Mon Sep 17 00:00:00 2001 From: Elias Date: Fri, 10 May 2019 18:00:04 +0300 Subject: [PATCH 08/12] Implemented account activation and added to admin --- mlp_api/userApp/admin.py | 14 +++++++++++++- mlp_api/userApp/models.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/mlp_api/userApp/admin.py b/mlp_api/userApp/admin.py index 5830dda..fe0eb27 100644 --- a/mlp_api/userApp/admin.py +++ b/mlp_api/userApp/admin.py @@ -1,6 +1,11 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from userApp.models import User, UserPersonalData, UserProgress +from userApp.models import ( + User, + UserPersonalData, + UserProgress, + AccountActivationCode +) from userApp.forms import UserCreationForm, UserChangeForm @@ -42,3 +47,10 @@ class UserProfileAdmin(admin.ModelAdmin): @admin.register(UserProgress) class UserProgressAdmin(admin.ModelAdmin): pass + +@admin.register(AccountActivationCode) +class AccountActivationAdmin(admin.ModelAdmin): + list_display = ('code', 'user') + list_display_links = ('code',) + readonly_fields = ('code',) + fields = ('code', 'user') diff --git a/mlp_api/userApp/models.py b/mlp_api/userApp/models.py index d7be397..ab248a0 100644 --- a/mlp_api/userApp/models.py +++ b/mlp_api/userApp/models.py @@ -194,7 +194,7 @@ def __str__(self): class AccountActivationCode(models.Model): - code = models.TextField(_('account activation code')) + code = models.TextField(_('account activation code'), blank=True) date_of_creation = models.DateTimeField(auto_now_add=True, auto_now=False) user = models.OneToOneField( From 927110743810fb1a83fed503a326fd5a9c12f7f5 Mon Sep 17 00:00:00 2001 From: Elias Date: Sun, 19 May 2019 10:57:58 +0300 Subject: [PATCH 09/12] include/celery --- mlp_api/commonApp/__init__.py | 0 mlp_api/commonApp/admin.py | 3 + mlp_api/commonApp/apps.py | 5 + mlp_api/commonApp/models.py | 3 + .../{projectApp => commonApp}/permissions.py | 0 mlp_api/commonApp/tasks.py | 6 ++ mlp_api/commonApp/tests.py | 3 + mlp_api/commonApp/utils.py | 17 +++ mlp_api/{userApp => commonApp}/validators.py | 66 +++++++++++- mlp_api/commonApp/views.py | 3 + mlp_api/mlp_api/__init__.py | 3 + mlp_api/mlp_api/celery.py | 18 ++++ mlp_api/mlp_api/settings.py | 5 + mlp_api/projectApp/models.py | 50 ++++++--- mlp_api/projectApp/tasks.py | 24 +++++ mlp_api/projectApp/utils.py | 8 ++ mlp_api/projectApp/validators.py | 59 ---------- mlp_api/projectApp/views.py | 2 +- mlp_api/requirements.txt | 1 + mlp_api/userApp/models.py | 101 ++++++++---------- mlp_api/userApp/tasks.py | 53 +++++++++ mlp_api/userApp/utils.py | 11 -- 22 files changed, 296 insertions(+), 145 deletions(-) create mode 100644 mlp_api/commonApp/__init__.py create mode 100644 mlp_api/commonApp/admin.py create mode 100644 mlp_api/commonApp/apps.py create mode 100644 mlp_api/commonApp/models.py rename mlp_api/{projectApp => commonApp}/permissions.py (100%) create mode 100644 mlp_api/commonApp/tasks.py create mode 100644 mlp_api/commonApp/tests.py create mode 100644 mlp_api/commonApp/utils.py rename mlp_api/{userApp => commonApp}/validators.py (61%) create mode 100644 mlp_api/commonApp/views.py create mode 100644 mlp_api/mlp_api/celery.py create mode 100644 mlp_api/projectApp/tasks.py create mode 100644 mlp_api/projectApp/utils.py delete mode 100644 mlp_api/projectApp/validators.py create mode 100644 mlp_api/userApp/tasks.py diff --git a/mlp_api/commonApp/__init__.py b/mlp_api/commonApp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mlp_api/commonApp/admin.py b/mlp_api/commonApp/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/mlp_api/commonApp/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/mlp_api/commonApp/apps.py b/mlp_api/commonApp/apps.py new file mode 100644 index 0000000..63d7d44 --- /dev/null +++ b/mlp_api/commonApp/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CommonappConfig(AppConfig): + name = 'commonApp' diff --git a/mlp_api/commonApp/models.py b/mlp_api/commonApp/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/mlp_api/commonApp/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/mlp_api/projectApp/permissions.py b/mlp_api/commonApp/permissions.py similarity index 100% rename from mlp_api/projectApp/permissions.py rename to mlp_api/commonApp/permissions.py diff --git a/mlp_api/commonApp/tasks.py b/mlp_api/commonApp/tasks.py new file mode 100644 index 0000000..aa42741 --- /dev/null +++ b/mlp_api/commonApp/tasks.py @@ -0,0 +1,6 @@ +from celery import shared_task +from django.core.mail import send_mail + +@shared_task +def send_email(subject, message, from_email, to, **kwargs): + send_mail(subject, message, from_email, to, **kwargs) diff --git a/mlp_api/commonApp/tests.py b/mlp_api/commonApp/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/mlp_api/commonApp/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/mlp_api/commonApp/utils.py b/mlp_api/commonApp/utils.py new file mode 100644 index 0000000..668623f --- /dev/null +++ b/mlp_api/commonApp/utils.py @@ -0,0 +1,17 @@ +import os +import random +from string import ascii_letters, digits + +def generate_random_string(min_len, max_len, chars=ascii_letters + digits): + """ + Takes minimum and maximum length of output string and one optional argument + - sequence of characters, on which a new random string will be based. + """ + randrange = range(random.randrange(min_len, max_len)) + return ''.join(random.choice(chars) for _ in randrange) + +def remove_directory_files(directory): + """Removes all files in given directory.""" + paths = (os.path.join(directory, item) for item in os.listdir(directory)) + files = (path for path in paths if os.path.isfile(path)) + for file in files: os.remove(file) diff --git a/mlp_api/userApp/validators.py b/mlp_api/commonApp/validators.py similarity index 61% rename from mlp_api/userApp/validators.py rename to mlp_api/commonApp/validators.py index 7868f0b..762256a 100644 --- a/mlp_api/userApp/validators.py +++ b/mlp_api/commonApp/validators.py @@ -1,8 +1,70 @@ -from django.utils.deconstruct import deconstructible -from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ +from django.utils.deconstruct import deconstructible from PIL import Image +@deconstructible +class FileSizeValidator: + """ + Takes maximum size, allowed for a file, in bytes. + Creates validator for a FileField. + """ + message = _( + "Ensure that file size are less than %(max_size)s %(prefix)sBytes." + ) + code = 'limit_file_size' + + _decimal_prefixes = ('k', 'M', 'G') + + + def __init__(self, max_size, message=None, code=None): + self.max_size = max_size + if message is not None: + self.message = message + if code is not None: + self.code = code + + def __call__(self, value): + try: + size = value.size + except FileNotFoundError: + # In case if image was removed manually from user's directory: + raise ValidationError("File was removed from project's directory.") + + if size > self.max_size: + raise ValidationError( + self.message, + code=self.code, + params=self._get_pretty_size_params() + ) + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) and + self.max_size == other.max_size and + self.message == other.message and + self.code == other.code + ) + + def _get_pretty_size_params(self): + """ + Returns a dictionary with message parameters + (max_size, prefix) as keys, and values, depends on + maximum allowed bytes for a file. + """ + max_size = self.max_size + prefix = '' + + options_quantity = len(FileSizeValidator._decimal_prefixes) + for i in range(options_quantity, 0, -1): + base = 1024 ** i + if self.max_size >= base: + max_size /= base + prefix = FileSizeValidator._decimal_prefixes[i - 1] + break + + return {'max_size':round(max_size, 2), 'prefix':prefix} + @deconstructible class ImageSizeValidator: """ diff --git a/mlp_api/commonApp/views.py b/mlp_api/commonApp/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/mlp_api/commonApp/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/mlp_api/mlp_api/__init__.py b/mlp_api/mlp_api/__init__.py index e69de29..fb989c4 100644 --- a/mlp_api/mlp_api/__init__.py +++ b/mlp_api/mlp_api/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ('celery_app',) diff --git a/mlp_api/mlp_api/celery.py b/mlp_api/mlp_api/celery.py new file mode 100644 index 0000000..86d75ab --- /dev/null +++ b/mlp_api/mlp_api/celery.py @@ -0,0 +1,18 @@ +import os +from celery import Celery + +# set default environment variable to the 'celery' program: +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mlp_api.settings') + +# create celery app and set related module's name ('mlp_api') +app = Celery('mlp_api') + +# Configure celery app using django's settings +# 'namespace' argument needs for refference to celery's settings in settings.py +# for example: 'CELERY_BROKER_URL' for message broker +app.config_from_object('django.conf:settings', namespace='CELERY') + +# If we need specify task for some django app, we can create 'tasks.py' in app's +# directory, create there required task, and say to celery app find all +# these django-app-specified tasks automatically +app.autodiscover_tasks() diff --git a/mlp_api/mlp_api/settings.py b/mlp_api/mlp_api/settings.py index 30c6885..4edfd69 100644 --- a/mlp_api/mlp_api/settings.py +++ b/mlp_api/mlp_api/settings.py @@ -154,6 +154,11 @@ 'PAGE_SIZE': 10, } +# Celery configuration: +CELERY_TASK_SERIALIZER = 'pickle' +CELERY_RESULT_SERIALIZER = 'pickle' +CELERY_ACCEPT_CONTENT = ['json', 'pickle'] + # Include settings for develop # File named settings_dev.py diff --git a/mlp_api/projectApp/models.py b/mlp_api/projectApp/models.py index ef7c0c6..21818d5 100644 --- a/mlp_api/projectApp/models.py +++ b/mlp_api/projectApp/models.py @@ -1,3 +1,4 @@ +from io import BytesIO from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.validators import ( @@ -6,10 +7,10 @@ FileExtensionValidator ) from django.utils.text import slugify - from tinymce.models import HTMLField -from projectApp.validators import FileSizeValidator - +from projectApp.tasks import process_project_preview +from projectApp.utils import get_project_preview_path +from commonApp.validators import FileSizeValidator class Project(models.Model): """Represents project, published on the web-site.""" @@ -30,7 +31,7 @@ class Project(models.Model): ) preview = models.ImageField( _('preview'), - upload_to='projectApp/previews/', + upload_to=get_project_preview_path, validators=[ FileExtensionValidator(allowed_extensions=['jpg', 'jpeg']), FileSizeValidator(2.5 * 1024 * 1024) @@ -75,21 +76,44 @@ class Meta: verbose_name = _("enrolled project") verbose_name_plural = _("enrolled projects") + def _prepare_preview_update(self): + """ + Checks if there is some difference between present and old images, like: + old was replaced or removed, or old doesn't exist and a new was passed. + Returns dictionary with parameters for image processing task. + (dictionary will be empty if nothing of told above will happen). + """ + + old_qs = Project.objects.filter(pk=self.id) + old_preview = old_qs.first().preview if old_qs.exists() else None + preview_process_params = {} + if self.preview != old_preview: + if old_preview: + preview_process_params.update({'img_old_path': old_preview.name}) + if self.preview: + img_file = self.preview.file.file + # At the moment we don't need to save the whole uploaded image + # cause it will do asynchronous celery worker + # so let's put an empty byte stream instead actual image file: + self.preview.save(self.preview.name, BytesIO(), save=False) + preview_process_params.update({ + 'img_path': self.preview.name, + 'img_file': img_file + }) + + return preview_process_params + def save(self, *args, **kwargs): # Update slug field, if it's empty - write slugified title there: self.slug = self.slug.lower() if self.slug else slugify(self.title) - - # Check preview, if it has updated, remove old image: - try: - old_self = Project.objects.get(id=self.id) - if (not self.preview) or self.preview != old_self.preview: - old_self.preview.delete(save=False) - except: - # If it's a new project or a first preview, just don't do nothing: - pass + # Prepare preview update: + preview_process_params = self._prepare_preview_update() super().save(*args, **kwargs) + if preview_process_params: + process_project_preview.delay(**preview_process_params) + def __str__(self): return "'%s' project." % self.title diff --git a/mlp_api/projectApp/tasks.py b/mlp_api/projectApp/tasks.py new file mode 100644 index 0000000..9addf98 --- /dev/null +++ b/mlp_api/projectApp/tasks.py @@ -0,0 +1,24 @@ +import os +from celery import shared_task +from django.conf import settings +from django.utils.text import get_valid_filename +from PIL import Image +from commonApp.utils import remove_directory_files + +@shared_task +def process_project_preview(img_path=None, img_file=None, img_old_path=None): + """ + The task takes next arguments: + img_path, img_old_path - path, relative to MEDIA_ROOT, + to corresponding new and old preview images. + img_file - a new profile's preview image file, uploaded by user. + """ + if img_old_path is not None: + old_preview_abs_path = os.path.join(settings.MEDIA_ROOT, img_old_path) + try: + os.remove(old_preview_abs_path) + except FileNotFoundError: + pass + if img_path is not None and img_file is not None: + preview_abs_path = os.path.join(settings.MEDIA_ROOT, img_path) + Image.open(img_file).save(preview_abs_path, 'JPEG') diff --git a/mlp_api/projectApp/utils.py b/mlp_api/projectApp/utils.py new file mode 100644 index 0000000..23ecfa9 --- /dev/null +++ b/mlp_api/projectApp/utils.py @@ -0,0 +1,8 @@ +from os import path + +def get_project_preview_path(instance, filename): + """ + Takes an instance of Project model and name of preview's picture. + Returns a path to project's picture relative to MEDIA_ROOT. + """ + return path.join('projectApp', 'previews', instance.slug, filename) diff --git a/mlp_api/projectApp/validators.py b/mlp_api/projectApp/validators.py deleted file mode 100644 index a11a72e..0000000 --- a/mlp_api/projectApp/validators.py +++ /dev/null @@ -1,59 +0,0 @@ -from django.core.exceptions import ValidationError -from django.utils.translation import gettext_lazy as _ -from django.utils.deconstruct import deconstructible - -@deconstructible -class FileSizeValidator: - """ - Takes maximum size, allowed for a file, in bytes. - Creates validator for a FileField. - """ - message = _( - "Ensure that file size are less than %(max_size)s %(prefix)sBytes." - ) - code = 'limit_file_size' - - _decimal_prefixes = ('k', 'M', 'G') - - - def __init__(self, max_size, message=None, code=None): - self.max_size = max_size - if message is not None: - self.message = message - if code is not None: - self.code = code - - def __call__(self, value): - if value.size > self.max_size: - raise ValidationError( - self.message, - code=self.code, - params=self._get_pretty_size_params() - ) - - def __eq__(self, other): - return ( - isinstance(other, self.__class__) and - self.max_size == other.max_size and - self.message == other.message and - self.code == other.code - ) - - def _get_pretty_size_params(self): - """ - Returns a dictionary with message parameters - (max_size, prefix) as keys, and values, depends on - maximum allowed bytes for a file. - """ - max_size = self.max_size - prefix = '' - - options_quantity = len(FileSizeValidator._decimal_prefixes) - for i in range(options_quantity, 0, -1): - base = 1024 ** i - if self.max_size >= base: - max_size /= base - prefix = FileSizeValidator._decimal_prefixes[i - 1] - break - - return {'max_size':round(max_size, 2), 'prefix':prefix} diff --git a/mlp_api/projectApp/views.py b/mlp_api/projectApp/views.py index d2581d0..f6509c3 100644 --- a/mlp_api/projectApp/views.py +++ b/mlp_api/projectApp/views.py @@ -1,7 +1,7 @@ from rest_framework import viewsets from projectApp.models import Project from projectApp.serializers import ProjectSerializer -from projectApp.permissions import IsAdminUserOrReadOnly +from commonApp.permissions import IsAdminUserOrReadOnly class ProjectViewSet(viewsets.ModelViewSet): diff --git a/mlp_api/requirements.txt b/mlp_api/requirements.txt index 64a7757..f0194a0 100644 --- a/mlp_api/requirements.txt +++ b/mlp_api/requirements.txt @@ -15,3 +15,4 @@ pytz==2019.1 redis==3.2.1 sqlparse==0.3.0 text-unidecode==1.2 +celery==4.3.0 diff --git a/mlp_api/userApp/models.py b/mlp_api/userApp/models.py index 7e0bb5c..59f8e4e 100644 --- a/mlp_api/userApp/models.py +++ b/mlp_api/userApp/models.py @@ -4,18 +4,17 @@ from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.core.mail import send_mail from django.core.validators import MinLengthValidator, FileExtensionValidator from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin from django.conf import settings from PIL import Image from userApp.manager import UserManager -from userApp.utils import ( - get_userpic_path, - get_thumbnail_name, - generate_random_string -) -from userApp.validators import MinImageSizeValidator +from userApp.utils import get_userpic_path +from userApp.tasks import UserAvatarProcessing +from commonApp.tasks import send_email +from commonApp.validators import MinImageSizeValidator +from commonApp.utils import generate_random_string + class User(AbstractBaseUser, PermissionsMixin): login = models.CharField( @@ -57,7 +56,7 @@ def __str__(self): return self.login + ": " + self.email def email_user(self, subject, message, from_email=None, **kwargs): - send_mail(subject, message, from_email, [self.email], **kwargs) + send_email.delay(subject, message, from_email, [self.email], **kwargs) class UserPersonalData(models.Model): @@ -100,6 +99,11 @@ class UserPersonalData(models.Model): verbose_name=_('user\'s authorizing data') ) + process_avatar = UserAvatarProcessing.initialize( + avatar_size=(460, 460), + avatar_thumbnail_size=(150, 150) + ) + class Meta: verbose_name = _("personal data of a user") verbose_name_plural = _("personal data of users") @@ -108,62 +112,41 @@ def __str__(self): names = (self.first_name, self.last_name, self.auth_data.login) return "%s %s (%s)" % names - # Handling user's profile picture: - avatar_size = (460, 460) # size in pixels - avatar_thumbnail_size = (150, 150) # size in pixels - - def _get_avatar_dir(self): - """ Returns path to directory for user's avatar. """ - user_dir = "user_%s" % self.auth_data.id - return os.path.join(settings.MEDIA_ROOT, 'userApp', 'avatar', user_dir) - - def _make_avatar_from_field(self): - if not self.avatar: return None - - # Verify if there user picture's directory exists: - image_dir = self._get_avatar_dir() - if not os.path.exists(image_dir): - os.makedirs(image_dir) - - # Resizing given user's picture: - correct_image = Image.open(self.avatar.file).resize(self.avatar_size) - image_bytestream = io.BytesIO() - correct_image.save(image_bytestream, 'JPEG') - self.avatar.file = image_bytestream - - # Creating thumbnail: - correct_image.thumbnail(self.avatar_thumbnail_size) - thumbnail_name = get_thumbnail_name(self.avatar.name) - thumbnail_path = os.path.join(image_dir, thumbnail_name) - correct_image.save(thumbnail_path, 'JPEG') - - def _update_avatar(self): - # Check avatar, if it is updated or removed, delete old image: + def _prepare_avatar_update(self): + """ + Checks if there is some difference between present and old images, like: + old was replaced or removed, or old doesn't exist and a new was passed. + Returns dictionary with parameters for image processing task. + (dictionary will be empty if nothing of told above will happen). + """ + try: old_avatar = UserPersonalData.objects.get(id=self.id).avatar - # In case of changes of avatar: - if self.avatar != old_avatar: - # If old avatar is present, it should be removed: - if old_avatar: - # Remove avatar thumbnail: - thumbnail_path = os.path.join( - settings.MEDIA_ROOT, - get_thumbnail_name(old_avatar.name) - ) - os.remove(thumbnail_path) - # Remove avatar: - old_avatar.delete(save=False) - - # If there is a new avatar, it should be stored: - self._make_avatar_from_field() - - except (UserPersonalData.DoesNotExist, FileNotFoundError): - self._make_avatar_from_field() + except UserPersonalData.DoesNotExist: + old_avatar = None + + avatar_processing_params = {} + if self.avatar != old_avatar: + if old_avatar: + avatar_processing_params.update({'img_old_path': old_avatar.name}) + if self.avatar: + img_file = self.avatar.file.file + # At the moment we don't need to save the whole uploaded image + # cause it will do asynchronous celery worker + # so let's put an empty byte stream instead actual image file: + self.avatar.save(self.avatar.name, io.BytesIO(), save=False) + avatar_processing_params.update({ + 'img_path': self.avatar.name, + 'img_file': img_file + }) + + return avatar_processing_params def save(self, *args, **kwargs): - self._update_avatar() + avatar_update_params = self._prepare_avatar_update() super().save(*args, **kwargs) - + if avatar_update_params: + self.process_avatar.delay(**avatar_update_params) class UserProgress(models.Model): level = models.PositiveSmallIntegerField( diff --git a/mlp_api/userApp/tasks.py b/mlp_api/userApp/tasks.py new file mode 100644 index 0000000..0b479b8 --- /dev/null +++ b/mlp_api/userApp/tasks.py @@ -0,0 +1,53 @@ +import os +from celery import current_app +from django.conf import settings +from django.utils.text import get_valid_filename +from PIL import Image +from userApp.utils import get_thumbnail_name +from commonApp.utils import remove_directory_files + +class UserAvatarProcessing(current_app.Task): + """ + Class-based task for handling user's avatar download event. + Initializes with two arguments: avatar size and avatar's thumbnail size, + both arguments are tuples with two elements: width, height . + """ + name = 'userApp.tasks.process_user_avatar' + + def __init__(self, avatar_size, avatar_thumbnail_size): + self.avatar_size = avatar_size + self.avatar_thumbnail_size = avatar_thumbnail_size + + def _make_user_avatar(self, avatar, avatar_file): + # Resizing given user's picture: + correct_image = Image.open(avatar_file).resize(self.avatar_size) + correct_image.save(avatar, 'JPEG') + # Creating thumbnail: + thumbnail = get_thumbnail_name(avatar) + correct_image.thumbnail(self.avatar_thumbnail_size) + correct_image.save(thumbnail, 'JPEG') + + def run(self, img_path=None, img_file=None, img_old_path=None): + """ + The task takes next arguments: + path, old_path - path, relative to MEDIA_ROOT, + to corresponding new and old avatar images. + file - a new avatar image file, uploaded by user. + """ + + if img_old_path is not None: + old_avatar = os.path.join(settings.MEDIA_ROOT, img_old_path) + avar_dir = os.path.dirname(old_avatar) + if os.path.isdir(avar_dir): + remove_directory_files(avar_dir) + if img_path is not None and img_file is not None: + avatar = os.path.join(settings.MEDIA_ROOT, img_path) + self._make_user_avatar(avatar, img_file) + + @classmethod + def initialize(cls, avatar_size, avatar_thumbnail_size): + current_app.tasks.register(cls( + avatar_size=avatar_size, + avatar_thumbnail_size=avatar_thumbnail_size + )) + return current_app.tasks[cls.name] diff --git a/mlp_api/userApp/utils.py b/mlp_api/userApp/utils.py index 3172eb8..32c5cd4 100644 --- a/mlp_api/userApp/utils.py +++ b/mlp_api/userApp/utils.py @@ -1,8 +1,5 @@ from os import path import re -import random -from string import ascii_letters, digits - def get_userpic_path(instance, filename): """ @@ -18,11 +15,3 @@ def get_thumbnail_name(avatar_name): Return name of related thumbnail file. """ return re.sub(r'\.jpe?g$', lambda fnd: "_150x150"+fnd.group(0), avatar_name) - -def generate_random_string(min_len, max_len, chars=ascii_letters + digits): - """ - Takes minimum and maximum length of output string and one optional argument - - sequence of characters, on which a new random string will be based. - """ - randrange = range(random.randrange(min_len, max_len)) - return ''.join(random.choice(chars) for _ in randrange) From d649d98047ddcd330eb17519428e730e245db43e Mon Sep 17 00:00:00 2001 From: Elias Date: Tue, 21 May 2019 13:11:41 +0300 Subject: [PATCH 10/12] Include Celery(fixed requirements.txt) --- mlp_api/commonApp/admin.py | 3 --- mlp_api/commonApp/apps.py | 5 ----- mlp_api/commonApp/models.py | 3 --- mlp_api/commonApp/tests.py | 3 --- mlp_api/commonApp/utils.py | 17 ----------------- mlp_api/commonApp/views.py | 3 --- mlp_api/projectApp/models.py | 2 +- mlp_api/projectApp/tasks.py | 2 -- mlp_api/projectApp/views.py | 2 +- mlp_api/requirements.txt | 6 +++--- mlp_api/userApp/models.py | 10 +++++----- mlp_api/userApp/tasks.py | 2 +- mlp_api/{commonApp => utils}/__init__.py | 0 mlp_api/utils/filesystem.py | 7 +++++++ mlp_api/{commonApp => utils}/permissions.py | 0 mlp_api/{commonApp => utils}/tasks.py | 0 mlp_api/{commonApp => utils}/validators.py | 0 17 files changed, 18 insertions(+), 47 deletions(-) delete mode 100644 mlp_api/commonApp/admin.py delete mode 100644 mlp_api/commonApp/apps.py delete mode 100644 mlp_api/commonApp/models.py delete mode 100644 mlp_api/commonApp/tests.py delete mode 100644 mlp_api/commonApp/utils.py delete mode 100644 mlp_api/commonApp/views.py rename mlp_api/{commonApp => utils}/__init__.py (100%) create mode 100644 mlp_api/utils/filesystem.py rename mlp_api/{commonApp => utils}/permissions.py (100%) rename mlp_api/{commonApp => utils}/tasks.py (100%) rename mlp_api/{commonApp => utils}/validators.py (100%) diff --git a/mlp_api/commonApp/admin.py b/mlp_api/commonApp/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/mlp_api/commonApp/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/mlp_api/commonApp/apps.py b/mlp_api/commonApp/apps.py deleted file mode 100644 index 63d7d44..0000000 --- a/mlp_api/commonApp/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class CommonappConfig(AppConfig): - name = 'commonApp' diff --git a/mlp_api/commonApp/models.py b/mlp_api/commonApp/models.py deleted file mode 100644 index 71a8362..0000000 --- a/mlp_api/commonApp/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/mlp_api/commonApp/tests.py b/mlp_api/commonApp/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/mlp_api/commonApp/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/mlp_api/commonApp/utils.py b/mlp_api/commonApp/utils.py deleted file mode 100644 index 668623f..0000000 --- a/mlp_api/commonApp/utils.py +++ /dev/null @@ -1,17 +0,0 @@ -import os -import random -from string import ascii_letters, digits - -def generate_random_string(min_len, max_len, chars=ascii_letters + digits): - """ - Takes minimum and maximum length of output string and one optional argument - - sequence of characters, on which a new random string will be based. - """ - randrange = range(random.randrange(min_len, max_len)) - return ''.join(random.choice(chars) for _ in randrange) - -def remove_directory_files(directory): - """Removes all files in given directory.""" - paths = (os.path.join(directory, item) for item in os.listdir(directory)) - files = (path for path in paths if os.path.isfile(path)) - for file in files: os.remove(file) diff --git a/mlp_api/commonApp/views.py b/mlp_api/commonApp/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/mlp_api/commonApp/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/mlp_api/projectApp/models.py b/mlp_api/projectApp/models.py index 21818d5..4046657 100644 --- a/mlp_api/projectApp/models.py +++ b/mlp_api/projectApp/models.py @@ -10,7 +10,7 @@ from tinymce.models import HTMLField from projectApp.tasks import process_project_preview from projectApp.utils import get_project_preview_path -from commonApp.validators import FileSizeValidator +from utils.validators import FileSizeValidator class Project(models.Model): """Represents project, published on the web-site.""" diff --git a/mlp_api/projectApp/tasks.py b/mlp_api/projectApp/tasks.py index 9addf98..d49b3ed 100644 --- a/mlp_api/projectApp/tasks.py +++ b/mlp_api/projectApp/tasks.py @@ -1,9 +1,7 @@ import os from celery import shared_task from django.conf import settings -from django.utils.text import get_valid_filename from PIL import Image -from commonApp.utils import remove_directory_files @shared_task def process_project_preview(img_path=None, img_file=None, img_old_path=None): diff --git a/mlp_api/projectApp/views.py b/mlp_api/projectApp/views.py index f6509c3..709f416 100644 --- a/mlp_api/projectApp/views.py +++ b/mlp_api/projectApp/views.py @@ -1,7 +1,7 @@ from rest_framework import viewsets from projectApp.models import Project from projectApp.serializers import ProjectSerializer -from commonApp.permissions import IsAdminUserOrReadOnly +from utils.permissions import IsAdminUserOrReadOnly class ProjectViewSet(viewsets.ModelViewSet): diff --git a/mlp_api/requirements.txt b/mlp_api/requirements.txt index 3bb3dee..864dbb0 100644 --- a/mlp_api/requirements.txt +++ b/mlp_api/requirements.txt @@ -1,6 +1,6 @@ -amqp==1.4.9 +amqp==2.4.2 anyjson==0.3.3 -billiard==3.3.0.23 +billiard==3.6.0.0 celery==3.1.26.post2 certifi==2019.3.9 cffi==1.12.3 @@ -20,7 +20,7 @@ gunicorn==19.9.0 idna==2.8 itypes==1.1.0 Jinja2==2.10.1 -kombu==3.0.37 +kombu==4.5.0 MarkupSafe==1.1.1 openapi-codec==1.3.2 Pillow==6.0.0 diff --git a/mlp_api/userApp/models.py b/mlp_api/userApp/models.py index bb0de64..6dbc175 100644 --- a/mlp_api/userApp/models.py +++ b/mlp_api/userApp/models.py @@ -4,6 +4,7 @@ from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from django.utils.crypto import get_random_string from django.core.validators import MinLengthValidator, FileExtensionValidator from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin from django.conf import settings @@ -11,9 +12,8 @@ from userApp.manager import UserManager from userApp.utils import get_userpic_path from userApp.tasks import UserAvatarProcessing -from commonApp.tasks import send_email -from commonApp.validators import MinImageSizeValidator -from commonApp.utils import generate_random_string +from utils.tasks import send_email +from utils.validators import MinImageSizeValidator class User(AbstractBaseUser, PermissionsMixin): @@ -217,7 +217,7 @@ def get_actuals(cls): return cls.objects.filter(date_of_creation__gte=time_floor) def __str__(self): - return self.code + return self.code30 def _generate_link(self): """ Returns reliable account activation link. """ @@ -240,6 +240,6 @@ def notificate_user(self): ) def save(self, *args, **kwargs): - self.code = generate_random_string(30, 40) + self.code = get_random_string(length=30) self.notificate_user() super().save(*args, **kwargs) diff --git a/mlp_api/userApp/tasks.py b/mlp_api/userApp/tasks.py index 0b479b8..f12385f 100644 --- a/mlp_api/userApp/tasks.py +++ b/mlp_api/userApp/tasks.py @@ -4,7 +4,7 @@ from django.utils.text import get_valid_filename from PIL import Image from userApp.utils import get_thumbnail_name -from commonApp.utils import remove_directory_files +from utils.filesystem import remove_directory_files class UserAvatarProcessing(current_app.Task): """ diff --git a/mlp_api/commonApp/__init__.py b/mlp_api/utils/__init__.py similarity index 100% rename from mlp_api/commonApp/__init__.py rename to mlp_api/utils/__init__.py diff --git a/mlp_api/utils/filesystem.py b/mlp_api/utils/filesystem.py new file mode 100644 index 0000000..f38fddc --- /dev/null +++ b/mlp_api/utils/filesystem.py @@ -0,0 +1,7 @@ +import os + +def remove_directory_files(directory): + """Removes all files in given directory.""" + paths = (os.path.join(directory, item) for item in os.listdir(directory)) + files = (path for path in paths if os.path.isfile(path)) + for file in files: os.remove(file) diff --git a/mlp_api/commonApp/permissions.py b/mlp_api/utils/permissions.py similarity index 100% rename from mlp_api/commonApp/permissions.py rename to mlp_api/utils/permissions.py diff --git a/mlp_api/commonApp/tasks.py b/mlp_api/utils/tasks.py similarity index 100% rename from mlp_api/commonApp/tasks.py rename to mlp_api/utils/tasks.py diff --git a/mlp_api/commonApp/validators.py b/mlp_api/utils/validators.py similarity index 100% rename from mlp_api/commonApp/validators.py rename to mlp_api/utils/validators.py From c6f9d9954cbb45882bb586636ea515170166f206 Mon Sep 17 00:00:00 2001 From: Elias Date: Tue, 21 May 2019 13:18:21 +0300 Subject: [PATCH 11/12] Include Celery(fixed requirements.txt) --- mlp_api/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlp_api/requirements.txt b/mlp_api/requirements.txt index 864dbb0..9dd6f38 100644 --- a/mlp_api/requirements.txt +++ b/mlp_api/requirements.txt @@ -1,7 +1,7 @@ amqp==2.4.2 anyjson==0.3.3 billiard==3.6.0.0 -celery==3.1.26.post2 +celery==4.3.0 certifi==2019.3.9 cffi==1.12.3 chardet==3.0.4 From 8b7460896b2bb00d1969792abde5efa990d17d49 Mon Sep 17 00:00:00 2001 From: Elias Date: Tue, 21 May 2019 13:25:03 +0300 Subject: [PATCH 12/12] Include Celery(fixed requirements.txt) --- mlp_api/userApp/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlp_api/userApp/models.py b/mlp_api/userApp/models.py index 6dbc175..2f08e3b 100644 --- a/mlp_api/userApp/models.py +++ b/mlp_api/userApp/models.py @@ -217,7 +217,7 @@ def get_actuals(cls): return cls.objects.filter(date_of_creation__gte=time_floor) def __str__(self): - return self.code30 + return self.code def _generate_link(self): """ Returns reliable account activation link. """