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.

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):
can_be_contacted = 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.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 datetime import timedelta
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@ -127,9 +128,11 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'authentication.User'
REST_FRAMERWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 5,
REST_FRAMEWORK = {
'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'))
"""
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 = [
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 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(Issue)
admin.site.register(User)
admin.site.register(Project, AdminProject)
admin.site.register(Issue, AdminIssue)
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,25 +14,40 @@ class Project(models.Model):
title = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True)
type = models.CharField(choices=Type.choices, max_length=10)
active = models.BooleanField(default=True)
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(
settings.AUTH_USER_MODEL, through='Contributor', related_name='contribution')
contributors = models.ManyToManyField(settings.AUTH_USER_MODEL,
through='ProjectContributor',
related_name='contribution')
def __str__(self):
return self.title
class Contributor(models.Model):
contributor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING)
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='project')
class ProjectContributor(models.Model):
contributor = models.ForeignKey(settings.AUTH_USER_MODEL,
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)
class Meta:
unique_together = ('contributor', 'project')
def __str__(self):
return self.contributor.username
class Issue(models.Model):
class Priority(models.TextChoices):
LOW = 'L'
MEDIUM = 'M'
HIGH = 'H'
LOW = 'Low'
MEDIUM = 'Medium'
HIGH = 'High'
class Status(models.TextChoices):
@ -50,18 +65,22 @@ class Issue(models.Model):
title = models.CharField(max_length=255, verbose_name='title')
date_created = models.DateTimeField(auto_now_add=True)
description = models.TextField()
project = models.ForeignKey(Project, null=True, on_delete=models.SET_NULL, blank=True)
status = models.CharField(Status.choices, max_length=15)
priority = models.CharField(Priority.choices, max_length=15)
tag = models.CharField(Tag.choices, max_length=15)
author = models.ForeignKey('Contributor', on_delete=models.DO_NOTHING)
status = models.CharField(choices=Status.choices, max_length=15)
priority = models.CharField(choices=Priority.choices, max_length=15)
tag = models.CharField(choices=Tag.choices, max_length=15)
project = models.ForeignKey(Project,
on_delete=models.CASCADE)
author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.DO_NOTHING,
related_name='issue_author', null=True)
class Comment(models.Model):
title = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True)
description = models.CharField(max_length=4000)
author = models.ForeignKey('Contributor', on_delete=models.DO_NOTHING)
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 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()