Clean up docker-compose and run Black formatter over entire codebase

This commit is contained in:
Keannu Christian Bernasol 2024-10-30 22:09:58 +08:00
parent 6c232b3e89
commit 069aba80b1
60 changed files with 1946 additions and 1485 deletions

View file

@ -6,11 +6,13 @@ from .models import CustomUser
class CustomUserAdmin(UserAdmin):
model = CustomUser
list_display = ('id', 'is_active', 'user_group',) + UserAdmin.list_display
list_display = (
"id",
"is_active",
"user_group",
) + UserAdmin.list_display
# Editable fields per instance
fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('avatar',)}),
)
fieldsets = UserAdmin.fieldsets + ((None, {"fields": ("avatar",)}),)
admin.site.register(CustomUser, CustomUserAdmin)

View file

@ -2,8 +2,8 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
default_auto_field = "django.db.models.BigAutoField"
name = "accounts"
def ready(self):
import accounts.signals

View file

@ -13,38 +13,145 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('user_groups', '0001_initial'),
("auth", "0012_alter_user_first_name_max_length"),
("user_groups", "0001_initial"),
]
operations = [
migrations.CreateModel(
name='CustomUser',
name="CustomUser",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('avatar', django_resized.forms.ResizedImageField(crop=None, force_format='WEBP', keep_meta=True, null=True, quality=100, scale=None, size=[1920, 1080], upload_to='avatars/')),
('onboarding', models.BooleanField(default=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='user_groups.usergroup')),
('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')),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="email address"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
(
"avatar",
django_resized.forms.ResizedImageField(
crop=None,
force_format="WEBP",
keep_meta=True,
null=True,
quality=100,
scale=None,
size=[1920, 1080],
upload_to="avatars/",
),
),
("onboarding", models.BooleanField(default=True)),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
(
"user_group",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="user_groups.usergroup",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
("objects", django.contrib.auth.models.UserManager()),
],
),
]

View file

@ -15,14 +15,16 @@ class CustomUser(AbstractUser):
# is_admin inherited from base user class
avatar = ResizedImageField(
null=True, force_format="WEBP", quality=100, upload_to='avatars/')
null=True, force_format="WEBP", quality=100, upload_to="avatars/"
)
# Used for onboarding processes
# Set this to False later on once the user makes actions
onboarding = models.BooleanField(default=True)
user_group = models.ForeignKey(
'user_groups.UserGroup', on_delete=models.SET_NULL, null=True)
"user_groups.UserGroup", on_delete=models.SET_NULL, null=True
)
@property
def group_member(self):
@ -57,4 +59,4 @@ class CustomUser(AbstractUser):
@property
def admin_url(self):
return reverse('admin:users_customuser_change', args=(self.pk,))
return reverse("admin:users_customuser_change", args=(self.pk,))

View file

@ -8,13 +8,14 @@ from django.core.cache import cache
from django.core import exceptions as django_exceptions
from rest_framework.settings import api_settings
from django.contrib.auth.password_validation import validate_password
# There can be multiple subject instances with the same name, only differing in course, year level, and semester. We filter them here
class SimpleCustomUserSerializer(ModelSerializer):
class Meta(BaseUserSerializer.Meta):
model = CustomUser
fields = ('id', 'username', 'email', 'full_name')
fields = ("id", "username", "email", "full_name")
class CustomUserSerializer(BaseUserSerializer):
@ -22,19 +23,36 @@ class CustomUserSerializer(BaseUserSerializer):
class Meta(BaseUserSerializer.Meta):
model = CustomUser
fields = ('id', 'username', 'email', 'avatar', 'first_name',
'last_name', 'user_group', 'group_member', 'group_owner')
read_only_fields = ('id', 'username', 'email', 'user_group',
'group_member', 'group_owner')
fields = (
"id",
"username",
"email",
"avatar",
"first_name",
"is_new",
"last_name",
"user_group",
"group_member",
"group_owner",
)
read_only_fields = (
"id",
"username",
"email",
"user_group",
"group_member",
"group_owner",
)
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['user_group'] = SimpleUserGroupSerializer(
instance.user_group, many=False).data
representation["user_group"] = SimpleUserGroupSerializer(
instance.user_group, many=False
).data
return representation
def update(self, instance, validated_data):
cache.delete(f'user:{instance.id}')
cache.delete(f"user:{instance.id}")
return super().update(instance, validated_data)
@ -42,16 +60,18 @@ class UserRegistrationSerializer(serializers.ModelSerializer):
email = serializers.EmailField(required=True)
username = serializers.CharField(required=True)
password = serializers.CharField(
write_only=True, style={'input_type': 'password', 'placeholder': 'Password'})
write_only=True, style={"input_type": "password", "placeholder": "Password"}
)
first_name = serializers.CharField(
required=True, allow_blank=False, allow_null=False)
required=True, allow_blank=False, allow_null=False
)
last_name = serializers.CharField(
required=True, allow_blank=False, allow_null=False)
required=True, allow_blank=False, allow_null=False
)
class Meta:
model = CustomUser
fields = ['email', 'username', 'password',
'first_name', 'last_name']
fields = ["email", "username", "password", "first_name", "last_name"]
def validate(self, attrs):
user_attrs = attrs.copy()
@ -69,14 +89,15 @@ class UserRegistrationSerializer(serializers.ModelSerializer):
raise serializers.ValidationError({"password": errors})
if self.Meta.model.objects.filter(username=attrs.get("username")).exists():
raise serializers.ValidationError(
"A user with that username already exists.")
"A user with that username already exists."
)
return super().validate(attrs)
def create(self, validated_data):
user = self.Meta.model(**validated_data)
user.username = validated_data['username']
user.username = validated_data["username"]
user.is_active = False
user.set_password(validated_data['password'])
user.set_password(validated_data["password"])
user.save()
return user

