Work on update order endpoint and taker requests

This commit is contained in:
Reckless_Satoshi 2022-01-04 16:13:08 -08:00
parent 4d9a5023e0
commit 9ade961e0f
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
10 changed files with 177 additions and 50 deletions

View File

@ -24,8 +24,8 @@ class EUserAdmin(UserAdmin):
@admin.register(Order) @admin.register(Order)
class OrderAdmin(admin.ModelAdmin): class OrderAdmin(admin.ModelAdmin):
list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at') list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at', 'invoice')
list_display_links = ('maker','taker') list_display_links = ['id']
pass pass
@admin.register(Profile) @admin.register(Profile)

View File

@ -44,11 +44,11 @@ class Order(models.Model):
UPI = 15, 'Updated invoice' UPI = 15, 'Updated invoice'
DIS = 16, 'In dispute' DIS = 16, 'In dispute'
MLD = 17, 'Maker lost dispute' MLD = 17, 'Maker lost dispute'
TLD = 18, 'Taker lost dispute' # TLD = 18, 'Taker lost dispute'
EXP = 19, 'Expired' # EXP = 19, 'Expired'
# order info, id = models.CharField(max_length=64, unique=True, null=False) # order info
status = models.PositiveSmallIntegerField(choices=Status.choices, default=Status.WFB) status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=int(Status.WFB))
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField() expires_at = models.DateTimeField()
@ -79,6 +79,7 @@ class Order(models.Model):
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): class Profile(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE) user = models.OneToOneField(User,on_delete=models.CASCADE)
# Ratings stored as a comma separated integer list # Ratings stored as a comma separated integer list
@ -91,7 +92,7 @@ class Profile(models.Model):
lost_disputes = models.PositiveIntegerField(null=False, default=0) lost_disputes = models.PositiveIntegerField(null=False, default=0)
# RoboHash # RoboHash
avatar = models.ImageField(default="static/assets/avatars/unknown.png", verbose_name='Avatar') avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar')
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs): def create_user_profile(sender, instance, created, **kwargs):
@ -102,13 +103,18 @@ class Profile(models.Model):
def save_user_profile(sender, instance, **kwargs): def save_user_profile(sender, instance, **kwargs):
instance.profile.save() instance.profile.save()
@receiver(pre_delete, sender=User)
def del_avatar_from_disk(sender, instance, **kwargs):
avatar_file=Path('frontend/' + instance.profile.avatar.url)
avatar_file.unlink() # FIX deleting user fails if avatar is not found
def __str__(self): def __str__(self):
return self.user.username return self.user.username
# to display avatars in admin panel # to display avatars in admin panel
def get_avatar(self): def get_avatar(self):
if not self.avatar: if not self.avatar:
return 'static/assets/avatars/unknown.png' return 'static/assets/misc/unknown_avatar.png'
return self.avatar.url return self.avatar.url
# method to create a fake table field in read only mode # method to create a fake table field in read only mode

View File

@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Order from .models import Order
class OrderSerializer(serializers.ModelSerializer): class ListOrderSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Order model = Order
fields = ('id','status','created_at','expires_at','type','currency','amount','payment_method','is_explicit','premium','satoshis','maker','taker') fields = ('id','status','created_at','expires_at','type','currency','amount','payment_method','is_explicit','premium','satoshis','maker','taker')
@ -9,4 +9,9 @@ class OrderSerializer(serializers.ModelSerializer):
class MakeOrderSerializer(serializers.ModelSerializer): class MakeOrderSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Order model = Order
fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis') fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis')
class UpdateOrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ('id','invoice')

View File

@ -1,9 +1,9 @@
from django.urls import path from django.urls import path
from .views import MakeOrder, OrderView, UserGenerator, BookView from .views import OrderMakerView, OrderView, UserView, BookView
urlpatterns = [ urlpatterns = [
path('make/', MakeOrder.as_view()), path('make/', OrderMakerView.as_view()),
path('order/', OrderView.as_view()), path('order/', OrderView.as_view({'get':'get','post':'take_or_update'})),
path('usergen/', UserGenerator.as_view()), path('usergen/', UserView.as_view()),
path('book/', BookView.as_view()), path('book/', BookView.as_view()),
] ]

