Skip to content

Commit

Permalink
Grouping feature (#719)
Browse files Browse the repository at this point in the history
* inital setup

* code fix

* 3rd page code

* backend code

* frontend

* backend-2

* frontend 2

* package.json

* team form

* team profile page

* stuck

* restore old ui

* migration fixes

* tested backend

* form all 3 steps and profile page

* edit and delete api integration

* call for testing

* step 3 and teams page

* backend for all teams

* migration files

* url change for all teams

* rebased code

* UI changes

* UX fixes

* edit-add projects api change

* rebase

* trying to fix not clickable issue

* adding team_enabled flag

* Revert "adding team_enabled flag"

This reverts commit 9bc54af.

* updating the TEAM_ENABLED flag
  • Loading branch information
iamhks authored Sep 18, 2023
1 parent baf7051 commit 7aa0021
Show file tree
Hide file tree
Showing 53 changed files with 19,328 additions and 162 deletions.
4 changes: 2 additions & 2 deletions zubhub_backend/zubhub/creators/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ class CreatorTagAdmin(admin.ModelAdmin):


class CreatorGroupAdmin(admin.ModelAdmin):
list_display = ["creator", group_projects, group_members, 'created_on']
search_fields = ['creator']
list_display = ('groupname', 'description', 'created_on')
search_fields = ['groupname']
list_filter = ["created_on"]

def get_readonly_fields(self, request, obj=None):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generated by Django 3.2 on 2023-07-27 18:38

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('creators', '0010_merge_20220921_2226'),
]

operations = [
migrations.DeleteModel(
name='CreatorGroup',
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated by Django 3.2 on 2023-08-17 15:33

from django.conf import settings
import django.contrib.postgres.indexes
import django.contrib.postgres.search
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import uuid


class Migration(migrations.Migration):

dependencies = [
('creators', '0011_delete_creatorgroup'),
]

operations = [
migrations.CreateModel(
name='CreatorGroup',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('groupname', models.CharField(error_messages={'unique': 'A group with that groupname already exists.'}, max_length=150, unique=True, verbose_name='groupname')),
('description', models.CharField(blank=True, max_length=10000, null=True)),
('created_on', models.DateTimeField(default=django.utils.timezone.now)),
('projects', models.JSONField(default=list)),
('projects_count', models.IntegerField(blank=True, default=0)),
('avatar', models.URLField(blank=True, max_length=1000, null=True)),
('search_vector', django.contrib.postgres.search.SearchVectorField(null=True)),
('followers_count', models.IntegerField(blank=True, default=0)),
('badges', models.ManyToManyField(blank=True, related_name='creator_group', to='creators.Badge')),
('followers', models.ManyToManyField(blank=True, related_name='following_group', to=settings.AUTH_USER_MODEL)),
('members', models.ManyToManyField(blank=True, related_name='group_members', to='creators.CreatorGroup')),
('tags', models.ManyToManyField(blank=True, related_name='creator_group', to='creators.CreatorTag')),
],
),
migrations.CreateModel(
name='CreatorGroupMembership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(choices=[('admin', 'Admin'), ('member', 'Member')], default='member', max_length=10)),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='creators.creatorgroup')),
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='group_memberships', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddIndex(
model_name='creatorgroup',
index=django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='creators_cr_search__188da8_gin'),
),
]
84 changes: 63 additions & 21 deletions zubhub_backend/zubhub/creators/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,40 +137,64 @@ def __str__(self):


class CreatorGroup(models.Model):
creator = models.OneToOneField(
Creator, on_delete=models.CASCADE, primary_key=True)
id = models.UUIDField(
primary_key=True, default=uuid.uuid4, editable=False, unique=True)
groupname = models.CharField(
max_length=150,
unique=True,
error_messages={
'unique': _('A group with that groupname already exists.'),
},
verbose_name=_('groupname')
)
description = models.CharField(max_length=10000, blank=True, null=True)
members = models.ManyToManyField(
Creator, blank=True, related_name="creator_groups")
'self', symmetrical=False, blank=True, related_name="group_members"
)
created_on = models.DateTimeField(default=timezone.now)
projects = models.JSONField(default=list) # Storing project IDs as a JSON list
projects_count = models.IntegerField(blank=True, default=0)
badges = models.ManyToManyField(Badge, blank=True, related_name="creator_group" )
tags = models.ManyToManyField(CreatorTag, blank=True, related_name="creator_group")
avatar = models.URLField(max_length=1000, blank=True, null=True)
search_vector = SearchVectorField(null=True)
followers = models.ManyToManyField(
Creator, symmetrical=False, blank=True, related_name="following_group")
followers_count = models.IntegerField(blank=True, default=0)

