mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-18 12:11:35 +00:00
Work on update order endpoint and taker requests
This commit is contained in:
parent
4d9a5023e0
commit
9ade961e0f
@ -24,8 +24,8 @@ class EUserAdmin(UserAdmin):
|
||||
|
||||
@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')
|
||||
list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at', 'invoice')
|
||||
list_display_links = ['id']
|
||||
pass
|
||||
|
||||
@admin.register(Profile)
|
||||
|
@ -44,11 +44,11 @@ class Order(models.Model):
|
||||
UPI = 15, 'Updated invoice'
|
||||
DIS = 16, 'In dispute'
|
||||
MLD = 17, 'Maker lost dispute'
|
||||
TLD = 18, 'Taker lost dispute'
|
||||
EXP = 19, 'Expired'
|
||||
# TLD = 18, 'Taker lost dispute'
|
||||
# EXP = 19, 'Expired'
|
||||
|
||||
# order info, id = models.CharField(max_length=64, unique=True, null=False)
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices, default=Status.WFB)
|
||||
# order info
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=int(Status.WFB))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField()
|
||||
|
||||
@ -79,6 +79,7 @@ class Order(models.Model):
|
||||
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
|
||||
@ -91,7 +92,7 @@ class Profile(models.Model):
|
||||
lost_disputes = models.PositiveIntegerField(null=False, default=0)
|
||||
|
||||
# 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)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
@ -102,13 +103,18 @@ class Profile(models.Model):
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
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):
|
||||
return self.user.username
|
||||
|
||||
# to display avatars in admin panel
|
||||
def get_avatar(self):
|
||||
if not self.avatar:
|
||||
return 'static/assets/avatars/unknown.png'
|
||||
return 'static/assets/misc/unknown_avatar.png'
|
||||
return self.avatar.url
|
||||
|
||||
# method to create a fake table field in read only mode
|
||||
|
@ -1,7 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Order
|
||||
|
||||
class OrderSerializer(serializers.ModelSerializer):
|
||||
class ListOrderSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Order
|
||||
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 Meta:
|
||||
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')
|
@ -1,9 +1,9 @@
|
||||
from django.urls import path
|
||||
from .views import MakeOrder, OrderView, UserGenerator, BookView
|
||||
from .views import OrderMakerView, OrderView, UserView, BookView
|
||||
|
||||
urlpatterns = [
|
||||
path('make/', MakeOrder.as_view()),
|
||||
path('order/', OrderView.as_view()),
|
||||
path('usergen/', UserGenerator.as_view()),
|
||||
path('make/', OrderMakerView.as_view()),
|
||||
path('order/', OrderView.as_view({'get':'get','post':'take_or_update'})),
|
||||
path('usergen/', UserView.as_view()),
|
||||
path('book/', BookView.as_view()),
|
||||
]
|
117
api/views.py
117
api/views.py
@ -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 import viewsets
|
||||
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 .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
|
||||
from .models import Order
|
||||
|
||||
from .nick_generator.nick_generator import NickGenerator
|
||||
@ -24,9 +26,27 @@ expiration_time = 8
|
||||
avatar_path = Path('frontend/static/assets/avatars')
|
||||
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.
|
||||
|
||||
class MakeOrder(APIView):
|
||||
class OrderMakerView(CreateAPIView):
|
||||
serializer_class = MakeOrderSerializer
|
||||
|
||||
def post(self,request):
|
||||
@ -41,17 +61,14 @@ class MakeOrder(APIView):
|
||||
satoshis = serializer.data.get('satoshis')
|
||||
is_explicit = serializer.data.get('is_explicit')
|
||||
|
||||
# query if the user is already a maker or taker, return error
|
||||
queryset = Order.objects.filter(maker=request.user.id)
|
||||
if queryset.exists():
|
||||
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)
|
||||
valid, response = validate_already_maker_or_taker(request)
|
||||
if not valid:
|
||||
return response
|
||||
|
||||
# Creates a new order in db
|
||||
order = Order(
|
||||
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,
|
||||
amount=amount,
|
||||
payment_method=payment_method,
|
||||
@ -65,11 +82,11 @@ class MakeOrder(APIView):
|
||||
if not serializer.is_valid():
|
||||
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):
|
||||
serializer_class = OrderSerializer
|
||||
class OrderView(viewsets.ViewSet):
|
||||
serializer_class = UpdateOrderSerializer
|
||||
lookup_url_kwarg = 'order_id'
|
||||
|
||||
def get(self, request, format=None):
|
||||
@ -81,15 +98,14 @@ class OrderView(APIView):
|
||||
# check if exactly one order is found in the db
|
||||
if len(order) == 1 :
|
||||
order = order[0]
|
||||
data = self.serializer_class(order).data
|
||||
data = ListOrderSerializer(order).data
|
||||
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
|
||||
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['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)
|
||||
|
||||
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'
|
||||
NickGen = NickGenerator(
|
||||
lang='English',
|
||||
@ -114,6 +171,7 @@ class UserGenerator(APIView):
|
||||
use_noun=True,
|
||||
max_num=999)
|
||||
|
||||
# Probably should be turned into a post method
|
||||
def get(self,request, format=None):
|
||||
'''
|
||||
Get a new user derived from a high entropy token
|
||||
@ -181,40 +239,39 @@ class UserGenerator(APIView):
|
||||
def delete(self,request):
|
||||
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
|
||||
# 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.
|
||||
logout(request)
|
||||
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)
|
||||
|
||||
class BookView(APIView):
|
||||
serializer_class = OrderSerializer
|
||||
class BookView(ListAPIView):
|
||||
serializer_class = ListOrderSerializer
|
||||
|
||||
def get(self,request, format=None):
|
||||
currency = request.GET.get('currency')
|
||||
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:
|
||||
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')
|
||||
book_data = []
|
||||
for order in queryset:
|
||||
data = OrderSerializer(order).data
|
||||
data = ListOrderSerializer(order).data
|
||||
user = User.objects.filter(id=data['maker'])
|
||||
if len(user) == 1:
|
||||
data['maker_nick'] = user[0].username
|
||||
# TODO avoid sending status and takers for book views
|
||||
#data.pop('status','taker')
|
||||
# Non participants should not see the status or who is the taker
|
||||
for key in ('status','taker'):
|
||||
del data[key]
|
||||
book_data.append(data)
|
||||
|
||||
return Response(book_data, status=status.HTTP_200_OK)
|
||||
|
15
dev_utils/reinitiate_db.sh
Normal file
15
dev_utils/reinitiate_db.sh
Normal 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
|
@ -91,7 +91,7 @@ export default class MakerPage extends Component {
|
||||
console.log(this.state)
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken},
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
|
||||
body: JSON.stringify({
|
||||
type: this.state.type,
|
||||
currency: this.state.currency,
|
||||
|
@ -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 { 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
|
||||
function pn(x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
@ -26,7 +43,7 @@ export default class OrderPage extends Component {
|
||||
statusText: data.status_message,
|
||||
type: data.type,
|
||||
currency: data.currency,
|
||||
currencyCode: (data.currency== 1 ) ? "USD": ((data.currency == 2 ) ? "EUR":"ETH"),
|
||||
currencyCode: this.getCurrencyCode(data.currency),
|
||||
amount: data.amount,
|
||||
paymentMethod: data.payment_method,
|
||||
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
|
||||
handleClickBackButton=()=>{
|
||||
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 (){
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
@ -53,7 +89,7 @@ export default class OrderPage extends Component {
|
||||
BTC {this.state.type ? " Sell " : " Buy "} Order
|
||||
</Typography>
|
||||
<Paper elevation={12} style={{ padding: 8,}}>
|
||||
<List component="nav" aria-label="mailbox folders">
|
||||
<List dense="true">
|
||||
<ListItem>
|
||||
<ListItemAvatar sx={{ width: 56, height: 56 }}>
|
||||
<Avatar
|
||||
@ -89,6 +125,12 @@ export default class OrderPage extends Component {
|
||||
{ this.state.takerNick!='None' ?
|
||||
<><ListItem>
|
||||
<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>
|
||||
<Divider /> </>: ""}
|
||||
</>
|
||||
@ -98,14 +140,16 @@ export default class OrderPage extends Component {
|
||||
<ListItemText primary={'#'+this.orderId} secondary="Order ID"/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
</Paper>
|
||||
|
||||
<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 item xs={12} align="center">
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>
|
||||
</Grid>
|
||||
</Paper>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
@ -74,7 +74,7 @@ export default class UserGenPage extends Component {
|
||||
this.setState({
|
||||
token: this.genBase62Token(32),
|
||||
})
|
||||
this.getGeneratedUser();
|
||||
this.reload_for_csrf_to_work();
|
||||
}
|
||||
|
||||
handleChangeToken=(e)=>{
|
||||
|
BIN
frontend/static/assets/misc/unknown_avatar.png
Normal file
BIN
frontend/static/assets/misc/unknown_avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
Loading…
Reference in New Issue
Block a user