Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit profile #60

Merged
merged 5 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DEPLOYING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The following are the steps to perform a deployment in production. In case you w
- `CGDS_CHUNK_SIZE`: size **in bytes** of the chunk in which the files of a CGDS study are downloaded, the bigger it is, the faster the download is, but the more server memory it consumes. Default `2097152`, i.e. 2MB.
- `THRESHOLD_ORDINAL`: number of different values for the GEM (CNA) information to be considered ordinal, if the number is <= to this value then it is considered categorical/ordinal and a boxplot is displayed, otherwise, it is considered continuous and the common correlation graph is displayed. Default `5`.
- `THRESHOLD_GEM_SIZE_TO_COLLECT`: GEM file size threshold (in MB) for the GEM dataset to be available in memory. This has a HUGE impact on the performance of the analysis. If the size is less than or equal to this threshold, it is allocated in memory, otherwise, it will be read lazily from the disk. If None GGCA automatically allocates in memory when the GEM dataset size is small (<= 100MB). Therefore, if you want to force to always use RAM to improve performance you should set a very high threshold, on the contrary, if you want a minimum memory usage at the cost of poor performance, set it to `0`. Default `None`.
- `MIN_PASSWORD_LEN`: Defines the minimum required length for user passwords when updating their profile. If the provided password is shorter than this length, the update will be rejected. Default `8`.
- PostgreSQL:
- `POSTGRES_USERNAME`: PostgreSQL connection username. **Must be equal to** `POSTGRES_USER`.
- `POSTGRES_PASSWORD`: PostgreSQL connection password. **Must be equal to** `POSTGRES_PASSWORD`.
Expand Down
31 changes: 0 additions & 31 deletions src/frontend/serializers.py

This file was deleted.

11 changes: 1 addition & 10 deletions src/frontend/urls.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
from django.conf import settings
from django.urls import path
from . import views

urlpatterns = [
path('', views.index_action, name='index'),
path('gem', views.gem_action, name='gem'),
path('login', views.login_action, name='login'),
path('my-datasets', views.datasets_action, name='datasets'),
path('survival', views.survival_action, name='survival'),
path('authenticate', views.authenticate_action, name='authentication'),
path('create-user', views.create_user_action, name='create_user'),
path('about-us', views.about_us_action, name='about_us'),
path('site-policy', views.terms_and_privacy_policy_action, name='site_policy'),
path('logout', views.logout_action, name='logout'),
path('user', views.CurrentUserView.as_view(), name='current_user')
path('site-policy', views.terms_and_privacy_policy_action, name='site_policy')
]

if settings.DEBUG:
urlpatterns.append(path('new-user-email-test', views.test_new_user_email))
urlpatterns.append(path('confirmation-email-test', views.test_confirmation_email))
179 changes: 1 addition & 178 deletions src/frontend/views.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import logging
from typing import Optional, Union
from django.contrib.auth.base_user import AbstractBaseUser
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, logout, login
from django.contrib.auth.models import User
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django_email_verification import sendConfirm
from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import UserSerializer
from django.conf import settings
from django.db import transaction, InternalError


def index_action(request):
Expand Down Expand Up @@ -52,169 +41,3 @@ def datasets_action(request):
def survival_action(request):
"""Survival Analysis view"""
return render(request, "frontend/survival.html")


def login_action(request):
"""Login view"""
if request.user.is_authenticated:
return redirect('index')

return render(request, "frontend/login.html")


def view_bad_credentials(request):
"""Returns login template with bad credential error in Its context"""
return render(request, "frontend/login.html", {'loginError': 'Your username or password is invalid'})


def authenticate_action(request):
"""Authentication view"""
username: Optional[str] = request.POST.get('username')
password: Optional[str] = request.POST.get('password')
if username is None or password is None:
return view_bad_credentials(request)

user: Union[AbstractBaseUser, AbstractBaseUser, None] = authenticate(username=username, password=password)

