Skip to content

Commit 54ace15

Browse files
Merge pull request #10 from rockstarr-programmerr/dev
Version 1.0.0
2 parents c27994e + 795ba9b commit 54ace15

File tree

79 files changed

+2525
-8
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+2525
-8
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ media/
77
.coverage
88
htmlcov/
99
temp/
10+
temp.*
1011
*.pid
1112
*.log

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ python manage.py runserver
3333

3434
### Debug
3535
#### For Visual studio code
36-
Just press F5 (Config is inside `.vscode/`)
36+
Just press F5 (Config is inside `keep-learning.code-workspace`)
3737

3838
#### For other editors
3939
Please contribute if you know how.
@@ -62,3 +62,37 @@ Go to this URL in your brower. (You may need to register new user and then login
6262
```
6363
http://localhost:8000/
6464
```
65+
66+
### Setup background tasks
67+
**NOTE** This is only needed if you develop features involving background tasks, like sending email, .etc
68+
69+
#### Rabbitmq
70+
##### Setup with docker
71+
```
72+
docker run -d --name rabbitmq -p 5672:5672 rabbitmq
73+
```
74+
75+
##### Create user and vhost
76+
Exec into container
77+
```
78+
docker exec -it rabbitmq bash
79+
```
80+
81+
Create user, vhost
82+
```
83+
rabbitmqctl add_user kl_user kl_password
84+
rabbitmqctl add_vhost kl_vhost
85+
rabbitmqctl set_permissions -p kl_vhost kl_user ".*" ".*" ".*"
86+
```
87+
88+
#### Celery
89+
##### Start the worker process
90+
```
91+
celery --app keep_learning worker --loglevel INFO --pool solo
92+
```
93+
94+
**NOTE** Every time you make changes to a task, celery worker should be restarted.
95+
96+
### Sending emails
97+
In development, emails are not actually sent, but instead saved to `temp/sent_emails`.
98+
You can inspect this directory to test email sending features.

account/business/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .teacher import register_teacher

account/business/reset_password.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from django.conf import settings
2+
from django.contrib.auth import get_user_model
3+
from django.contrib.auth.tokens import PasswordResetTokenGenerator
4+
from django.core.exceptions import ValidationError
5+
from django.utils.encoding import force_bytes
6+
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
7+
8+
from account.serializers import EmailResetPasswordLinkTaskSerializer
9+
from account.tasks import send_email_reset_password_link_task
10+
from keep_learning.utils.url import update_url_params
11+
12+
User = get_user_model()
13+
14+
15+
class ResetPasswordTokenInvalid(Exception):
16+
pass
17+
18+
19+
class ResetPasswordBusiness:
20+
token_generator = PasswordResetTokenGenerator()
21+
22+
def __init__(self, user):
23+
self.user = user
24+
25+
def send_email(self):
26+
url = self.get_link()
27+
serializer = EmailResetPasswordLinkTaskSerializer(instance=self.user)
28+
send_email_reset_password_link_task.delay(serializer.data, url)
29+
30+
def reset_password(self, password, token):
31+
self.check_token(token)
32+
self.user.set_password(password)
33+
self.user.save()
34+
35+
def get_link(self):
36+
token = self.token_generator.make_token(self.user)
37+
uid = urlsafe_base64_encode(force_bytes(self.user.pk))
38+
url = update_url_params(settings.WEB_RESET_PASSWORD_URL, {'token': token, 'uid': uid})
39+
return url
40+
41+
def check_token(self, token):
42+
is_valid = self.token_generator.check_token(self.user, token)
43+
if not is_valid:
44+
raise ResetPasswordTokenInvalid
45+
46+
@staticmethod
47+
def get_user_by_email(email):
48+
return User.objects.filter(email=email).first()
49+
50+
@staticmethod
51+
def get_user_by_uid(uid):
52+
try:
53+
uid = urlsafe_base64_decode(uid).decode()
54+
user = User.objects.get(pk=uid)
55+
except (TypeError, ValueError, OverflowError, User.DoesNotExist, ValidationError):
56+
user = None
57+
return user

account/business/teacher.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.contrib.auth import get_user_model
2+
3+
User = get_user_model()
4+
5+
6+
def register_teacher(data):
7+
email = data.pop('email')
8+
password = data.pop('password')
9+
teacher = User.objects.create_teacher(email=email, password=password, **data)
10+
return teacher

account/managers.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from django.contrib.auth.models import UserManager
2+
from django.db import models
3+
from django.utils.translation import gettext_lazy as _
4+
5+
6+
def get_first_part_of_email(email):
7+
return email.split('@')[0]
8+
9+
10+
class UserTypes(models.TextChoices):
11+
TEACHER = 'teacher', _('Teacher')
12+
STUDENT = 'student', _('Student')
13+
ADMIN = 'admin', _('Admin')
14+
15+
16+
class CustomUserManager(UserManager):
17+
"""
18+
Customize to allow creating user with just email, no need username
19+
"""
20+
def create_user(self, username=None, email=None, password=None, **extra_fields):
21+
assert bool(email), 'Email is required for creating user.'
22+
username = get_first_part_of_email(email)
23+
24+
if not 'name' in extra_fields:
25+
extra_fields['name'] = get_first_part_of_email(email)
26+
27+
return super().create_user(username, email=email, password=password, **extra_fields)
28+
29+
def create_superuser(self, username=None, email=None, password=None, **extra_fields):
30+
assert bool(email), 'Email is required for creating user.'
31+
username = get_first_part_of_email(email)
32+
33+
if not 'name' in extra_fields:
34+
extra_fields['name'] = get_first_part_of_email(email)
35+
if not 'user_type' in extra_fields:
36+
user_type = UserTypes.ADMIN
37+
extra_fields['user_type'] = user_type
38+
39+
return super().create_superuser(username, email=email, password=password, **extra_fields)
40+
41+
def create_teacher(self, email=None, password=None, **extra_fields):
42+
extra_fields['user_type'] = UserTypes.TEACHER
43+
return self.create_user(email=email, password=password, **extra_fields)
44+
45+
def create_student(self, email=None, password=None, **extra_fields):
46+
extra_fields['user_type'] = UserTypes.STUDENT
47+
return self.create_user(email=email, password=password, **extra_fields)
48+
49+
50+
class TeacherManager(UserManager):
51+
def get_queryset(self):
52+
return super().get_queryset().filter(user_type=UserTypes.TEACHER)
53+
54+
55+
class StudentManager(UserManager):
56+
def get_queryset(self):
57+
return super().get_queryset().filter(user_type=UserTypes.STUDENT)

account/migrations/0001_initial.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Generated by Django 3.2.7 on 2021-10-02 15:35
2+
3+
import account.managers
4+
import django.core.validators
5+
from django.db import migrations, models
6+
import django.utils.timezone
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
initial = True
12+
13+
dependencies = [
14+
('auth', '0012_alter_user_first_name_max_length'),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='User',
20+
fields=[
21+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22+
('password', models.CharField(max_length=128, verbose_name='password')),
23+
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
24+
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
25+
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
26+
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
27+
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
28+
('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')),
29+
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
30+
('email', models.EmailField(error_messages={'unique': 'A user with that email already exists.'}, max_length=254, unique=True)),
31+
('name', models.CharField(max_length=150)),
32+
('phone_number', models.CharField(blank=True, max_length=20)),
33+
('avatar', models.ImageField(blank=True, upload_to='users/avatar/%Y/%m', validators=[django.core.validators.validate_image_file_extension])),
34+
('avatar_thumbnail', models.ImageField(blank=True, upload_to='users/avatar_thumbnail/%Y/%m', validators=[django.core.validators.validate_image_file_extension])),
35+
('username', models.CharField(blank=True, max_length=150)),
36+
('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')),
37+
('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')),
38+
],
39+
options={
40+
'verbose_name': 'user',
41+
'verbose_name_plural': 'users',
42+
'abstract': False,
43+
},
44+
managers=[
45+
('objects', account.managers.CustomUserManager()),
46+
],
47+
),
48+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 3.2.7 on 2021-10-02 15:47
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('account', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='user',
15+
name='user_type',
16+
field=models.CharField(choices=[('teacher', 'teacher'), ('student', 'student'), ('admin', 'admin')], default='admin', max_length=20),
17+
preserve_default=False,
18+
),
19+
]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 3.2.7 on 2021-10-04 14:17
2+
3+
import account.managers
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('account', '0002_user_user_type'),
11+
]
12+
13+
operations = [
14+
migrations.AlterModelManagers(
15+
name='user',
16+
managers=[
17+
('objects', account.managers.CustomUserManager()),
18+
('teachers', account.managers.TeacherManager()),
19+
('students', account.managers.StudentManager()),
20+
],
21+
),
22+
migrations.AlterField(
23+
model_name='user',
24+
name='user_type',
25+
field=models.CharField(choices=[('teacher', 'Teacher'), ('student', 'Student'), ('admin', 'Admin')], max_length=20),
26+
),
27+
]

