Compare commits

...

5 Commits

Author SHA1 Message Date
3636d4a72b working on permission is_contributor; checkpoint 2025-05-30 10:24:01 +02:00
776ba21695 project model create and add contributors 2025-05-26 20:08:34 +02:00
278ea3ed0a renamed Contributor`s FK to user more explicit 2025-05-25 21:26:11 +02:00
80a2eb5b5d added active to Project 2025-05-25 21:21:41 +02:00
635ad35c55 user ok 2025-05-24 13:47:37 +02:00
29 changed files with 1145 additions and 33 deletions

15
Pipfile Normal file
View File

@ -0,0 +1,15 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
django = "*"
djangorestframework = "*"
djangorestframework-simplejwt = "*"
requests = "*"
[dev-packages]
[requires]
python_version = "3.10"

211
Pipfile.lock generated Normal file
View File

@ -0,0 +1,211 @@
{
"_meta": {
"hash": {
"sha256": "509028d446b2c9fe27b4bc6e6456cf0d861abb7bee972433e88387bb6f11aa2e"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.10"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"asgiref": {
"hashes": [
"sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47",
"sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"
],
"markers": "python_version >= '3.8'",
"version": "==3.8.1"
},
"certifi": {
"hashes": [
"sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6",
"sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"
],
"markers": "python_version >= '3.6'",
"version": "==2025.4.26"
},
"charset-normalizer": {
"hashes": [
"sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4",
"sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45",
"sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7",
"sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0",
"sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7",
"sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d",
"sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d",
"sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0",
"sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184",
"sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db",
"sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b",
"sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64",
"sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b",
"sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8",
"sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff",
"sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344",
"sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58",
"sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e",
"sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471",
"sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148",
"sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a",
"sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836",
"sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e",
"sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63",
"sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c",
"sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1",
"sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01",
"sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366",
"sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58",
"sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5",
"sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c",
"sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2",
"sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a",
"sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597",
"sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b",
"sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5",
"sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb",
"sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f",
"sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0",
"sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941",
"sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0",
"sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86",
"sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7",
"sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7",
"sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455",
"sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6",
"sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4",
"sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0",
"sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3",
"sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1",
"sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6",
"sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981",
"sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c",
"sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980",
"sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645",
"sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7",
"sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12",
"sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa",
"sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd",
"sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef",
"sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f",
"sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2",
"sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d",
"sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5",
"sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02",
"sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3",
"sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd",
"sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e",
"sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214",
"sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd",
"sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a",
"sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c",
"sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681",
"sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba",
"sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f",
"sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a",
"sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28",
"sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691",
"sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82",
"sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a",
"sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027",
"sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7",
"sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518",
"sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf",
"sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b",
"sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9",
"sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544",
"sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da",
"sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509",
"sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f",
"sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a",
"sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"
],
"markers": "python_version >= '3.7'",
"version": "==3.4.2"
},
"django": {
"hashes": [
"sha256:57fe1f1b59462caed092c80b3dd324fd92161b620d59a9ba9181c34746c97284",
"sha256:a9b680e84f9a0e71da83e399f1e922e1ab37b2173ced046b541c72e1589a5961"
],
"index": "pypi",
"markers": "python_version >= '3.10'",
"version": "==5.2.1"
},
"djangorestframework": {
"hashes": [
"sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361",
"sha256:f022ff46613584de994c0c6a4aebbace5fd700555fbe9d33b865ebf173eba6c9"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==3.16.0"
},
"djangorestframework-simplejwt": {
"hashes": [
"sha256:474a1b737067e6462b3609627a392d13a4da8a08b1f0574104ac6d7b1406f90e",
"sha256:4ef6b38af20cdde4a4a51d1fd8e063cbbabb7b45f149cc885d38d905c5a62edb"
],
"index": "pypi",
"markers": "python_version >= '3.9'",
"version": "==5.5.0"
},
"idna": {
"hashes": [
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
"sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
],
"markers": "python_version >= '3.6'",
"version": "==3.10"
},
"pyjwt": {
"hashes": [
"sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850",
"sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"
],
"markers": "python_version >= '3.8'",
"version": "==2.9.0"
},
"requests": {
"hashes": [
"sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760",
"sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.32.3"
},
"sqlparse": {
"hashes": [
"sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272",
"sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"
],
"markers": "python_version >= '3.8'",
"version": "==0.5.3"
},
"typing-extensions": {
"hashes": [
"sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c",
"sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"
],
"markers": "python_version >= '3.8'",
"version": "==4.13.2"
},
"urllib3": {
"hashes": [
"sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466",
"sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"
],
"markers": "python_version >= '3.9'",
"version": "==2.4.0"
}
},
"develop": {}
}

View File

@ -1,3 +1,3 @@
from django.contrib import admin #from django.contrib import admin
# Register your models here. # Register your models here.

View File

@ -0,0 +1,47 @@
# Generated by Django 5.2.1 on 2025-05-23 03:58
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
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')),
('can_be_contacted', models.BooleanField(default=False)),
('can_data_be_shared', models.BooleanField(default=False)),
('age', models.IntegerField()),
('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_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,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-05-23 04:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='user',
name='age',
field=models.IntegerField(null=True),
),
]

View File

@ -5,10 +5,8 @@ from django.contrib.auth.models import AbstractUser, Group
class User(AbstractUser): class User(AbstractUser):
can_be_contacted = models.BooleanField(default=False) can_be_contacted = models.BooleanField(default=False)
can_data_be_shared = models.BooleanField(default=False) can_data_be_shared = models.BooleanField(default=False)
age = models.IntegerField() age = models.IntegerField(null=True)
def __str__(self):
return self.username

View File

@ -0,0 +1,73 @@
from rest_framework.serializers import ModelSerializer, ValidationError
from rest_framework import serializers
from authentication.models import User
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = ['id',
'username',
'email',
'age',
'can_be_contacted',
'can_data_be_shared']
class UserUpdateSerializer(ModelSerializer):
class Meta:
model = User
fields = ['email', 'can_be_contacted', 'can_data_be_shared']
class UserRegisterSerializer(ModelSerializer):
password2 = serializers.CharField(write_only=True)
password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['username',
'email',
'password',
'password2',
'age',
'can_be_contacted',
'can_data_be_shared']
def validate(self, data):
if data['password'] != data['password2']:
raise ValidationError("Passwords don't match.")
return data
def validate_age(self, value):
if value < 15:
raise ValidationError("You must be older than 15")
return value
def create(self, validated_data):
"""
Create and return a new `User` instance, given the validated data.
"""
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
password=validated_data['password'],
age=validated_data['age'],
can_be_contacted=validated_data['can_be_contacted'],
can_data_be_shared=validated_data['can_data_be_shared'],
)
return user
class PasswordUpdateSerializer(ModelSerializer):
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)
class Meta:
model = User
fields = ['old_password', 'new_password']

View File

@ -1,3 +1,96 @@
from django.contrib.auth import update_session_auth_hash
from django.shortcuts import render from django.shortcuts import render
from django.utils.autoreload import raise_last_exception
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from authentication.models import User
from authentication.serializers import (UserSerializer,
UserUpdateSerializer,
UserRegisterSerializer,
PasswordUpdateSerializer)
class UserCreateView(APIView):
"""
Allow user registration for anyone
"""
#TODELETE : for testing purpose
def get(self, request, *args, **kwargs):
user = User.objects.all()
serializer = UserSerializer(user, many=True)
return Response(serializer.data)
def post(self, request):
"""
User subscription
Args:
"""
serializer = UserRegisterSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
response = {
"message": "User created successfully",
"data": serializer.data
}
return Response(data=response,
status=status.HTTP_201_CREATED)
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
class PasswordUpdateView(APIView):
permission_classes = [IsAuthenticated]
def put(self, request):
user = request.user
serializer = PasswordUpdateSerializer(data=request.data)
if serializer.is_valid():
if not user.check_password(serializer.data.get("old_password")):
return Response({"old_password":"Wrong password"},
status=status.HTTP_400_BAD_REQUEST)
user.set_password(serializer.data.get('new_password'))
user.save()
update_session_auth_hash(request, user)
return Response(serializer.errors,
status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
class UserView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
return Response(UserSerializer(request.user).data)
def put(self, request):
user = request.user
serializer = UserUpdateSerializer(user, data=request.data)
print(serializer.initial_data)
if serializer.is_valid():
serializer.save()
return Response("Data updated",
status=status.HTTP_201_CREATED)
return Response("Error",
status=status.HTTP_400_BAD_REQUEST)
def delete(self, request):
user = request.user
username = request.user.username
if 'user' in request.data:
if username == request.data['user']:
user.delete()
return Response(f"User {username} deleted.",
status=status.HTTP_204_NO_CONTENT)
return Response("Token's owner and user provided don't match",
status=status.HTTP_400_BAD_REQUEST)
return Response("Username to delete must be given in data",
status=status.HTTP_400_BAD_REQUEST)
# Create your views here.

View File

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
""" """
from pathlib import Path from pathlib import Path
from datetime import timedelta
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -127,9 +128,11 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'authentication.User' AUTH_USER_MODEL = 'authentication.User'
REST_FRAMERWORK = { REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 5,
'DEFAULT_AUTHENTICATION_CLASSES': ('rest_framework_simplejwt.authentication.JWTAuthentication',) 'DEFAULT_AUTHENTICATION_CLASSES': ('rest_framework_simplejwt.authentication.JWTAuthentication',)
} }
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
}

View File

@ -15,8 +15,27 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path, include
from authentication.views import (UserView, UserCreateView, PasswordUpdateView)
from support.views import ProjectViewSet, IssueViewSet, CommentViewSet, ContributorViewSet
from rest_framework import routers
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
router = routers.SimpleRouter()
#router.register('user', UserViewSet, basename='user')
router.register('project', ProjectViewSet, basename='project')
router.register('issue', IssueViewSet, basename='issue')
router.register('comment', CommentViewSet, basename='comment')
router.register('contributors', ContributorViewSet)
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls')),
path('api/', include(router.urls)),
path('api/user/', UserView.as_view(), name='user'),
path('api/user/create/', UserCreateView.as_view(), name='user_create'),
path('api/user/password-update/', PasswordUpdateView.as_view(), name='password_update'),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
] ]

View File

@ -1,9 +1,21 @@
from django.contrib import admin from django.contrib import admin
from support.models import Project, Issue, Comment, Contributor from support.models import Project, Issue, Comment, ProjectContributor
from authentication.models import User
class AdminProject(admin.ModelAdmin):
list_display = ('id', 'title', 'author', 'contributors')
@admin.display(description='contributors')
def contributors(self, obj):
return obj.contributor
class AdminIssue(admin.ModelAdmin):
list_display = ('id', 'title', 'author', 'project')
admin.site.register(Project) admin.site.register(User)
admin.site.register(Issue) admin.site.register(Project, AdminProject)
admin.site.register(Issue, AdminIssue)
admin.site.register(Comment) admin.site.register(Comment)
admin.site.register(Contributor) admin.site.register(ProjectContributor)

View File

@ -0,0 +1,71 @@
# Generated by Django 5.2.1 on 2025-05-23 03:58
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Contributor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.CharField(blank=True, max_length=255)),
('contributor', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Issue',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),
('date_created', models.DateTimeField(auto_now_add=True)),
('description', models.TextField()),
('status', models.CharField(max_length=15, verbose_name=[('ToDo', 'Todo'), ('InProgress', 'Inprogress'), ('Finished', 'Finished')])),
('priority', models.CharField(max_length=15, verbose_name=[('L', 'Low'), ('M', 'Medium'), ('H', 'High')])),
('tag', models.CharField(max_length=15, verbose_name=[('Bug', 'Bug'), ('Feature', 'Feature'), ('Task', 'Task')])),
('author', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='support.contributor')),
],
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('date_created', models.DateTimeField(auto_now_add=True)),
('description', models.CharField(max_length=4000)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='support.contributor')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='support.issue')),
],
),
migrations.CreateModel(
name='Project',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('date_created', models.DateTimeField(auto_now_add=True)),
('type', models.CharField(choices=[('BackEnd', 'Backend'), ('FrontEnd', 'Frontend'), ('iOS', 'Ios'), ('Android', 'Android')], max_length=10)),
('description', models.CharField(max_length=4000)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='author', to='support.contributor')),
('contributors', models.ManyToManyField(related_name='contribution', through='support.Contributor', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='issue',
name='project',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='support.project'),
),
migrations.AddField(
model_name='contributor',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project', to='support.project'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.2.1 on 2025-05-25 19:20
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='project',
name='active',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='issue',
name='project',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='support.project'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-05-25 19:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('support', '0002_project_active_alter_issue_project'),
]
operations = [
migrations.RenameField(
model_name='contributor',
old_name='contributor',
new_name='user',
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.1 on 2025-05-25 19:36
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0003_rename_contributor_contributor_user'),
]
operations = [
migrations.AlterField(
model_name='project',
name='author',
field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='author', to='support.contributor'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.1 on 2025-05-25 19:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0004_alter_project_author'),
]
operations = [
migrations.AlterField(
model_name='project',
name='author',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='author', to='support.contributor'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.1 on 2025-05-25 19:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0005_alter_project_author'),
]
operations = [
migrations.AlterField(
model_name='project',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='author', to='support.contributor'),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.2.1 on 2025-05-25 19:49
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0006_alter_project_author'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='contributor',
name='active',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='project',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='author', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-05-25 19:52
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('support', '0007_contributor_active_alter_project_author'),
]
operations = [
migrations.RenameField(
model_name='contributor',
old_name='user',
new_name='contributor_user',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-05-26 05:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('support', '0008_rename_user_contributor_contributor_user'),
]
operations = [
migrations.RenameField(
model_name='contributor',
old_name='contributor_user',
new_name='username',
),
]

View File

@ -0,0 +1,52 @@
# Generated by Django 5.2.1 on 2025-05-26 05:53
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0009_rename_contributor_user_contributor_username'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='comment_author', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='issue',
name='author',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='issue_author', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='project',
name='author',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='project_author', to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='ProjectContributor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('active', models.BooleanField(default=True)),
('data', models.CharField(blank=True, max_length=255)),
('contributor', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project', to='support.project')),
],
options={
'unique_together': {('contributor', 'project')},
},
),
migrations.AlterField(
model_name='project',
name='contributors',
field=models.ManyToManyField(related_name='contribution', through='support.ProjectContributor', to=settings.AUTH_USER_MODEL),
),
migrations.DeleteModel(
name='Contributor',
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.1 on 2025-05-26 18:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0010_alter_comment_author_alter_issue_author_and_more'),
]
operations = [
migrations.AlterField(
model_name='issue',
name='priority',
field=models.CharField(choices=[('L', 'Low'), ('M', 'Medium'), ('H', 'High')], max_length=15),
),
migrations.AlterField(
model_name='issue',
name='status',
field=models.CharField(choices=[('ToDo', 'Todo'), ('InProgress', 'Inprogress'), ('Finished', 'Finished')], max_length=15),
),
migrations.AlterField(
model_name='issue',
name='tag',
field=models.CharField(choices=[('Bug', 'Bug'), ('Feature', 'Feature'), ('Task', 'Task')], max_length=15),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-05-26 18:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0011_alter_issue_priority_alter_issue_status_and_more'),
]
operations = [
migrations.AlterField(
model_name='issue',
name='priority',
field=models.CharField(choices=[('Low', 'Low'), ('Medium', 'Medium'), ('High', 'High')], max_length=15),
),
migrations.AlterField(
model_name='issue',
name='status',
field=models.CharField(choices=[('ToDo', 'Todo'), ('In Progress', 'Inprogress'), ('Finished', 'Finished')], max_length=15),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.1 on 2025-05-26 19:06
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0012_alter_issue_priority_alter_issue_status'),
]
operations = [
migrations.AlterField(
model_name='issue',
name='project',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='support.project'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.1 on 2025-05-27 09:30
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('support', '0013_alter_issue_project'),
]
operations = [
migrations.AlterField(
model_name='issue',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='support.project'),
),
]

View File

@ -14,30 +14,45 @@ class Project(models.Model):
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
type = models.CharField(choices=Type.choices, max_length=10) type = models.CharField(choices=Type.choices, max_length=10)
active = models.BooleanField(default=True)
description = models.CharField(max_length=4000) description = models.CharField(max_length=4000)
author = models.ForeignKey('Contributor', on_delete=models.DO_NOTHING, related_name='author') author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.DO_NOTHING,
related_name='project_author', null=True)
contributors = models.ManyToManyField( contributors = models.ManyToManyField(settings.AUTH_USER_MODEL,
settings.AUTH_USER_MODEL, through='Contributor', related_name='contribution') through='ProjectContributor',
related_name='contribution')
def __str__(self):
return self.title
class Contributor(models.Model): class ProjectContributor(models.Model):
contributor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING) contributor = models.ForeignKey(settings.AUTH_USER_MODEL,
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='project') on_delete=models.DO_NOTHING)
active = models.BooleanField(default=True)
project = models.ForeignKey('Project',
on_delete=models.CASCADE,
related_name='project')
data = models.CharField(max_length=255, blank=True) data = models.CharField(max_length=255, blank=True)
class Meta:
unique_together = ('contributor', 'project')
def __str__(self):
return self.contributor.username
class Issue(models.Model): class Issue(models.Model):
class Priority(models.TextChoices): class Priority(models.TextChoices):
LOW = 'L' LOW = 'Low'
MEDIUM = 'M' MEDIUM = 'Medium'
HIGH = 'H' HIGH = 'High'
class Status(models.TextChoices): class Status(models.TextChoices):
TODO = 'ToDo' TODO = 'ToDo'
INPROGRESS = 'InProgress' INPROGRESS = 'In Progress'
FINISHED = 'Finished' FINISHED = 'Finished'
@ -50,18 +65,22 @@ class Issue(models.Model):
title = models.CharField(max_length=255, verbose_name='title') title = models.CharField(max_length=255, verbose_name='title')
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
description = models.TextField() description = models.TextField()
project = models.ForeignKey(Project, null=True, on_delete=models.SET_NULL, blank=True) status = models.CharField(choices=Status.choices, max_length=15)
status = models.CharField(Status.choices, max_length=15) priority = models.CharField(choices=Priority.choices, max_length=15)
priority = models.CharField(Priority.choices, max_length=15) tag = models.CharField(choices=Tag.choices, max_length=15)
tag = models.CharField(Tag.choices, max_length=15) project = models.ForeignKey(Project,
on_delete=models.CASCADE)
author = models.ForeignKey('Contributor', on_delete=models.DO_NOTHING) author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.DO_NOTHING,
related_name='issue_author', null=True)
class Comment(models.Model): class Comment(models.Model):
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
description = models.CharField(max_length=4000) description = models.CharField(max_length=4000)
author = models.ForeignKey('Contributor', on_delete=models.DO_NOTHING)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE) issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.DO_NOTHING,
related_name='comment_author', null=True)

View File

@ -0,0 +1,19 @@
from rest_framework.permissions import BasePermission
from support.models import Project
class IsAuthor(BasePermission):
def has_object_permission(self, request, view, object):
return bool(request.user
and request.user.is_authenticated
and request.user == object.author
)
class IsContributor(BasePermission):
def has_object_permission(self, request, view, object):
return bool(request.user
and request.user.is_authenticated
and request.user in object.contributors.all()
)

View File

@ -0,0 +1,97 @@
from rest_framework.serializers import (ModelSerializer,
StringRelatedField,
SlugRelatedField,
SerializerMethodField,
ValidationError)
from support.models import Project, ProjectContributor, Issue, Comment
class ContributorSerializer(ModelSerializer):
class Meta:
model = ProjectContributor
fields = ['contributor', 'project', 'data']
class ContributorListSerializer(ModelSerializer):
class Meta:
model = ProjectContributor
fields = ['contributor']
class ProjectSerializer(ModelSerializer):
author = StringRelatedField(many=False)
contributors = SlugRelatedField(many=True,
read_only='True',
slug_field='username')
class Meta:
model = Project
fields = ['id', 'author', 'contributors', 'title', 'type', 'date_created']
def validate_title(self, value):
if Project.objects.filter(title=value).exists():
raise ValidationError("Project already exists.")
return value
class ProjectDetailSerializer(ModelSerializer):
contributors = SlugRelatedField(many=True,
read_only='True',
slug_field='username')
author = StringRelatedField(many=False)
issues = SerializerMethodField()
class Meta:
model = Project
fields = ['title',
'date_created', 'type',
'author', 'contributors', 'description', 'issues']
def get_issues(self, instance):
queryset = Issue.objects.filter(project=instance.pk)
serializer = IssueSerializer(queryset, many=True)
return serializer.data
class IssueSerializer(ModelSerializer):
author = StringRelatedField(many=False)
class Meta:
model = Issue
fields = ['id', 'title', 'project', 'date_created', 'priority',
'tag', 'status', 'author']
read_only_field = ['author']
def validate_author(self, data):
if Project.objects.filter(contributors=data.author).exists():
raise ValidationError("Requestor isn't contributor")
return data
def validate_project(self, data):
# if data['user'] not in data['project'].contributors:
# raise ValidationError("User must be a contributor to the project")
#print(data.project)
#if self.context['request'].user not in data.contributors:
# raise ValidationError("User must be a contributor to the project")
#print(self.get_contributors(data))
return data
class CommentListSerializer(ModelSerializer):
class Meta:
model = Comment
fields = ['title', 'date_created', 'author']
class CommentDetailSerializer(ModelSerializer):
class Meta:
model = Comment
fields = ['title', 'date_created', 'author', 'description']

View File

@ -1,3 +1,118 @@
from django.shortcuts import render from django.shortcuts import render
from rest_framework.serializers import raise_errors_on_nested_writes
from rest_framework.viewsets import ModelViewSet
from support.models import Project, ProjectContributor, Issue, Comment
from authentication.models import User
from support.serializers import (ProjectSerializer,
ProjectDetailSerializer,
ContributorSerializer,
IssueSerializer,
CommentListSerializer,
CommentDetailSerializer)
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import (IsAuthenticated,
IsAuthenticatedOrReadOnly)
from support.permissions import IsAuthor, IsContributor
from rest_framework.decorators import action
# Create your views here.
class ProjectViewSet(ModelViewSet):
permission_classes = [IsAuthenticatedOrReadOnly]
serializer_class = ProjectSerializer
detail_serializer_class = ProjectDetailSerializer
queryset = Project.objects.filter(active=True)
def get_serializer_class(self):
if self.action == 'retrieve':
return self.detail_serializer_class
return super().get_serializer_class()
def perform_create(self, serializer):
"""set authenticated user as author and contributor on creation"""
test = serializer.save(author=self.request.user)
data = {'contributor': self.request.user.id, 'project': test.id}
contributor_serializer = ContributorSerializer(data=data)
if contributor_serializer.is_valid():
contributor_serializer.save()
@action(detail=True, methods=['patch'],
permission_classes=[IsContributor],
basename='add_contributor')
def add_contributor(self, request, pk):
"""Create the user/project contributor's relation"""
if 'contributor' in request.data:
contributor = User.objects.get(username=request.data['contributor'])
data = {'contributor': contributor.id, 'project': pk}
serializer = ContributorSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(f"User {contributor} added",
status=status.HTTP_202_ACCEPTED)
return Response("This user is already contributing",
status=status.HTTP_226_IM_USED)
return Response(f"Key error;`contributor` is expected, "
f"not `{list(request.data)[0]}`",
status=status.HTTP_400_BAD_REQUEST)
class IssueViewSet(ModelViewSet):
permission_classes = [IsContributor]
serializer_class = IssueSerializer
def get_queryset(self):
project_id = int(self.request.GET.get('project'))
project = Project.objects.get(id=project_id)
self.check_object_permissions(self.request, project)
return Issue.objects.filter(project=project_id)
def get_contributors(self, project):
queryset = ProjectContributor.objects.filter(project=project)
contributors_serializer = ContributorSerializer(queryset, many=True)
return contributors_serializer.data
def create(self, request, *args, **kwargs):
print(request.data['project'])
project = Project.objects.get(id=request.data['project'])
serializer = IssueSerializer(data=request.data)
print(request.data['project'], type(request.data['project']))
print(self.get_contributors(request.data['project']))
if self.request.user in project.contributors:
if serializer.is_valid(raise_exception=True):
serializer.author = self.request.user
serializer.save()
response = {
"message": f"Issue created for project {project}",
"data": serializer.data
}
return Response(response, status = status.HTTP_201_CREATED)
#def perform_create(self, serializer):
# """set authenticated user as author and contributor on creation"""
# serializer.save(author=self.request.user)
class ContributorViewSet(ModelViewSet):
serializer_class = ContributorSerializer
queryset = ProjectContributor.objects.all()
class CommentViewSet(ModelViewSet):
serializer_class = CommentListSerializer
detail_serializer_class = CommentDetailSerializer
queryset = Comment.objects.all()
def get_serializer_class(self):
if self.action == 'retrieve':
return self.detail_serializer_class
return super().get_serializer_class()