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 {
-
+
-
+