Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 6.0.6 on 2026-07-22 08:25

import utils.storage
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('editions', '0002_initial'),
]

operations = [
migrations.AlterField(
model_name='editionpublicdocument',
name='uploaded_document',
field=models.FileField(blank=True, null=True, storage=utils.storage.select_public_storage, upload_to='editions_public/%Y/%W/', verbose_name='uploaded document'),
),
migrations.AlterField(
model_name='projectdatadocument',
name='uploaded_document',
field=models.FileField(blank=True, null=True, upload_to='projects/%Y/%W/', verbose_name='uploaded document'),
),
]
4 changes: 2 additions & 2 deletions backend/editions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class EditionPublicDocument(EditionRelatedModel, CommonTimeStampModel):

uploaded_document = models.FileField(
verbose_name=_("uploaded document"),
upload_to="editions_public/%Y/%m/",
upload_to="editions_public/%Y/%W/",
storage=select_public_storage,
blank=True,
null=True,
Expand Down Expand Up @@ -414,7 +414,7 @@ class ProjectDataDocument(ProjectDataRelatedModel, CommonTimeStampModel):

uploaded_document = models.FileField(
verbose_name=_("uploaded document"),
upload_to="projects/%Y/%m/",
upload_to="projects/%Y/%W/",
blank=True,
null=True,
)
Expand Down
14 changes: 14 additions & 0 deletions backend/funding/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,10 @@
"django.contrib.sessions",
"django.contrib.staticfiles",
# Third party apps:
"allauth",
"allauth.account",
"allauth.socialaccount",
# TODO: include the providers you want to enable like "allauth.socialaccount.providers.amazon_cognito"
"csp",
"auditlog",
"corsheaders",
Expand Down Expand Up @@ -342,6 +346,7 @@
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"inertia.middleware.InertiaMiddleware",
"allauth.account.middleware.AccountMiddleware",
"auditlog.middleware.AuditlogMiddleware",
# Funding Call middlewares:
"editions.middleware.maintenance_mode", # noqa
Expand Down Expand Up @@ -417,6 +422,7 @@
}
}


# Password validation

AUTH_PASSWORD_VALIDATORS = [
Expand All @@ -441,6 +447,14 @@

AUTH_USER_MODEL = "users.User"

AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
"django.contrib.auth.backends.ModelBackend",
# `allauth` specific authentication methods, such as login by email
"allauth.account.auth_backends.AuthenticationBackend",
]


# Email settings
EMAIL_BACKEND = env.str("EMAIL_BACKEND")
EMAIL_SEND_METHOD = env.str("EMAIL_SEND_METHOD")
Expand Down
3 changes: 2 additions & 1 deletion backend/funding/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""

from django.urls import path
from django.urls import include, path

from editions.views import temp_landing_page

urlpatterns = [
path("", temp_landing_page, name="landing-page"),
path("temp-accounts/", include("allauth.urls")),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 6.0.6 on 2026-07-22 08:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('orgs', '0002_initial'),
]

operations = [
migrations.AlterField(
model_name='organizationdocument',
name='uploaded_document',
field=models.FileField(blank=True, null=True, upload_to='orgs/%Y/%W/', verbose_name='uploaded document'),
),
]
2 changes: 1 addition & 1 deletion backend/orgs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class OrganizationDocument(OrganizationRelatedModel):

uploaded_document = models.FileField(
verbose_name=_("uploaded document"),
upload_to="orgs/%Y/%m/",
upload_to="orgs/%Y/%W/",
blank=True,
null=True,
)
Expand Down
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies = [
"blessed~=1.44.0",
"chardet~=7.4.3",
"django~=6.0.6",
"django-allauth[socialaccount]~=65.18.0",
"django-auditlog~=3.4.1",
"django-cors-headers~=4.9.0",
"django-csp~=4.0.0",
Expand Down
32 changes: 32 additions & 0 deletions backend/users/migrations/0002_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 6.0.6 on 2026-07-22 08:19

import django.core.validators
import django.db.models.deletion
import utils.models
import utils.storage
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('picture', models.FileField(blank=True, null=True, storage=utils.storage.select_public_storage, upload_to='profiles_public/%Y/%m/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=('jpg', 'jpeg', 'png')), utils.models.MaxFileSizeValidator(2097152)], verbose_name='picture')),
('accepted_newsletter', models.DateTimeField(blank=True, null=True, verbose_name='Accepted to receive newsletters')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'profile',
'verbose_name_plural': 'profiles',
'ordering': ['user__email'],
},
),
]
21 changes: 21 additions & 0 deletions backend/users/migrations/0003_alter_profile_picture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 6.0.6 on 2026-07-22 08:25

import django.core.validators
import utils.models
import utils.storage
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0002_profile'),
]