if user is not None:
login(request, user)
# If 'next' param is specified, it redirects to that URL
next_url = request.POST.get('next')
return redirect(next_url if next_url is not None else 'index')
else:
# Checks if User is active
try:
user: User = User.objects.get(username=username)
if user is not None and not user.is_active:
return render(
request,
"frontend/login.html",
{
'loginWarning': 'Your account has not been validated yet. Please, check your email account for '
'our verification email'
}
)
return view_bad_credentials(request)
except User.DoesNotExist:
return view_bad_credentials(request)


def is_sign_up_form_valid(username: str, email: str, password: str, password_repeated: str) -> bool:
"""
Checks if Sign Up form is valid
@param username: Username to check
@param email: Email to check
@param password: Password to check
@param password_repeated: Repeated password to check
@return: True if is all the field valid. False otherwise
"""
password_striped = password.strip()
password_repeated_striped = password_repeated.strip()
return len(username.strip()) > 0 \
and len(email.strip()) > 0 \
and len(password_striped) > 0 \
and len(password_repeated_striped) > 0 \
and password_striped == password_repeated_striped


def user_already_exists_username(username: str) -> bool:
"""
Check if exists any user in the DB with the same username passed by parameter
@param username: Username to check
@return: True if exists the user. False otherwise
"""
return User.objects.filter(username=username).exists()


def user_already_exists_email(email: str) -> bool:
"""
Check if exists any user in the DB with the same email passed by parameter
@param email: Email to check
@return: True if exists the user. False otherwise
"""
return User.objects.filter(email=email).exists()


@transaction.atomic
def create_user_action(request):
"""User creation view"""
username: str = request.POST.get('newUsername', '')
email: str = request.POST.get('email', '')
password: str = request.POST.get('newPassword', '')
password_repeated: str = request.POST.get('newPasswordRepeated', '')
if not is_sign_up_form_valid(username, email, password, password_repeated):
return render(request, "frontend/login.html", {'loginError': 'Invalid fields'})

if user_already_exists_username(username):
return render(request, "frontend/login.html", {'loginError': 'There is already a user with that username'})

if user_already_exists_email(email):
return render(request, "frontend/login.html", {'loginError': 'There is already a user with that email'})

# If it's set to not send an email asking for confirmation just creates the user and redirect to login
if not settings.EMAIL_NEW_USER_CONFIRMATION_ENABLED:
User.objects.create_user(username, email, password)
return render(request, "frontend/login.html", {'loginSuccess': 'User created successfully'})

# Creates the user and sends email
# FIXME: library does not return exceptions thrown in sender thread. After a PR it should be tested
error_sending_email = False
try:
with transaction.atomic():
user = User.objects.create_user(username, email, password)
try:
sendConfirm(user)
except Exception as ex:
logging.warning(f'Error sending email -> {ex}')
raise InternalError # Rollback
except InternalError:
error_sending_email = True

if error_sending_email:
return render(request, "frontend/login.html", {'loginError': 'An error have occurred. Please, try again. If the'
' problem persists contact us, please'})

# Hides email
idx_at = email.index('@')
idx_to_replace = idx_at - 2
hidden_email = '*' * idx_to_replace + email[idx_to_replace:]

return render(
request,
"frontend/login.html",
{'loginWarning': f'We have sent a confirmation email to {hidden_email}'}
)


def test_new_user_email(request):
"""
To test new user email template in browser. Only accessible in DEBUG mode
@param request: Request object
@return: HTTP Response
"""
return render(request, settings.EMAIL_MAIL_HTML, {'link': '#'})


def test_confirmation_email(request):
"""
To test confirmation email template in browser. Only accessible in DEBUG mode
@param request: Request object
@return: HTTP Response
"""
return render(request, settings.EMAIL_PAGE_TEMPLATE, {'success': True})


def logout_action(request):
"""Closes User session"""
logout(request)
return redirect('index')


class CurrentUserView(APIView):
"""Gets current User info"""
permission_classes = [permissions.IsAuthenticated]

@staticmethod
def get(request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
2 changes: 1 addition & 1 deletion src/institutions/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
from .models import Institution
from frontend.serializers import UserSerializer
from users.serializers import UserSerializer


class InstitutionSerializer(serializers.ModelSerializer):
Expand Down
4 changes: 3 additions & 1 deletion src/multiomics_intermediate/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
'inferences',
'molecules_details',
'chunked_upload',
'users',
]