View file

@ -12,38 +12,37 @@ import json
@receiver(post_migrate)
def create_users(sender, **kwargs):
if sender.name == "accounts":
with open(os.path.join(ROOT_DIR, 'seed_data.json'), "r") as f:
with open(os.path.join(ROOT_DIR, "seed_data.json"), "r") as f:
seed_data = json.loads(f.read())
for user in seed_data['users']:
USER = CustomUser.objects.filter(
email=user['email']).first()
for user in seed_data["users"]:
USER = CustomUser.objects.filter(email=user["email"]).first()
if not USER:
if user['password'] == 'USE_REGULAR':
password = get_secret('SEED_DATA_PASSWORD')
elif user['password'] == 'USE_ADMIN':
password = get_secret('SEED_DATA_ADMIN_PASSWORD')
if user["password"] == "USE_REGULAR":
password = get_secret("SEED_DATA_PASSWORD")
elif user["password"] == "USE_ADMIN":
password = get_secret("SEED_DATA_ADMIN_PASSWORD")
else:
password = user['password']
if (user['is_superuser'] == True):
password = user["password"]
if user["is_superuser"] == True:
# Admin users are created regardless of SEED_DATA value
USER = CustomUser.objects.create_superuser(
username=user['username'],
email=user['email'],
username=user["username"],
email=user["email"],
password=password,
)
print('Created Superuser:', user['email'])
print("Created Superuser:", user["email"])
else:
# Only create non-admin users if SEED_DATA=True
if SEED_DATA:
USER = CustomUser.objects.create_user(
username=user['email'],
email=user['email'],
username=user["email"],
email=user["email"],
password=password,
)
print('Created User:', user['email'])
print("Created User:", user["email"])
USER.first_name = user['first_name']
USER.last_name = user['last_name']
USER.first_name = user["first_name"]
USER.last_name = user["last_name"]
USER.is_active = True
USER.save()
@ -51,53 +50,57 @@ def create_users(sender, **kwargs):
@receiver(post_migrate)
def create_celery_beat_schedules(sender, **kwargs):
if sender.name == "django_celery_beat":
with open(os.path.join(ROOT_DIR, 'seed_data.json'), "r") as f:
with open(os.path.join(ROOT_DIR, "seed_data.json"), "r") as f:
seed_data = json.loads(f.read())
# Creating Schedules
for schedule in seed_data['schedules']:
if schedule['type'] == 'crontab':
for schedule in seed_data["schedules"]:
if schedule["type"] == "crontab":
# Check if Schedule already exists
SCHEDULE = CrontabSchedule.objects.filter(minute=schedule['minute'],
hour=schedule['hour'],
day_of_week=schedule['day_of_week'],
day_of_month=schedule['day_of_month'],
month_of_year=schedule['month_of_year'],
timezone=schedule['timezone']
).first()
SCHEDULE = CrontabSchedule.objects.filter(
minute=schedule["minute"],
hour=schedule["hour"],
day_of_week=schedule["day_of_week"],
day_of_month=schedule["day_of_month"],
month_of_year=schedule["month_of_year"],
timezone=schedule["timezone"],
).first()
# If it does not exist, create a new Schedule
if not SCHEDULE:
SCHEDULE = CrontabSchedule.objects.create(
minute=schedule['minute'],
hour=schedule['hour'],
day_of_week=schedule['day_of_week'],
day_of_month=schedule['day_of_month'],
month_of_year=schedule['month_of_year'],
timezone=schedule['timezone']
minute=schedule["minute"],
hour=schedule["hour"],
day_of_week=schedule["day_of_week"],
day_of_month=schedule["day_of_month"],
month_of_year=schedule["month_of_year"],
timezone=schedule["timezone"],
)
print(
f'Created Crontab Schedule for Hour:{SCHEDULE.hour},Minute:{SCHEDULE.minute}')
f"Created Crontab Schedule for Hour:{SCHEDULE.hour},Minute:{SCHEDULE.minute}"
)
else:
print(
f'Crontab Schedule for Hour:{SCHEDULE.hour},Minute:{SCHEDULE.minute} already exists')
for task in seed_data['scheduled_tasks']:
TASK = PeriodicTask.objects.filter(name=task['name']).first()
f"Crontab Schedule for Hour:{SCHEDULE.hour},Minute:{SCHEDULE.minute} already exists"
)
for task in seed_data["scheduled_tasks"]:
TASK = PeriodicTask.objects.filter(name=task["name"]).first()
if not TASK:
if task['schedule']['type'] == 'crontab':
SCHEDULE = CrontabSchedule.objects.filter(minute=task['schedule']['minute'],
hour=task['schedule']['hour'],
day_of_week=task['schedule']['day_of_week'],
day_of_month=task['schedule']['day_of_month'],
month_of_year=task['schedule']['month_of_year'],
timezone=task['schedule']['timezone']
).first()
if task["schedule"]["type"] == "crontab":
SCHEDULE = CrontabSchedule.objects.filter(
minute=task["schedule"]["minute"],
hour=task["schedule"]["hour"],
day_of_week=task["schedule"]["day_of_week"],
day_of_month=task["schedule"]["day_of_month"],
month_of_year=task["schedule"]["month_of_year"],
timezone=task["schedule"]["timezone"],
).first()
TASK = PeriodicTask.objects.create(
crontab=SCHEDULE,
name=task['name'],
task=task['task'],
enabled=task['enabled']
name=task["name"],
task=task["task"],
enabled=task["enabled"],
)
print(f'Created Periodic Task: {TASK.name}')
print(f"Created Periodic Task: {TASK.name}")
else:
raise Exception('Schedule for Periodic Task not found')
raise Exception("Schedule for Periodic Task not found")
else:
print(f'Periodic Task: {TASK.name} already exists')
print(f"Periodic Task: {TASK.name} already exists")

