mirror of
https://github.com/RoboSats/robosats.git
synced 2025-08-04 19:40:05 +00:00
Extend user model with profiles and set up admin panel
This commit is contained in:
15
api/admin.py
15
api/admin.py
@ -1,3 +1,16 @@
|
|||||||
from django.contrib import admin
|
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)
|
@ -1,11 +1,14 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
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
|
# TODO
|
||||||
# Load hparams from .env file
|
# Load hparams from .env file
|
||||||
|
|
||||||
min_satoshis_trade = 10*1000
|
min_satoshis_trade = 10*1000
|
||||||
max_satoshis_trade = 500*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.
|
is_explicit = models.BooleanField(default=False, null=False) # pricing method. A explicit amount of sats, or a relative premium above/below market.
|
||||||
|
|
||||||
# order participants
|
# 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
|
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
|
# order collateral
|
||||||
@ -72,3 +75,33 @@ class Order(models.Model):
|
|||||||
# buyer payment LN invoice
|
# buyer payment LN invoice
|
||||||
has_invoice = models.BooleanField(default=False, null=False) # has invoice and is valid
|
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)
|
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.
|
18
api/views.py
18
api/views.py
@ -3,6 +3,7 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
from .serializers import OrderSerializer, MakeOrderSerializer
|
from .serializers import OrderSerializer, MakeOrderSerializer
|
||||||
from .models import Order
|
from .models import Order
|
||||||
@ -145,16 +146,20 @@ class UserGenerator(APIView):
|
|||||||
|
|
||||||
# generate avatar
|
# generate avatar
|
||||||
rh = Robohash(hash)
|
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!)
|
# Does not replace image if existing (avoid re-avatar in case of nick collusion)
|
||||||
with open(avatar_path.joinpath(nickname+".png"), "wb") as f:
|
|
||||||
|
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")
|
rh.img.save(f, format="png")
|
||||||
|
|
||||||
# Create new credentials if nickname is new
|
# Create new credentials and logsin if nickname is new
|
||||||
if len(User.objects.filter(username=nickname)) == 0:
|
if len(User.objects.filter(username=nickname)) == 0:
|
||||||
User.objects.create_user(username=nickname, password=token, is_staff=False)
|
User.objects.create_user(username=nickname, password=token, is_staff=False)
|
||||||
user = authenticate(request, username=nickname, password=token)
|
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)
|
login(request, user)
|
||||||
return Response(context, status=status.HTTP_201_CREATED)
|
return Response(context, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
@ -167,7 +172,7 @@ class UserGenerator(APIView):
|
|||||||
context['found'] = 'We found your Robosat. Welcome back!'
|
context['found'] = 'We found your Robosat. Welcome back!'
|
||||||
return Response(context, status=status.HTTP_202_ACCEPTED)
|
return Response(context, status=status.HTTP_202_ACCEPTED)
|
||||||
else:
|
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['found'] = 'Bad luck, this nickname is taken'
|
||||||
context['bad_request'] = 'Enter a different token'
|
context['bad_request'] = 'Enter a different token'
|
||||||
return Response(context, status=status.HTTP_403_FORBIDDEN)
|
return Response(context, status=status.HTTP_403_FORBIDDEN)
|
||||||
@ -179,6 +184,7 @@ class UserGenerator(APIView):
|
|||||||
# However it might be a long time recovered user
|
# However it might be a long time recovered user
|
||||||
# Only delete if user live is < 5 minutes
|
# 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:
|
if user is not None:
|
||||||
avatar_file = avatar_path.joinpath(str(request.user)+".png")
|
avatar_file = avatar_path.joinpath(str(request.user)+".png")
|
||||||
avatar_file.unlink() # Unsafe if avatar does not exist.
|
avatar_file.unlink() # Unsafe if avatar does not exist.
|
||||||
@ -195,7 +201,7 @@ class BookView(APIView):
|
|||||||
def get(self,request, format=None):
|
def get(self,request, format=None):
|
||||||
currency = request.GET.get('currency')
|
currency = request.GET.get('currency')
|
||||||
type = request.GET.get('type')
|
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:
|
if len(queryset)== 0:
|
||||||
return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ export default class BookPage extends Component {
|
|||||||
</Grid>
|
</Grid>
|
||||||
{this.state.orders.map((order) =>
|
{this.state.orders.map((order) =>
|
||||||
<Grid container item sm={4}>
|
<Grid container item sm={4}>
|
||||||
<Card elevation={6} sx={{ width: 245 }}>
|
<Card elevation={6} sx={{ width: 945 }}>
|
||||||
{/* Linking to order details not working yet as expected */}
|
{/* Linking to order details not working yet as expected */}
|
||||||
{/* <CardActionArea onClick={this.handleCardClick(15)} component={RouterLink} to="/order"> */}
|
{/* <CardActionArea onClick={this.handleCardClick(15)} component={RouterLink} to="/order"> */}
|
||||||
<CardActionArea>
|
<CardActionArea>
|
||||||
@ -139,8 +139,8 @@ export default class BookPage extends Component {
|
|||||||
<Typography variant="subtitle1" color="text.secondary">
|
<Typography variant="subtitle1" color="text.secondary">
|
||||||
◑ Priced {order.is_explicit ?
|
◑ Priced {order.is_explicit ?
|
||||||
" explicitly at " + this.pn(order.satoshis) + " Sats" : (
|
" explicitly at " + this.pn(order.satoshis) + " Sats" : (
|
||||||
" to market with " +
|
" at " +
|
||||||
parseFloat(parseFloat(order.premium).toFixed(4)) + "% premium"
|
parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market"
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ export default class MakerPage extends Component {
|
|||||||
premium: 0,
|
premium: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleClickIsExplicit=(e)=>{
|
handleClickExplicit=(e)=>{
|
||||||
this.setState({
|
this.setState({
|
||||||
isExplicit: true,
|
isExplicit: true,
|
||||||
satoshis: 10000,
|
satoshis: 10000,
|
||||||
@ -197,8 +197,7 @@ export default class MakerPage extends Component {
|
|||||||
control={<Radio color="secondary"/>}
|
control={<Radio color="secondary"/>}
|
||||||
label="Explicit"
|
label="Explicit"
|
||||||
labelPlacement="Top"
|
labelPlacement="Top"
|
||||||
onClick={this.handleClickisExplicit}
|
onClick={this.handleClickExplicit}
|
||||||
onShow="false"
|
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<FormHelperText >
|
<FormHelperText >
|
||||||
|
@ -131,9 +131,9 @@ export default class UserGenPage extends Component {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<ButtonGroup variant="contained" aria-label="outlined primary button group">
|
<ButtonGroup variant="contained" aria-label="outlined primary button group">
|
||||||
<Button color='primary' to='/make' component={Link}>Make Order</Button>
|
<Button color='primary' to='/make/' component={Link}>Make Order</Button>
|
||||||
<Button to='/home' component={Link}>INFO</Button>
|
<Button to='/home' component={Link}>INFO</Button>
|
||||||
<Button color='secondary' to='/book' component={Link}>View Book</Button>
|
<Button color='secondary' to='/book/' component={Link}>View Book</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
|
Reference in New Issue
Block a user