operations = [
migrations.AlterField(
model_name='profile',
name='picture',
field=models.FileField(blank=True, null=True, storage=utils.storage.select_public_storage, upload_to='profiles_public/%Y/%W/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=('jpg', 'jpeg', 'png')), utils.models.MaxFileSizeValidator(2097152)], verbose_name='picture'),
),
]
33 changes: 33 additions & 0 deletions backend/users/migrations/0004_loginattempt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 6.0.6 on 2026-07-22 12:11

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('users', '0003_alter_profile_picture'),
]

operations = [
migrations.CreateModel(
name='LoginAttempt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='Timestamp of the creation of the object', verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Timestamp of the last update of the object', verbose_name='updated at')),
('email', models.EmailField(blank=True, editable=False, max_length=254, verbose_name='email')),
('success', models.BooleanField(default=False, editable=False, verbose_name='success')),
('remote_ua', models.CharField(blank=True, editable=False, max_length=150, verbose_name='remote user agent')),
('remote_addr', models.GenericIPAddressField(blank=True, editable=False, null=True, verbose_name='remote address')),
('user', models.ForeignKey(blank=True, editable=False, help_text='successful login user account', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='logins', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'login attempt',
'verbose_name_plural': 'login attempts',
'ordering': ('-created_at',),
},
),
]
85 changes: 85 additions & 0 deletions backend/users/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from auditlog.registry import auditlog
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractUser, UserManager
from django.core.validators import FileExtensionValidator
from django.db import models
from django.db.models.functions import Lower
from django.utils.translation import gettext_lazy as _

from utils.models import CommonTimeStampModel, MaxFileSizeValidator
from utils.storage import select_public_storage


class CustomUserManager(UserManager):
def _create_user(self, email, password, **extra_fields):
Expand Down Expand Up @@ -52,6 +58,10 @@ class User(AbstractUser):

email = models.EmailField(verbose_name=_("email address"), blank=False, null=False, unique=True)

# Type hinting for related models
profile: "models.manager.RelatedManager[Profile]"

# Model managers
objects = CustomUserManager()

USERNAME_FIELD = "email"
Expand All @@ -64,6 +74,81 @@ class Meta:
models.UniqueConstraint(Lower("email"), name="email_unique"),
]

def __str__(self) -> str:
return _("User {id}: {email}").format(id=self.pk, email=self.email)

def to_dict(self):
# TODO
return {}


class Profile(models.Model):
"""
Additional user information, not related to the authentication process
"""

user = models.OneToOneField(
settings.AUTH_USER_MODEL,
verbose_name=_("user"),
related_name="profile",
blank=False,
null=False,
on_delete=models.CASCADE,
)

picture = models.FileField(
verbose_name=_("picture"),
upload_to="profiles_public/%Y/%W/",
storage=select_public_storage,
blank=True,
null=True,
validators=(
FileExtensionValidator(allowed_extensions=("jpg", "jpeg", "png")),
MaxFileSizeValidator(settings.MAX_DOCUMENT_SIZE),
),
)

accepted_newsletter = models.DateTimeField(verbose_name=_("Accepted to receive newsletters"), null=True, blank=True)

class Meta:
verbose_name = _("profile")
verbose_name_plural = _("profiles")
ordering = ["user__email"]

def __str__(self) -> str:
return _("(User {user_id}) Profile {id}").format(user_id=self.user.pk, id=self.pk)


class LoginAttempt(CommonTimeStampModel):
"""
Store user login attempts
"""

email = models.EmailField(verbose_name=_("email"), blank=True, null=False, editable=False)
user = models.ForeignKey(
User,
verbose_name=_("user"),
related_name="logins",
blank=True,
null=True,
editable=False,
on_delete=models.SET_NULL,
help_text=_("successful login user account"),
)
success = models.BooleanField(verbose_name=_("success"), editable=False, default=False)
remote_ua = models.CharField(
verbose_name=_("remote user agent"), max_length=150, blank=True, null=False, editable=False
)
remote_addr = models.GenericIPAddressField(blank=True, null=True, editable=False, verbose_name=_("remote address"))

class Meta: # type: ignore
verbose_name = _("login attempt")
verbose_name_plural = _("login attempts")
ordering = ("-created_at",)

def __str__(self) -> str:
return _("Login {id} Email {email}").format(id=self.pk, email=self.email)


auditlog.register(Profile)
auditlog.register(User, exclude_fields=["password"])
18 changes: 18 additions & 0 deletions backend/utils/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _


Expand All @@ -16,3 +18,19 @@ class CommonTimeStampModel(models.Model):

class Meta:
abstract = True


@deconstructible
class MaxFileSizeValidator:
"""
Validator which checks that file size is less or equal to the specified size limit
"""

def __init__(self, max_size=1024):
self.max_size = max_size

def __call__(self, value):
if not value and not hasattr(value, "size"):
return
if value.size > self.max_size:
raise ValidationError(_("The file is too large."))
6 changes: 6 additions & 0 deletions backend/utils/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from auditlog.middleware import AuditlogMiddleware
from django.http import HttpRequest


def get_remote_addr(request: HttpRequest):
return AuditlogMiddleware._get_remote_addr(request)
Loading
Loading