Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Grouping feature #719

Merged
merged 35 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1df76b2
inital setup
iamhks Jul 13, 2023
ddc3521
code fix
iamhks Jul 17, 2023
c79cb46
3rd page code
iamhks Jul 17, 2023
f69bb06
backend code
iamhks Jul 20, 2023
af76d8c
frontend
iamhks Jul 23, 2023
c6ac9c3
backend-2
iamhks Jul 23, 2023
fe0671c
frontend 2
iamhks Jul 24, 2023
8b4ed90
package.json
iamhks Jul 24, 2023
71dbc49
team form
iamhks Jul 25, 2023
78dc915
team profile page
iamhks Jul 25, 2023
32f8515
stuck
iamhks Jul 26, 2023
bfbfff3
restore old ui
iamhks Jul 28, 2023
b7324d4
migration fixes
iamhks Jul 28, 2023
aa9842e
tested backend
iamhks Aug 3, 2023
950721f
form all 3 steps and profile page
iamhks Aug 12, 2023
9112df0
edit and delete api integration
iamhks Aug 13, 2023
04b2508
call for testing
iamhks Aug 14, 2023
ad65e9b
step 3 and teams page
iamhks Aug 17, 2023
fe30318
backend for all teams
iamhks Aug 17, 2023
7ca58a6
migration files
iamhks Aug 17, 2023
a45bf6a
url change for all teams
iamhks Aug 18, 2023
9c76954
Merge branch 'master' of https://github.com/iamhks/zubhub into groupi…
iamhks Aug 25, 2023
0386929
Merge branch 'master' of https://github.com/iamhks/zubhub into groupi…
iamhks Aug 27, 2023
2188a56
rebased code
iamhks Aug 27, 2023
1a3ebe6
Merge pull request #1 from iamhks/master
iamhks Sep 3, 2023
1c107f7
UI changes
iamhks Sep 4, 2023
441337c
UX fixes
iamhks Sep 7, 2023
ed97ab0
edit-add projects api change
iamhks Sep 7, 2023
4ac6b5a
rebase
iamhks Sep 9, 2023
f4eba11
Merge pull request #2 from iamhks/master
iamhks Sep 9, 2023
bad5c2f
trying to fix not clickable issue
iamhks Sep 9, 2023
65de286
Merge branch 'grouping-feature' of https://github.com/iamhks/zubhub i…
iamhks Sep 9, 2023
9bc54af
adding team_enabled flag
iamhks Sep 10, 2023
185b126
Revert "adding team_enabled flag"
iamhks Sep 15, 2023
ecad2e3
updating the TEAM_ENABLED flag
iamhks Sep 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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',
),
]
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(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tuxology is there a auto formatter for python? The indentation/styling seems to be off

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
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 @@ -246,26 +262,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 @@ -280,3 +292,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')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think group_name is more readable


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', 'members', 'created_on', 'projects_count')

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
25 changes: 25 additions & 0 deletions zubhub_backend/zubhub/creators/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ def upload_file_task(self, user_id, username):
except Exception as e:
raise self.retry(exc=e, countdown=int(
uniform(2, 4) ** self.request.retries))


@shared_task(name="creators.tasks.upload_file_task_group", bind=True, acks_late=True, max_retries=10)
def upload_file_task_group(self, user_id, username):
from creators.models import CreatorGroup

creator = CreatorGroup.objects.filter(id=user_id)

key = 'avatar/{0}'.format(username)

try:
res = requests.get(creator[0].avatar)
res = upload_file_to_media_server(res.content, key)
res = res.json()
res = res["url"]

if isinstance(res, str):
creator.update(avatar=res)
else:
raise Exception()

except Exception as e:
raise self.retry(exc=e, countdown=int(
uniform(2, 4) ** self.request.retries))


@shared_task(name="creators.tasks.update_creator_tag_index_task", bind=True, acks_late=True, max_retries=10)
def update_creator_tag_index_task(self):
Expand Down
Loading