View File

@ -1,11 +1,13 @@
from rest_framework import serializers, status from rest_framework import status
from rest_framework.generics import CreateAPIView, ListAPIView
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import viewsets
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 ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
from .models import Order from .models import Order
from .nick_generator.nick_generator import NickGenerator from .nick_generator.nick_generator import NickGenerator
@ -24,9 +26,27 @@ expiration_time = 8
avatar_path = Path('frontend/static/assets/avatars') avatar_path = Path('frontend/static/assets/avatars')
avatar_path.mkdir(parents=True, exist_ok=True) avatar_path.mkdir(parents=True, exist_ok=True)
def validate_already_maker_or_taker(request):
'''Checks if the user is already partipant of an order'''
queryset = Order.objects.filter(maker=request.user.id)
if queryset.exists():
return False, Response({'Bad Request':'You are already maker of an order'}, status=status.HTTP_400_BAD_REQUEST)
queryset = Order.objects.filter(taker=request.user.id)
if queryset.exists():
return False, Response({'Bad Request':'You are already taker of an order'}, status=status.HTTP_400_BAD_REQUEST)
return True, None
def validate_ln_invoice(invoice):
'''Checks if a LN invoice is valid'''
#TODO
return True
# Create your views here. # Create your views here.
class MakeOrder(APIView): class OrderMakerView(CreateAPIView):
serializer_class = MakeOrderSerializer serializer_class = MakeOrderSerializer
def post(self,request): def post(self,request):
@ -41,17 +61,14 @@ class MakeOrder(APIView):
satoshis = serializer.data.get('satoshis') satoshis = serializer.data.get('satoshis')
is_explicit = serializer.data.get('is_explicit') is_explicit = serializer.data.get('is_explicit')
# query if the user is already a maker or taker, return error valid, response = validate_already_maker_or_taker(request)
queryset = Order.objects.filter(maker=request.user.id) if not valid:
if queryset.exists(): return response
return Response({'Bad Request':'You are already maker of an order'},status=status.HTTP_400_BAD_REQUEST)
queryset = Order.objects.filter(taker=request.user.id)
if queryset.exists():
return Response({'Bad Request':'You are already taker of an order'},status=status.HTTP_400_BAD_REQUEST)
# Creates a new order in db # Creates a new order in db
order = Order( order = Order(
type=otype, type=otype,
status=int(Order.Status.PUB), # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond)
currency=currency, currency=currency,
amount=amount, amount=amount,
payment_method=payment_method, payment_method=payment_method,
@ -65,11 +82,11 @@ class MakeOrder(APIView):
if not serializer.is_valid(): if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)
return Response(OrderSerializer(order).data, status=status.HTTP_201_CREATED) return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
class OrderView(APIView): class OrderView(viewsets.ViewSet):
serializer_class = OrderSerializer serializer_class = UpdateOrderSerializer
lookup_url_kwarg = 'order_id' lookup_url_kwarg = 'order_id'
def get(self, request, format=None): def get(self, request, format=None):
@ -81,15 +98,14 @@ class OrderView(APIView):
# check if exactly one order is found in the db # check if exactly one order is found in the db
if len(order) == 1 : if len(order) == 1 :
order = order[0] order = order[0]
data = self.serializer_class(order).data data = ListOrderSerializer(order).data
nickname = request.user.username nickname = request.user.username
# Check if requester is participant in the order and add boolean to response
data['is_participant'] = (str(order.maker) == nickname or str(order.taker) == nickname)
#To do fix: data['status_message'] = Order.Status.get(order.status).label #To do fix: data['status_message'] = Order.Status.get(order.status).label
data['status_message'] = Order.Status.WFB.label # Hardcoded WFB, should use order.status value. data['status_message'] = Order.Status.WFB.label # Hardcoded WFB, should use order.status value.
# Check if requester is participant in the order and add boolean to response
data['is_participant'] = (str(order.maker) == nickname or str(order.taker) == nickname)
data['maker_nick'] = str(order.maker) data['maker_nick'] = str(order.maker)
data['taker_nick'] = str(order.taker) data['taker_nick'] = str(order.taker)
@ -105,7 +121,48 @@ class OrderView(APIView):
return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
class UserGenerator(APIView): def take_or_update(self, request, format=None):
order_id = request.GET.get(self.lookup_url_kwarg)
serializer = UpdateOrderSerializer(data=request.data)
order = Order.objects.get(id=order_id)
if serializer.is_valid():
invoice = serializer.data.get('invoice')
# If this is an empty POST request (no invoice), it must be taker request!
if not invoice and order.status == int(Order.Status.PUB):
valid, response = validate_already_maker_or_taker(request)
if not valid:
return response
order.taker = self.request.user
order.status = int(Order.Status.TAK)
data = ListOrderSerializer(order).data
# An invoice came in! update it
elif invoice:
if validate_ln_invoice(invoice):
order.invoice = invoice
#TODO Validate if request comes from PARTICIPANT AND BUYER
#If the order status was Payment Failed. Move foward to invoice Updated.
if order.status == int(Order.Status.FAI):
order.status = int(Order.Status.UPI)
else:
return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'})
# Something else is going on. Probably not allowed.
else:
return Response({'bad_request':'Not allowed'})
order.save()
return self.get(request)
class UserView(APIView):
lookup_url_kwarg = 'token' lookup_url_kwarg = 'token'
NickGen = NickGenerator( NickGen = NickGenerator(
lang='English', lang='English',
@ -114,6 +171,7 @@ class UserGenerator(APIView):
use_noun=True, use_noun=True,
max_num=999) max_num=999)
# Probably should be turned into a post method
def get(self,request, format=None): def get(self,request, format=None):
''' '''
Get a new user derived from a high entropy token Get a new user derived from a high entropy token
@ -181,40 +239,39 @@ class UserGenerator(APIView):
def delete(self,request): def delete(self,request):
user = User.objects.get(id = request.user.id) user = User.objects.get(id = request.user.id)
# TO DO. Pressing give me another will delete the logged in user # TO DO. Pressing "give me another" deletes the logged in user
# 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! # 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.unlink() # Unsafe if avatar does not exist.
logout(request) logout(request)
user.delete() user.delete()
return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_301_MOVED_PERMANENTLY) return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_302_FOUND)
return Response(status=status.HTTP_403_FORBIDDEN) return Response(status=status.HTTP_403_FORBIDDEN)
class BookView(APIView): class BookView(ListAPIView):
serializer_class = OrderSerializer serializer_class = ListOrderSerializer
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, status=0) # TODO status = 1 for orders that are Public queryset = Order.objects.filter(currency=currency, type=type, status=int(Order.Status.PUB))
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)
queryset = queryset.order_by('created_at') queryset = queryset.order_by('created_at')
book_data = [] book_data = []
for order in queryset: for order in queryset:
data = OrderSerializer(order).data data = ListOrderSerializer(order).data
user = User.objects.filter(id=data['maker']) user = User.objects.filter(id=data['maker'])
if len(user) == 1: if len(user) == 1:
data['maker_nick'] = user[0].username data['maker_nick'] = user[0].username
# TODO avoid sending status and takers for book views # Non participants should not see the status or who is the taker
#data.pop('status','taker') for key in ('status','taker'):
del data[key]
book_data.append(data) book_data.append(data)
return Response(book_data, status=status.HTTP_200_OK) return Response(book_data, status=status.HTTP_200_OK)