View file

@ -4,20 +4,26 @@ from celery import shared_task
@shared_task
def get_paying_users():
from subscriptions.models import UserSubscription
# Get a list of user subscriptions
active_subscriptions = UserSubscription.objects.filter(
valid=True).distinct('user')
active_subscriptions = UserSubscription.objects.filter(valid=True).distinct("user")
# Get paying users
active_users = []
# Paying regular users
active_users += [
subscription.user.id for subscription in active_subscriptions if subscription.user is not None and subscription.user.user_group is None]
subscription.user.id
for subscription in active_subscriptions
if subscription.user is not None and subscription.user.user_group is None
]
# Paying users within groups
active_users += [
subscription.user_group.members for subscription in active_subscriptions if subscription.user_group is not None and subscription.user is None]
subscription.user_group.members
for subscription in active_subscriptions
if subscription.user_group is not None and subscription.user is None
]
# Return paying users
return active_users

View file

@ -3,10 +3,10 @@ from rest_framework.routers import DefaultRouter
from accounts import views
router = DefaultRouter()
router.register(r'users', views.CustomUserViewSet, basename='users')
router.register(r"users", views.CustomUserViewSet, basename="users")
urlpatterns = [
path('', include(router.urls)),
path('', include('djoser.urls')),
path('', include('djoser.urls.jwt')),
path("", include(router.urls)),
path("", include("djoser.urls")),
path("", include("djoser.urls.jwt")),
]

View file

