diff --git a/apiserver/apiserver/api/models.py b/apiserver/apiserver/api/models.py index 4a66f40..5a3b600 100644 --- a/apiserver/apiserver/api/models.py +++ b/apiserver/apiserver/api/models.py @@ -19,6 +19,7 @@ def today_alberta_tz(): class Member(models.Model): user = models.OneToOneField(User, related_name='member', blank=True, null=True, on_delete=models.SET_NULL) signup_helper = models.ForeignKey(User, related_name='signed_up', blank=True, null=True, on_delete=models.SET_NULL) + sponsorship = models.ManyToManyField('self', related_name='sponsored_by', symmetrical=False, blank=True) old_email = models.CharField(max_length=254, blank=True, null=True) photo_large = models.CharField(max_length=64, blank=True, null=True) photo_medium = models.CharField(max_length=64, blank=True, null=True) @@ -331,4 +332,4 @@ class HistoryChange(models.Model): list_display = ['field', 'old', 'new', 'index'] search_fields = ['field', 'old', 'new', 'index__history_user__username'] def __str__(self): - return self.field + return self.field \ No newline at end of file diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py index 3b5c960..c264306 100644 --- a/apiserver/apiserver/api/serializers.py +++ b/apiserver/apiserver/api/serializers.py @@ -248,6 +248,8 @@ class MemberSerializer(serializers.ModelSerializer): email = fields.UserEmailField(serializers.EmailField) phone = serializers.CharField() protocoin = serializers.SerializerMethodField() + sponsorship = OtherMemberSerializer(many=True, read_only=True) + sponsored_by = OtherMemberSerializer(many=True, read_only=True) total_protocoin = serializers.SerializerMethodField() class Meta: diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py index 74f78e9..91ceaae 100644 --- a/apiserver/apiserver/api/views.py +++ b/apiserver/apiserver/api/views.py @@ -1895,6 +1895,24 @@ class StorageSpaceViewSet(Base, List, Retrieve, Update): return Response(200) +class SponsorshipViewSet(Base): + permission_classes = [AllowMetadata | IsAuthenticated] + queryset = models.Member.objects.all() + + @action(detail=True, methods=['post']) + def offer(self, request, pk=None): + value = self.request.data['value'] + member = self.get_object() + sponsor = self.request.user.member + if value == 'true': + sponsor.sponsorship.add(member.id) + logging.info('Member %s is now sponsoring member %s', sponsor.preferred_name, member.preferred_name) + else: + sponsor.sponsorship.remove(member.id) + logging.info('Member %s revoked sponsorship for member %s', sponsor.preferred_name, member.preferred_name) + sponsor.save() + return Response(200) + class RegistrationView(RegisterView): serializer_class = serializers.MyRegisterSerializer @@ -1917,4 +1935,4 @@ class MyLoginView(LoginView): @api_view() def null_view(request, *args, **kwargs): - raise Http404 + raise Http404 \ No newline at end of file diff --git a/apiserver/apiserver/urls.py b/apiserver/apiserver/urls.py index 27d1025..470e1a6 100644 --- a/apiserver/apiserver/urls.py +++ b/apiserver/apiserver/urls.py @@ -19,6 +19,7 @@ router.register(r'members', views.MemberViewSet, basename='members') router.register(r'courses', views.CourseViewSet, basename='course') router.register(r'history', views.HistoryViewSet, basename='history') router.register(r'vetting', views.VettingViewSet, basename='vetting') +router.register(r'sponsorship', views.SponsorshipViewSet, basename='sponsorship') router.register(r'pinball', views.PinballViewSet, basename='pinball') router.register(r'storage', views.StorageSpaceViewSet, basename='storage') router.register(r'hosting', views.HostingViewSet, basename='hosting') @@ -62,4 +63,4 @@ urlpatterns.append(path(ADMIN_ROUTE, admin.site.urls)) if settings.DEBUG: urlpatterns += [ path('api-auth/', include('rest_framework.urls')), - ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/webclient/src/Account.js b/webclient/src/Account.js index 2854e2c..6250829 100644 --- a/webclient/src/Account.js +++ b/webclient/src/Account.js @@ -6,7 +6,9 @@ import 'react-image-crop/dist/ReactCrop.css'; import './light.css'; import { MembersDropdown } from './Members.js'; import { Button, Container, Form, Grid, Header, Message, Segment } from 'semantic-ui-react'; +import './components/MembersList' import { requester, randomString } from './utils.js'; +import { MembersList } from './components/MembersList'; function LogoutEverywhere(props) { const { token } = props; @@ -335,6 +337,19 @@ export function AccountForm(props) { ); }; +export function Sponsorship(props) { + const { user: { member } } = props; + + return ( +
{member.public_bio || 'None yet.'}
+ { !isMe && !isSponsoring && } + { !isMe && isSponsoring && } } @@ -385,5 +407,4 @@ export function MemberDetail(props) { }