account/models.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,81 @@
1+
from django.contrib.auth.models import AbstractUser
2+
from django.core.validators import validate_image_file_extension
13
from django.db import models
4+
from django.utils.translation import gettext_lazy as _
5+
from PIL import Image
26

3-
# Create your models here.
7+
from account.managers import (CustomUserManager, StudentManager,
8+
TeacherManager, UserTypes)
9+
10+
AVATAR_WIDTH = 256
11+
AVATAR_HEIGHT = 256
12+
AVATAR_THUMBNAIL_WIDTH = 64
13+
AVATAR_THUMBNAIL_HEIGHT = 64
14+
15+
16+
class User(AbstractUser):
17+
Types = UserTypes
18+
19+
email = models.EmailField(
20+
unique=True,
21+
error_messages={
22+
'unique': _('A user with that email already exists.'),
23+
},
24+
)
25+
name = models.CharField(
26+
max_length=150,
27+
)
28+
phone_number = models.CharField(
29+
max_length=20,
30+
blank=True,
31+
)
32+
user_type = models.CharField(
33+
max_length=20,
34+
choices=Types.choices,
35+
)
36+
avatar = models.ImageField(
37+
upload_to='users/avatar/%Y/%m',
38+
blank=True,
39+
validators=[validate_image_file_extension],
40+
)
41+
avatar_thumbnail = models.ImageField(
42+
upload_to='users/avatar_thumbnail/%Y/%m',
43+
blank=True,
44+
validators=[validate_image_file_extension],
45+
)
46+
username = models.CharField(
47+
max_length=150,
48+
blank=True,
49+
)
50+
51+
# NOTE: Order of managers is important, first manager is considered "default" by Django.
52+
objects = CustomUserManager()
53+
teachers = TeacherManager()
54+
students = StudentManager()
55+
56+
USERNAME_FIELD = 'email'
57+
REQUIRED_FIELDS = []
58+
59+
def __str__(self):
60+
return f'{self.name} ({self.email}) - {self.get_user_type_display()}'
61+
62+
def save(self, *args, **kwargs):
63+
super().save(*args, **kwargs)
64+
self._make_thumbnail(self.avatar, AVATAR_WIDTH, AVATAR_HEIGHT)
65+
self._make_thumbnail(self.avatar_thumbnail, AVATAR_THUMBNAIL_WIDTH, AVATAR_THUMBNAIL_HEIGHT)
66+
67+
@staticmethod
68+
def _make_thumbnail(image, width, height):
69+
if image and (
70+
image.width > width or
71+
image.height > height
72+
):
73+
with Image.open(image.path) as f:
74+
f.thumbnail((width, height))
75+
f.save(image.path)
76+
77+
def is_teacher(self):
78+
return self.user_type == self.Types.TEACHER
79+
80+
def is_student(self):
81+
return self.user_type == self.Types.STUDENT

0 commit comments

Comments
 (0)