View File

@ -0,0 +1,15 @@
#!/bin/bash
rm db.sqlite3
rm -R api/migrations
rm -R frontend/migrations
rm -R frontend/static/assets/avatars
python3 manage.py makemigrations
python3 manage.py makemigrations api
python3 manage.py migrate
python3 manage.py createsuperuser
python3 manage.py runserver

View File

@ -91,7 +91,7 @@ export default class MakerPage extends Component {
console.log(this.state) console.log(this.state)
const requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken}, headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
body: JSON.stringify({ body: JSON.stringify({
type: this.state.type, type: this.state.type,
currency: this.state.currency, currency: this.state.currency,

View File

@ -2,6 +2,23 @@ import React, { Component } from "react";
import { Paper, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider} from "@material-ui/core" import { Paper, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider} from "@material-ui/core"
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
// pretty numbers // pretty numbers
function pn(x) { function pn(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@ -26,7 +43,7 @@ export default class OrderPage extends Component {
statusText: data.status_message, statusText: data.status_message,
type: data.type, type: data.type,
currency: data.currency, currency: data.currency,
currencyCode: (data.currency== 1 ) ? "USD": ((data.currency == 2 ) ? "EUR":"ETH"), currencyCode: this.getCurrencyCode(data.currency),
amount: data.amount, amount: data.amount,
paymentMethod: data.payment_method, paymentMethod: data.payment_method,
isExplicit: data.is_explicit, isExplicit: data.is_explicit,
@ -41,10 +58,29 @@ export default class OrderPage extends Component {
}); });
} }
// Gets currency code (3 letters) from numeric (e.g., 1 -> USD)
// Improve this function so currencies are read from json
getCurrencyCode(val){
return (val == 1 ) ? "USD": ((val == 2 ) ? "EUR":"ETH")
}
// Fix to use proper react props // Fix to use proper react props
handleClickBackButton=()=>{ handleClickBackButton=()=>{
window.history.back(); window.history.back();
} }
handleClickTakeOrderButton=()=>{
console.log(this.state)
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken},
body: JSON.stringify({}),
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((response) => response.json())
.then((data) => (console.log(data) & this.getOrderDetails(data.id)));
}
render (){ render (){
return ( return (
<Grid container spacing={1}> <Grid container spacing={1}>
@ -53,7 +89,7 @@ export default class OrderPage extends Component {
BTC {this.state.type ? " Sell " : " Buy "} Order BTC {this.state.type ? " Sell " : " Buy "} Order
</Typography> </Typography>
<Paper elevation={12} style={{ padding: 8,}}> <Paper elevation={12} style={{ padding: 8,}}>
<List component="nav" aria-label="mailbox folders"> <List dense="true">
<ListItem> <ListItem>
<ListItemAvatar sx={{ width: 56, height: 56 }}> <ListItemAvatar sx={{ width: 56, height: 56 }}>
<Avatar <Avatar
@ -89,6 +125,12 @@ export default class OrderPage extends Component {
{ this.state.takerNick!='None' ? { this.state.takerNick!='None' ?
<><ListItem> <><ListItem>
<ListItemText primary={this.state.takerNick} secondary="Order taker"/> <ListItemText primary={this.state.takerNick} secondary="Order taker"/>
<ListItemAvatar sx={{ width: 56, height: 56 }}>
<Avatar
alt={this.state.makerNick}
src={window.location.origin +'/static/assets/avatars/' + this.state.takerNick + '.png'}
/>
</ListItemAvatar>
</ListItem> </ListItem>
<Divider /> </>: ""} <Divider /> </>: ""}
</> </>
@ -98,14 +140,16 @@ export default class OrderPage extends Component {
<ListItemText primary={'#'+this.orderId} secondary="Order ID"/> <ListItemText primary={'#'+this.orderId} secondary="Order ID"/>
</ListItem> </ListItem>
</List> </List>
</Paper>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
{this.state.isParticipant ? "" : <Button variant='contained' color='primary' to='/home' component={Link}>Take Order</Button>} {this.state.isParticipant ? "" : <Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>}
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button> <Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>
</Grid> </Grid>
</Paper>
</Grid> </Grid>
</Grid> </Grid>
); );

View File

@ -74,7 +74,7 @@ export default class UserGenPage extends Component {
this.setState({ this.setState({
token: this.genBase62Token(32), token: this.genBase62Token(32),
}) })
this.getGeneratedUser(); this.reload_for_csrf_to_work();
} }
handleChangeToken=(e)=>{ handleChangeToken=(e)=>{

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB