diff --git a/api/admin.py b/api/admin.py index 8c38f3f3..5bf592fe 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,3 +1,16 @@ from django.contrib import admin +from django.contrib.auth.models import Group +from .models import Order, Profile -# Register your models here. +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at') + list_display_links = ('maker','taker') + pass + +@admin.register(Profile) +class UserProfileAdmin(admin.ModelAdmin): + list_display = ('user','id','total_ratings','avg_rating','num_disputes','lost_disputes','avatar') + pass + +admin.site.unregister(Group) \ No newline at end of file diff --git a/api/models.py b/api/models.py index ad8028e9..cde408f1 100644 --- a/api/models.py +++ b/api/models.py @@ -1,11 +1,14 @@ from django.db import models from django.contrib.auth.models import User -from django.core.validators import MaxValueValidator, MinValueValidator +from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list +from django.db.models.signals import post_save, pre_delete +from django.dispatch import receiver + +from pathlib import Path ############################# # TODO # Load hparams from .env file - min_satoshis_trade = 10*1000 max_satoshis_trade = 500*1000 @@ -57,7 +60,7 @@ class Order(models.Model): is_explicit = models.BooleanField(default=False, null=False) # pricing method. A explicit amount of sats, or a relative premium above/below market. # order participants - maker = models.ForeignKey(User, related_name='maker', on_delete=models.SET_NULL, null=True, default=None) # unique = True, a maker can only make one order + maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, null=True, default=None) # unique = True, a maker can only make one order taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None) # unique = True, a taker can only take one order # order collateral @@ -72,3 +75,33 @@ class Order(models.Model): # buyer payment LN invoice has_invoice = models.BooleanField(default=False, null=False) # has invoice and is valid invoice = models.CharField(max_length=300, unique=False, null=True, default=None) + +class Profile(models.Model): + user = models.OneToOneField(User,on_delete=models.CASCADE) + + # Ratings stored as a comma separated integer list + total_ratings = models.PositiveIntegerField(null=False, default=0) + latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list]) # Will only store latest ratings + avg_rating = models.DecimalField(max_digits=3, decimal_places=1, default=None, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)]) + + # Disputes + num_disputes = models.PositiveIntegerField(null=False, default=0) + lost_disputes = models.PositiveIntegerField(null=False, default=0) + + # RoboHash + avatar = models.ImageField(default="static/assets/avatars/unknown.png") + + @receiver(post_save, sender=User) + def create_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) + + @receiver(post_save, sender=User) + def save_user_profile(sender, instance, **kwargs): + instance.profile.save() + + # Move avatar handling from views.py to here + # @receiver(pre_delete, sender=User) + # def _mymodel_delete(sender, instance, **kwargs): + # avatar_file = Path('frontend', instance.profile.avatar) + # avatar_file.unlink() # Unsafe if avatar does not exist. \ No newline at end of file diff --git a/api/views.py b/api/views.py index 358b7d1d..b4883db9 100644 --- a/api/views.py +++ b/api/views.py @@ -3,6 +3,7 @@ from rest_framework.views import APIView from rest_framework.response import Response from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User +from django.conf.urls.static import static from .serializers import OrderSerializer, MakeOrderSerializer from .models import Order @@ -145,16 +146,20 @@ class UserGenerator(APIView): # generate avatar rh = Robohash(hash) - rh.assemble(roboset='set1') # bgset='any' for backgrounds ON + rh.assemble(roboset='set1', bgset='any')# for backgrounds ON - # replaces image if existing (in of case nickname collusion avatar would change!) - with open(avatar_path.joinpath(nickname+".png"), "wb") as f: - rh.img.save(f, format="png") + # Does not replace image if existing (avoid re-avatar in case of nick collusion) - # Create new credentials if nickname is new + image_path = avatar_path.joinpath(nickname+".png") + if not image_path.exists(): + with open(image_path, "wb") as f: + rh.img.save(f, format="png") + + # Create new credentials and logsin if nickname is new if len(User.objects.filter(username=nickname)) == 0: User.objects.create_user(username=nickname, password=token, is_staff=False) user = authenticate(request, username=nickname, password=token) + user.profile.avatar = str(image_path)[9:] # removes frontend/ from url (ugly, to be fixed) login(request, user) return Response(context, status=status.HTTP_201_CREATED) @@ -167,7 +172,7 @@ class UserGenerator(APIView): context['found'] = 'We found your Robosat. Welcome back!' return Response(context, status=status.HTTP_202_ACCEPTED) else: - # It is unlikely (1/20 Billions) but maybe the nickname is taken + # It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) context['found'] = 'Bad luck, this nickname is taken' context['bad_request'] = 'Enter a different token' return Response(context, status=status.HTTP_403_FORBIDDEN) @@ -179,6 +184,7 @@ class UserGenerator(APIView): # However it might be a long time recovered user # Only delete if user live is < 5 minutes + # TODO check if user exists AND it is not a maker or taker! if user is not None: avatar_file = avatar_path.joinpath(str(request.user)+".png") avatar_file.unlink() # Unsafe if avatar does not exist. @@ -195,7 +201,7 @@ class BookView(APIView): def get(self,request, format=None): currency = request.GET.get('currency') type = request.GET.get('type') - queryset = Order.objects.filter(currency=currency, type=type) + queryset = Order.objects.filter(currency=currency, type=type, status=0) # TODO status = 1 for orders that are Public if len(queryset)== 0: return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND) diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index 1cc7c4e3..21e0c88f 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -103,7 +103,7 @@ export default class BookPage extends Component { {this.state.orders.map((order) => - + {/* Linking to order details not working yet as expected */} {/* */} @@ -139,8 +139,8 @@ export default class BookPage extends Component { ◑ Priced {order.is_explicit ? " explicitly at " + this.pn(order.satoshis) + " Sats" : ( - " to market with " + - parseFloat(parseFloat(order.premium).toFixed(4)) + "% premium" + " at " + + parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market" )} diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js index de0d97bb..61d38a46 100644 --- a/frontend/src/components/MakerPage.js +++ b/frontend/src/components/MakerPage.js @@ -79,7 +79,7 @@ export default class MakerPage extends Component { premium: 0, }); } - handleClickIsExplicit=(e)=>{ + handleClickExplicit=(e)=>{ this.setState({ isExplicit: true, satoshis: 10000, @@ -197,8 +197,7 @@ export default class MakerPage extends Component { control={} label="Explicit" labelPlacement="Top" - onClick={this.handleClickisExplicit} - onShow="false" + onClick={this.handleClickExplicit} /> diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index 83c57f4f..358f2188 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -131,9 +131,9 @@ export default class UserGenPage extends Component { - + - +