Extend user model with profiles and set up admin panel

This commit is contained in:
Reckless_Satoshi
2022-01-04 05:47:37 -08:00
parent 0823febf73
commit 5b712b0765
6 changed files with 70 additions and 19 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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>

View File

@ -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 >

View File

@ -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">