@ -1,4 +1,3 @@
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
import re
@ -6,9 +5,10 @@ import re
class UppercaseValidator(object):
def validate(self, password, user=None):
if not re.findall('[A-Z]', password):
if not re.findall("[A-Z]", password):
raise ValidationError(
_("The password must contain at least 1 uppercase letter (A-Z)."))
_("The password must contain at least 1 uppercase letter (A-Z).")
)
def get_help_text(self):
return _("Your password must contain at least 1 uppercase letter (A-Z).")
@ -16,9 +16,10 @@ class UppercaseValidator(object):
class LowercaseValidator(object):
def validate(self, password, user=None):
if not re.findall('[a-z]', password):
if not re.findall("[a-z]", password):
raise ValidationError(
_("The password must contain at least 1 lowercase letter (a-z)."))
_("The password must contain at least 1 lowercase letter (a-z).")
)
def get_help_text(self):
return _("Your password must contain at least 1 lowercase letter (a-z).")
@ -26,19 +27,25 @@ class LowercaseValidator(object):
class SpecialCharacterValidator(object):
def validate(self, password, user=None):
if not re.findall('[@#$%^&*()_+/\<>;:!?]', password):
if not re.findall("[@#$%^&*()_+/\<>;:!?]", password):
raise ValidationError(
_("The password must contain at least 1 special character (@, #, $, etc.)."))
_(
"The password must contain at least 1 special character (@, #, $, etc.)."
)
)
def get_help_text(self):
return _("Your password must contain at least 1 special character (@, #, $, etc.).")
return _(
"Your password must contain at least 1 special character (@, #, $, etc.)."
)
class NumberValidator(object):
def validate(self, password, user=None):
if not any(char.isdigit() for char in password):
raise ValidationError(
_("The password must contain at least one numerical digit (0-9)."))
_("The password must contain at least one numerical digit (0-9).")
)
def get_help_text(self):
return _("Your password must contain at least numerical digit (0-9).")

View file

@ -22,28 +22,27 @@ class CustomUserViewSet(DjoserUserViewSet):
user = self.request.user
# If user is admin, show all active users
if user.is_superuser:
key = 'users'
key = "users"
# Get cache
queryset = cache.get(key)
# Set cache if stale or does not exist
if not queryset:
queryset = CustomUser.objects.filter(is_active=True)
cache.set(key, queryset, 60*60)
cache.set(key, queryset, 60 * 60)
return queryset
elif not user.user_group:
key = f'user:{user.id}'
key = f"user:{user.id}"
queryset = cache.get(key)
if not queryset:
queryset = CustomUser.objects.filter(is_active=True)
cache.set(key, queryset, 60*60)
cache.set(key, queryset, 60 * 60)
return queryset
elif user.user_group:
key = f'usergroup_users:{user.user_group.id}'
key = f"usergroup_users:{user.user_group.id}"
queryset = cache.get(key)
if not queryset:
queryset = CustomUser.objects.filter(
user_group=user.user_group)
cache.set(key, queryset, 60*60)
queryset = CustomUser.objects.filter(user_group=user.user_group)
cache.set(key, queryset, 60 * 60)
return queryset
else:
return CustomUser.objects.none()
@ -52,10 +51,10 @@ class CustomUserViewSet(DjoserUserViewSet):
user = self.request.user
# Clear cache
cache.delete(f'users')
cache.delete(f'user:{user.id}')
cache.delete(f"users")
cache.delete(f"user:{user.id}")
if user.user_group:
cache.delete(f'usergroup_users:{user.user_group.id}')
cache.delete(f"usergroup_users:{user.user_group.id}")
super().perform_update(serializer, *args, **kwargs)
user = serializer.instance
@ -84,16 +83,18 @@ class CustomUserViewSet(DjoserUserViewSet):
settings.EMAIL.confirmation(self.request, context).send(to)
# Clear cache
cache.delete('users')
cache.delete(f'user:{user.id}')
cache.delete("users")
cache.delete(f"user:{user.id}")
if user.user_group:
cache.delete(f'usergroup_users:{user.user_group.id}')
cache.delete(f"usergroup_users:{user.user_group.id}")
except Exception as e:
print('Warning: Unable to send email')
print("Warning: Unable to send email")
print(e)
@action(methods=['post'], detail=False, url_path='activation', url_name='activation')
@action(
methods=["post"], detail=False, url_path="activation", url_name="activation"
)
def activation(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -103,16 +104,16 @@ class CustomUserViewSet(DjoserUserViewSet):
# Construct a response with user's first name, last name, and email
user_data = {
'first_name': user.first_name,
'last_name': user.last_name,
'email': user.email,
'username': user.username
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email,
"username": user.username,
}
# Clear cache
cache.delete('users')
cache.delete(f'user:{user.id}')
cache.delete("users")
cache.delete(f"user:{user.id}")
if user.user_group:
cache.delete(f'usergroup_users:{user.user_group.id}')
cache.delete(f"usergroup_users:{user.user_group.id}")
return Response(user_data, status=status.HTTP_200_OK)