MIDDLEWARE = [
Expand Down Expand Up @@ -151,6 +152,8 @@
},
]

MIN_PASSWORD_LEN: int = int(os.getenv('MIN_PASSWORD_LEN', 8))

# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/

Expand Down Expand Up @@ -296,7 +299,6 @@
EMAIL_PAGE_TEMPLATE = 'confirm_template.html'
EMAIL_PAGE_DOMAIN = 'https://multiomix.org'


# Modulector settings
MODULECTOR_SETTINGS = {
'host': os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org'),
Expand Down
1 change: 1 addition & 0 deletions src/multiomics_intermediate/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@
path('molecules/', include('molecules_details.urls')),
path('admin/', admin.site.urls),
path('email/', include(mail_urls)),
path('users/', include('users.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
8 changes: 4 additions & 4 deletions src/user_files/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from rest_framework import serializers
from common.functions import get_enum_from_value, create_survival_columns_from_json
from datasets_synchronization.models import SurvivalColumnsTupleUserFile
from .models_choices import FileType
from .utils import has_uploaded_file_valid_format, get_invalid_format_response
from frontend.serializers import UserSimpleSerializer
from user_files.models_choices import FileType
from user_files.utils import has_uploaded_file_valid_format, get_invalid_format_response
from users.serializers import UserSimpleSerializer
from institutions.serializers import InstitutionSimpleSerializer
from tags.serializers import TagSerializer
from .models import UserFile
from user_files.models import UserFile


class SurvivalColumnsTupleUserFileSimpleSerializer(serializers.ModelSerializer):
Expand Down
2 changes: 1 addition & 1 deletion src/user_files/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from rest_framework.exceptions import ValidationError
from rest_framework.views import APIView
from common.pagination import StandardResultsSetPagination
from .serializers import UserFileSerializer, UserFileWithoutFileObjSerializer
from user_files.serializers import UserFileSerializer, UserFileWithoutFileObjSerializer
from .models import UserFile
from rest_framework.response import Response

Expand Down
Empty file added src/users/admin.py
Empty file.
6 changes: 6 additions & 0 deletions src/users/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'
Empty file.
1 change: 1 addition & 0 deletions src/users/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

67 changes: 67 additions & 0 deletions src/users/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from django.forms import ValidationError
from rest_framework import serializers
from django.contrib.auth import get_user_model
from institutions.models import Institution
from django.db import transaction
from django.contrib.auth.hashers import make_password

from multiomics_intermediate import settings



class UserSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = ['id', 'username', 'is_superuser', 'first_name', 'last_name']

def to_representation(self, instance):
# Adds custom fields
data = super(UserSerializer, self).to_representation(instance)

# Check if current user has any institutions where he's admin
data['is_institution_admin'] = Institution.objects.filter(
institutionadministration__user=instance,
institutionadministration__is_institution_admin=True
).exists()

# User at this point is not anonymous
data['is_anonymous'] = False

return data


class UserSimpleSerializer(serializers.ModelSerializer):
"""User serializer with fewer data"""
class Meta:
model = get_user_model()
fields = ['id', 'username']

class UserUpdateSerializer(serializers.ModelSerializer):
""""fields that are received to update the user"""
first_name = serializers.CharField()
last_name = serializers.CharField()
password = serializers.CharField(write_only=True, required=False, min_length=8)

class Meta:
model = get_user_model()
fields = ['first_name', 'last_name', 'password']

def update(self, instance, validated_data):
"""Updates the users first_name, last_name, and password fields."""
with transaction.atomic():
instance.first_name = validated_data.get('first_name', instance.first_name)
instance.last_name = validated_data.get('last_name', instance.last_name)

password = validated_data.pop('password', '')
password_length = len(password)

if password_length != 0:
minimum_password_len = settings.MIN_PASSWORD_LEN
if password_length < minimum_password_len:
raise ValidationError(f'Password must be at least {minimum_password_len} chars long')
else:
instance.set_password(password)

instance.save()
return instance

Loading
Loading