class Meta:
indexes = (GinIndex(fields=["search_vector"]),)

def __str__(self):
return self.creator.username
return self.groupname

def save(self, *args, **kwargs):
if not self.avatar:
self.avatar = 'https://robohash.org/{0}'.format(self.groupname)

self.followers_count = self.followers.count()
if self.projects:
self.projects_count = len(self.projects)
else:
self.projects_count = 0
super().save(*args, **kwargs)

def get_projects(self, **kwargs):
limit = kwargs.get("limit")
projects = self.creator.projects.all()
count = 0
members = self.members.prefetch_related("projects")
for member in members.all():
if not projects:
projects = member.projects.all()
else:
projects |= member.projects.all()

if projects:
projects = projects.order_by("-created_on")
count = projects.count()
if limit:
projects = projects[0:int(limit)]

# update projects_count if neccessary
# Retrieve the project IDs from the 'projects' field
project_ids = self.projects

count = len(project_ids)
if limit:
# Return the first 'limit' project IDs
project_ids = project_ids[:limit]

# update projects_count if necessary
if self.projects_count != count:
self.projects_count = count
self.save()

return projects
return project_ids

def send_group_invite_confirmation(self, **kwargs):
group_invite_confirmation = GroupInviteConfirmationHMAC(
Expand All @@ -179,6 +203,24 @@ def send_group_invite_confirmation(self, **kwargs):
return group_invite_confirmation


class CreatorGroupMembership(models.Model):
ROLE_CHOICES = [
('admin', 'Admin'),
('member', 'Member'),
]

group = models.ForeignKey(
CreatorGroup, on_delete=models.CASCADE, related_name='memberships'
)
member = models.ForeignKey(
Creator, on_delete=models.CASCADE, related_name='group_memberships'
)
role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='member')

def __str__(self):
return f"{self.member.username} - {self.role}"


class PhoneNumber(models.Model):

user = models.ForeignKey(
Expand Down
3 changes: 3 additions & 0 deletions zubhub_backend/zubhub/creators/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@

class CreatorNumberPagination(PageNumberPagination):
page_size = 20

class CreatorGroupNumberPagination(PageNumberPagination):
page_size = 20
7 changes: 7 additions & 0 deletions zubhub_backend/zubhub/creators/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ class IsOwner(BasePermission):

def has_object_permission(self, request, view, objects):
return objects.pk == request.user.pk

class IsGroupAdmin(BasePermission):
message = 'You must be an admin of this group to perform this action.'

def has_object_permission(self, request, view, obj):
# Check if the authenticated user is an admin in the group
return obj.memberships.filter(member=request.user, role='admin').exists()
119 changes: 100 additions & 19 deletions zubhub_backend/zubhub/creators/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from django.contrib.auth import get_user_model

from django.utils.translation import gettext_lazy as _
import csv
from .admin import badges
from .models import CreatorGroup, Creator, CreatorGroupMembership
from .models import Location, PhoneNumber
from allauth.account.models import EmailAddress
from rest_auth.registration.serializers import RegisterSerializer
Expand All @@ -15,6 +17,7 @@
from .utils import setup_user_phone
from projects.models import Comment
from projects.utils import parse_comment_trees
from rest_framework.validators import UniqueValidator
Creator = get_user_model()


Expand Down Expand Up @@ -79,6 +82,17 @@ def get_profile_comments(self, obj):
return parse_comment_trees(user, root_comments, creators_dict)


class CreatorGroupMinimalSerializer(serializers.ModelSerializer):
class Meta:
model = CreatorGroup
fields = '__all__'

class CreatorGroupMembershipSerializer(serializers.ModelSerializer):
class Meta:
model = CreatorGroupMembership
fields = ('member', 'role')


class CreatorSerializer(CreatorMinimalSerializer):
phone = serializers.CharField(allow_blank=True, default="")
email = serializers.EmailField(allow_blank=True, default="")
Expand All @@ -91,16 +105,18 @@ class CreatorSerializer(CreatorMinimalSerializer):
badges = serializers.SlugRelatedField(slug_field="badge_title",
read_only=True,
many=True)
group_memberships = CreatorGroupMembershipSerializer(many=True, read_only=True)


class Meta:
model = Creator

fields = ('id', 'username', 'email', 'phone', 'avatar', 'location',
'comments', 'dateOfBirth', 'bio', 'followers',
'following_count', 'projects_count', 'members_count', 'tags', 'badges')
'following_count', 'projects_count', 'members_count', 'tags', 'badges', 'group_memberships')

read_only_fields = [
"id", "projects_count", "following_count", "dateOfBirth", "tags", "badges"
"id", "projects_count", "following_count", "dateOfBirth", "tags", "badges", "group_memberships"
]

def validate_email(self, email):
Expand Down Expand Up @@ -255,26 +271,22 @@ class ConfirmGroupInviteSerializer(serializers.Serializer):
key = serializers.CharField()


class MemberRoleSerializer(serializers.Serializer):
member = serializers.CharField()
role = serializers.ChoiceField(choices=[('admin', 'Admin'), ('member', 'Member')])


class AddGroupMembersSerializer(serializers.Serializer):
group_members = serializers.JSONField(required=False, allow_null=True)
csv = serializers.FileField(required=False,
allow_null=True,
allow_empty_file=True)
group_members = MemberRoleSerializer(many=True, required=True)

def validate_group_members(self, group_members):
if (len(group_members) == 0 and not self.initial_data.get("csv")):
raise serializers.ValidationError(
_("you must submit group member usernames either through the form or as csv"
))
return group_members
def validate(self, data):
group_members = data.get('group_members', [])

def validate_csv(self, csv):
if (not csv and len(self.initial_data.get("group_members")) == 0):
raise serializers.ValidationError(
_("you must submit group member usernames either through the form or as csv"
))
return csv
if not group_members:
raise serializers.ValidationError(_("You must submit at least one group member."))

return data


class CustomPasswordResetSerializer(PasswordResetSerializer):
password_reset_form_class = PasswordResetForm
Expand All @@ -289,3 +301,72 @@ def validate_email(self, value):
raise serializers.ValidationError(
_('No account found with this email. Verify and try again.'))
return value


class CreatorGroupWithMembershipsSerializer(serializers.ModelSerializer):
members = CreatorGroupMembershipSerializer(many=True)

class Meta:
model = CreatorGroup
fields = ('groupname', 'description', 'members', 'created_on', 'projects_count')

def to_representation(self, instance):
data = super().to_representation(instance)
data['members'] = CreatorGroupMembershipSerializer(instance.memberships.all(), many=True).data
return data


class CreatorGroupSerializer(serializers.ModelSerializer):
members = CreatorGroupMembershipSerializer(many=True, read_only=True)
groupname = serializers.CharField(
max_length=150,
validators=[UniqueValidator(queryset=CreatorGroup.objects.all())],
error_messages={
'unique': _('A group with that groupname already exists.'),
}
)

class Meta:
model = CreatorGroup
fields = ('groupname', 'description', 'projects','members', 'created_on', 'projects_count', 'avatar')

def create(self, validated_data):
members_data = validated_data.pop('members', [])
group = CreatorGroup.objects.create(**validated_data)
for member_data in members_data:
member = member_data['member']
role = member_data.get('role', 'member') # Default role is 'member' if not provided
CreatorGroupMembership.objects.create(group=group, member=member, role=role)
return group

def to_representation(self, instance):
data = super().to_representation(instance)
data['members'] = CreatorGroupMembershipSerializer(instance.memberships.all(), many=True).data
return data

def update(self, instance, validated_data):
instance.groupname = validated_data.get('groupname', instance.groupname)
instance.description = validated_data.get('description', instance.description)
instance.save()

members_data = validated_data.pop('members', [])
existing_members = set(instance.members.all())

for member_data in members_data:
member = member_data['member']
role = member_data.get('role', 'member') # Default role is 'member' if not provided

# Check if the member is already part of the group
if member in existing_members:
membership = CreatorGroupMembership.objects.get(group=instance, member=member)
membership.role = role
membership.save()
else:
# If the member is not part of the group, add them with the specified role
CreatorGroupMembership.objects.create(group=instance, member=member, role=role)

# Remove any members who were not included in the updated data
members_to_remove = existing_members - set([member_data['member'] for member_data in members_data])
CreatorGroupMembership.objects.filter(group=instance, member__in=members_to_remove).delete()

return instance
Loading

0 comments on commit 7aa0021

Please sign in to comment.