init
commit
7478caf99b
@ -0,0 +1,9 @@
|
||||
.venv
|
||||
__pycache__
|
||||
.DS_Store
|
||||
.idea
|
||||
*.pyc
|
||||
.env
|
||||
/data
|
||||
/media
|
||||
.coverage
|
@ -0,0 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from bids.models import ItemBid
|
||||
|
||||
|
||||
admin.site.register(ItemBid)
|
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BidsConfig(AppConfig):
|
||||
name = 'bids'
|
@ -0,0 +1,11 @@
|
||||
from django import forms
|
||||
from bids.models import ItemBid
|
||||
|
||||
|
||||
class CreateItemBidForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ItemBid
|
||||
fields = ['bid_price_xmr']
|
||||
labels = {
|
||||
'bid_price_xmr': 'Bid Price (XMR)'
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-13 05:46
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('items', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ItemBid',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('bid_date', models.DateTimeField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('bid_price_xmr', models.FloatField()),
|
||||
('accepted', models.BooleanField(default=False)),
|
||||
('bidder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bidder', to=settings.AUTH_USER_MODEL)),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bids', to='items.Item')),
|
||||
],
|
||||
),
|
||||
]
|
@ -0,0 +1,15 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from items.models import Item
|
||||
|
||||
|
||||
class ItemBid(models.Model):
|
||||
item = models.ForeignKey(Item, related_name='bids', on_delete=models.CASCADE)
|
||||
bidder = models.ForeignKey(User, related_name='bidder', on_delete=models.CASCADE)
|
||||
bid_date = models.DateTimeField(auto_now_add=True)
|
||||
last_updated = models.DateTimeField(auto_now=True)
|
||||
bid_price_xmr = models.FloatField()
|
||||
accepted = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id} - {self.item.name} - {self.bidder} > {self.item.owner}"
|
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.list_bids, name='list_bids'),
|
||||
path('<int:bid_id>/accept/', views.accept_bid, name='accept_bid'),
|
||||
path('<int:bid_id>/delete/', views.delete_bid, name='delete_bid'),
|
||||
path('<int:bid_id>/edit/', views.edit_bid, name='edit_bid'),
|
||||
path('item/<int:item_id>/create/', views.create_bid, name='create_bid'),
|
||||
]
|
@ -0,0 +1,169 @@
|
||||
from django.shortcuts import render, HttpResponseRedirect, reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.conf import settings
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from bids.forms import CreateItemBidForm
|
||||
from bids.models import ItemBid
|
||||
from sales.models import ItemSale
|
||||
from items.models import Item
|
||||
from core.monero import AuctionWallet
|
||||
|
||||
|
||||
@login_required
|
||||
def list_bids(request):
|
||||
page_query = request.GET.get('page', 1)
|
||||
bid_list = ItemBid.objects.filter(bidder=request.user)
|
||||
paginator = Paginator(bid_list, 20)
|
||||
|
||||
try:
|
||||
bids = paginator.page(page_query)
|
||||
except PageNotAnInteger:
|
||||
bids = paginator.page(1)
|
||||
except EmptyPage:
|
||||
bids = paginator.page(paginator.num_pages)
|
||||
|
||||
context = {
|
||||
'bids': bids
|
||||
}
|
||||
|
||||
return render(request, 'bids/list_bids.html', context)
|
||||
|
||||
@login_required
|
||||
def create_bid(request, item_id):
|
||||
item = Item.objects.get(id=item_id)
|
||||
current_user_bid = item.bids.filter(bidder=request.user).first()
|
||||
|
||||
# Do not allow bidding if current user is the owner
|
||||
if request.user == item.owner:
|
||||
messages.error(request, "You can't bid on an item you posted.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[item_id]))
|
||||
|
||||
# Do not allow bidding if item is not available
|
||||
if item.available is False:
|
||||
messages.error(request, "You can't bid on an item pending sale.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[item_id]))
|
||||
|
||||
# Redirect user to edit their existing bid if one exists
|
||||
if current_user_bid:
|
||||
return HttpResponseRedirect(reverse('edit_bid', args=[current_user_bid.id]))
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CreateItemBidForm(request.POST)
|
||||
if form.is_valid():
|
||||
bid = form.save(commit=False)
|
||||
bid.bidder = request.user
|
||||
bid.item = item
|
||||
bid.save()
|
||||
return HttpResponseRedirect(reverse('get_item', args=[item_id]))
|
||||
else:
|
||||
context = {
|
||||
'form': CreateItemBidForm(),
|
||||
'item': item
|
||||
}
|
||||
|
||||
return render(request, 'bids/create_bid.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_bid(request, bid_id):
|
||||
bid = ItemBid.objects.get(id=bid_id)
|
||||
|
||||
# Do not allow editing if current user doesn't own the bid
|
||||
if request.user != bid.bidder:
|
||||
messages.error(request, "You can't edit a bid that doesn't belong to you.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
||||
|
||||
# Do not allow editing if bid is accepted already
|
||||
if bid.accepted:
|
||||
messages.error(request, "You can't edit a bid that has already been accepted.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CreateItemBidForm(request.POST, instance=bid)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
||||
else:
|
||||
context = {
|
||||
'form': CreateItemBidForm(instance=bid),
|
||||
'bid': bid
|
||||
}
|
||||
|
||||
return render(request, 'bids/edit_bid.html', context)
|
||||
|
||||
@login_required
|
||||
def accept_bid(request, bid_id):
|
||||
aw = AuctionWallet()
|
||||
bid = ItemBid.objects.get(id=bid_id)
|
||||
platform_fee_xmr = bid.bid_price_xmr * (settings.PLATFORM_FEE_PERCENT / 100)
|
||||
expected_payment_xmr = bid.bid_price_xmr + platform_fee_xmr
|
||||
account_label = f'Sale account for Item #{bid.item.id}, Bid #{bid.id}'
|
||||
|
||||
# Do not allow accepting your own bid
|
||||
if request.user == bid.bidder:
|
||||
messages.error(request, "You can't accept your own bid.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
||||
|
||||
# Do not allow accepting the bid unless you own the item that received the bid
|
||||
if request.user != bid.item.owner:
|
||||
messages.error(request, "You can't accept a bid if you don't own the item.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
||||
|
||||
# Do not proceed if item is not available
|
||||
if bid.item.available is False:
|
||||
messages.error(request, "You can't accept the bid because the item is pending sale.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
||||
|
||||
# Do not proceed if bid is already accepted
|
||||
if bid.accepted:
|
||||
messages.error(request, "You can't accept a bid if it has already been accepted.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
||||
|
||||
# Do not proceed if there platform wallet is not connected
|
||||
if aw.connected is False:
|
||||
messages.error(request, "You can't accept the bid because the platform wallet is not properly connected.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
||||
|
||||
# Item becomes unavailable
|
||||
bid.item.available = False
|
||||
bid.item.save()
|
||||
|
||||
# Bid becomes accepted
|
||||
bid.accepted = True
|
||||
bid.save()
|
||||
|
||||
# Generate new Monero account for the sale
|
||||
new_account = aw.wallet.new_account(label=account_label)
|
||||
|
||||
# Sale is created
|
||||
sale = ItemSale(
|
||||
item=bid.item,
|
||||
bid=bid,
|
||||
escrow_address=new_account.address(),
|
||||
escrow_account_index=new_account.index,
|
||||
agreed_price_xmr=bid.bid_price_xmr,
|
||||
platform_fee_xmr=platform_fee_xmr,
|
||||
expected_payment_xmr=expected_payment_xmr
|
||||
)
|
||||
sale.save()
|
||||
|
||||
return HttpResponseRedirect(reverse('get_sale', args=[bid.id]))
|
||||
|
||||
@login_required
|
||||
def delete_bid(request, bid_id):
|
||||
bid = ItemBid.objects.get(id=bid_id)
|
||||
|
||||
# Do not allow deleting the bid unless you own the bid
|
||||
if request.user != bid.bidder:
|
||||
messages.error(request, "You can't delete a bid you did not create.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
||||
|
||||
# Do not allow deleting if the bid is accepted
|
||||
if bid.accepted:
|
||||
messages.error(request, "You can't delete a bid if it has been accepted.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
||||
|
||||
bid.delete()
|
||||
messages.success(request, f"Bid #{bid_id} on item \"{bid.item.name}\" ({bid.item.id}) has been deleted.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[bid.item.id]))
|
@ -0,0 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from core.models import UserShippingAddress
|
||||
|
||||
|
||||
admin.site.register(UserShippingAddress)
|
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
name = 'core'
|
@ -0,0 +1,20 @@
|
||||
from django import forms
|
||||
from core.models import UserShippingAddress
|
||||
|
||||
|
||||
class UserShippingAddressForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserShippingAddress
|
||||
fields = [
|
||||
'address1',
|
||||
'address2',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
'zip'
|
||||
]
|
||||
|
||||
labels = {
|
||||
'address1': 'Address',
|
||||
'address2': 'Address (additional info)'
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
from random import choice
|
||||
from secrets import token_urlsafe
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.timezone import now
|
||||
from django.contrib.auth.models import User
|
||||
from items.models import Item
|
||||
from bids.models import ItemBid
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Generates fake items within the application for testing'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('-i', '--items', type=int, help='Number of items to create', default=5)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
dummy_data = {
|
||||
'item_names': [
|
||||
'Do Androids Dream of Electric Sheep?',
|
||||
'The Hitchhiker\'s Guide to the Galaxy',
|
||||
'Something Wicked This Way Comes',
|
||||
'Pride and Prejudice and Zombies',
|
||||
'The Curious Incident of the Dog in the Night-Time',
|
||||
'I Was Told There\'d Be Cake',
|
||||
'To Kill a Mockingbird',
|
||||
'The Unbearable Lightness of Being',
|
||||
'Eats, Shoots & Leaves: The Zero Tolerance Approach to Punctuation',
|
||||
'The Hollow Chocolate Bunnies of the Apocalypse',
|
||||
'A Clockwork Orange',
|
||||
'Are You There, Vodka? It\'s Me, Chelsea'
|
||||
],
|
||||
'item_descriptions': [
|
||||
'Brand new, never opened or used.',
|
||||
'Light usage, good condition',
|
||||
'Spilled some water on it, fair condition, but good enough',
|
||||
'Mint condition - collectors item'
|
||||
],
|
||||
'item_ask_price': [
|
||||
'.1', '.23', '.51', '.233', '.47', '.09'
|
||||
],
|
||||
'new_items': []
|
||||
}
|
||||
|
||||
for index,value in enumerate(range(kwargs['items'])):
|
||||
random_item = choice(dummy_data['item_names'])
|
||||
random_desc = choice(dummy_data['item_descriptions'])
|
||||
random_price = choice(dummy_data['item_ask_price'])
|
||||
random_user = choice(User.objects.all())
|
||||
|
||||
item = Item(
|
||||
owner=random_user,
|
||||
name=random_item,
|
||||
description=random_desc,
|
||||
ask_price_xmr=random_price,
|
||||
)
|
||||
item.save()
|
||||
|
||||
dummy_data['new_items'].append(item)
|
||||
self.stdout.write(self.style.SUCCESS(f'Item "{item.name} ({item.id})" created successfully!'))
|
||||
|
||||
for i in dummy_data['new_items']:
|
||||
all_users = User.objects.all().exclude(username=i.owner.username)
|
||||
for u in all_users:
|
||||
bid = ItemBid(
|
||||
item=i,
|
||||
bidder=u,
|
||||
bid_price_xmr=i.ask_price_xmr
|
||||
)
|
||||
bid.save()
|
||||
self.stdout.write(self.style.SUCCESS(f'Bid #{bid.id} for user "{bid.bidder}" created successfully!'))
|
@ -0,0 +1,28 @@
|
||||
from django.shortcuts import HttpResponseRedirect, reverse
|
||||
from core.models import UserShippingAddress
|
||||
|
||||
|
||||
class EnforceShippingAddressCreationMiddleware(object):
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
|
||||
# If current user is authenticated, get their shipping information and current page
|
||||
# If current page is not them editing their address or logging out, redirect them
|
||||
if request.user.is_authenticated:
|
||||
profile = UserShippingAddress.objects.filter(user=request.user).first()
|
||||
is_profile_absent = profile is None
|
||||
|
||||
allowed_paths = [
|
||||
reverse('edit_shipping'),
|
||||
reverse('logout')
|
||||
]
|
||||
on_allowed_path = request.path not in allowed_paths
|
||||
|
||||
if is_profile_absent and on_allowed_path:
|
||||
return HttpResponseRedirect(reverse('edit_shipping'))
|
||||
|
||||
response = self.get_response(request)
|
||||
|
||||
return response
|
@ -0,0 +1,30 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-26 19:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserShippingAddress',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('address1', models.CharField(max_length=100)),
|
||||
('address2', models.CharField(blank=True, max_length=100)),
|
||||
('city', models.CharField(max_length=100)),
|
||||
('state', models.CharField(max_length=60)),
|
||||
('country', models.CharField(max_length=60)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||
('zip', models.PositiveIntegerField()),
|
||||
],
|
||||
),
|
||||
]
|
@ -0,0 +1,15 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class UserShippingAddress(models.Model):
|
||||
user = models.ForeignKey(User, related_name='profile', on_delete=models.CASCADE)
|
||||
address1 = models.CharField(max_length=100)
|
||||
address2 = models.CharField(max_length=100, blank=True)
|
||||
city = models.CharField(max_length=100)
|
||||
state = models.CharField(max_length=60)
|
||||
country = models.CharField(max_length=60)
|
||||
zip = models.PositiveIntegerField()
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
@ -0,0 +1,52 @@
|
||||
from django.conf import settings
|
||||
from monero.daemon import Daemon
|
||||
from monero.wallet import Wallet
|
||||
from monero.backends.jsonrpc import JSONRPCDaemon, JSONRPCWallet
|
||||
|
||||
|
||||
class AuctionDaemon(object):
|
||||
def __init__(self):
|
||||
self.host = settings.DAEMON_HOST
|
||||
self.port = settings.DAEMON_PORT
|
||||
self.username = settings.DAEMON_USER
|
||||
self.password = settings.DAEMON_PASS
|
||||
self.daemon = Daemon(JSONRPCDaemon(
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
user=self.username,
|
||||
password=self.password,
|
||||
timeout=5
|
||||
))
|
||||
|
||||
try:
|
||||
status = self.daemon.info()['status']
|
||||
if status == 'OK':
|
||||
self.connected = True
|
||||
else:
|
||||
self.connected = False
|
||||
except:
|
||||
self.connected = False
|
||||
|
||||
|
||||
|
||||
class AuctionWallet(object):
|
||||
def __init__(self):
|
||||
self.host = settings.WALLET_HOST
|
||||
self.port = settings.WALLET_PORT
|
||||
self.username = settings.WALLET_USER
|
||||
self.password = settings.WALLET_PASS
|
||||
|
||||
try:
|
||||
self.wallet = Wallet(JSONRPCWallet(
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
user=self.username,
|
||||
password=self.password,
|
||||
timeout=5
|
||||
))
|
||||
if self.wallet:
|
||||
self.connected = True
|
||||
else:
|
||||
self.connected = False
|
||||
except:
|
||||
self.connected = False
|
@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.home, name='home'),
|
||||
path('shipping/edit/', views.edit_shipping, name='edit_shipping')
|
||||
]
|
@ -0,0 +1,43 @@
|
||||
from django.shortcuts import render, HttpResponseRedirect, reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib import messages
|
||||
from core.forms import UserShippingAddressForm
|
||||
from core.models import UserShippingAddress
|
||||
from core.monero import AuctionDaemon
|
||||
|
||||
|
||||
def home(request):
|
||||
daemon = AuctionDaemon()
|
||||
|
||||
if daemon.connected:
|
||||
daemon_info = daemon.daemon.info()
|
||||
else:
|
||||
daemon_info = False
|
||||
|
||||
return render(request, 'home.html', {'daemon_info': daemon_info})
|
||||
|
||||
@login_required
|
||||
def edit_shipping(request):
|
||||
profile = UserShippingAddress.objects.filter(user=request.user).first()
|
||||
|
||||
if request.method == 'POST':
|
||||
form = UserShippingAddressForm(request.POST, instance=profile)
|
||||
if form.is_valid():
|
||||
saved_profile = form.save(commit=False)
|
||||
saved_profile.user = request.user
|
||||
saved_profile.save()
|
||||
messages.success(request, 'Profile updated.')
|
||||
return HttpResponseRedirect(reverse('home'))
|
||||
else:
|
||||
messages.error(request, 'Unable to save shipping information.')
|
||||
form_errors = form.errors.get_json_data()
|
||||
for err in form_errors:
|
||||
err_data = form_errors[err][0]
|
||||
messages.error(request, f'{err}: {err_data["message"]}')
|
||||
|
||||
context = {
|
||||
'form': UserShippingAddressForm(instance=profile)
|
||||
}
|
||||
|
||||
return render(request, 'core/edit_shipping.html', context)
|
@ -0,0 +1,16 @@
|
||||
version: '3'
|
||||
services:
|
||||
db:
|
||||
image: postgres:9.6.15-alpine
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASS}
|
||||
POSTGRES_USER: ${DB_USER}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
volumes:
|
||||
- ./data/postgresql:/var/lib/postgresql/data
|
||||
cache:
|
||||
image: redis:5.0.7-buster
|
||||
ports:
|
||||
- 6379:6379
|
@ -0,0 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from items.models import Item, ItemImage
|
||||
|
||||
|
||||
admin.site.register(Item)
|
||||
admin.site.register(ItemImage)
|
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ItemsConfig(AppConfig):
|
||||
name = 'items'
|
@ -0,0 +1,17 @@
|
||||
from django import forms
|
||||
from items.models import Item, address_is_valid_monero
|
||||
|
||||
|
||||
class CreateItemForm(forms.ModelForm):
|
||||
payout_address = forms.CharField(validators=[address_is_valid_monero])
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ['name', 'description', 'ask_price_xmr', 'payout_address']
|
||||
labels = {
|
||||
'ask_price_xmr': 'Asking Price (XMR)',
|
||||
'payout_address': 'Payout Wallet Address'
|
||||
}
|
||||
help_texts = {
|
||||
'payout_address': 'Monero address where funds will be sent after sale is confirmed'
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-24 08:08
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import items.models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Item',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('list_date', models.DateTimeField(auto_now_add=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('description', models.TextField(max_length=500)),
|
||||
('ask_price_xmr', models.FloatField()),
|
||||
('available', models.BooleanField(default=True)),
|
||||
('payout_address', models.CharField(max_length=100, validators=[items.models.address_is_valid_monero])),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ItemImage',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('image', models.ImageField(upload_to='%Y/%m/%d')),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='items.Item')),
|
||||
('thumbnail', models.ImageField(upload_to='%Y/%m/%d')),
|
||||
],
|
||||
),
|
||||
]
|
@ -0,0 +1,111 @@
|
||||
from os import path as os_path
|
||||
from secrets import token_urlsafe
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.models import User
|
||||
from monero.address import address
|
||||
from PIL import Image, ExifTags
|
||||
from io import BytesIO
|
||||
from core.monero import AuctionDaemon
|
||||
|
||||
|
||||
def address_is_valid_monero(value):
|
||||
try:
|
||||
address(value)
|
||||
return True
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
_('%(value)s is an invalid Monero address'),
|
||||
params={'value': value},
|
||||
)
|
||||
|
||||
class Item(models.Model):
|
||||
owner = models.ForeignKey(User, related_name='owner', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=100)
|
||||
list_date = models.DateTimeField(auto_now_add=True)
|
||||
last_updated = models.DateTimeField(auto_now=True)
|
||||
description = models.TextField(max_length=500)
|
||||
ask_price_xmr = models.FloatField()
|
||||
available = models.BooleanField(default=True)
|
||||
payout_address = models.CharField(max_length=100, validators=[address_is_valid_monero])
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id} - {self.owner} - {self.name}"
|
||||
|
||||
|
||||
class ItemImage(models.Model):
|
||||
item = models.ForeignKey(Item, related_name='images', on_delete=models.CASCADE)
|
||||
image = models.ImageField(upload_to='%Y/%m/%d')
|
||||
thumbnail = models.ImageField(upload_to='%Y/%m/%d')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.correct_image()
|
||||
self.copy_to_storage()
|
||||
super(ItemImage, self).save(*args, **kwargs)
|
||||
|
||||
def copy_to_storage(self):
|
||||
pass
|
||||
|
||||
def correct_image(self):
|
||||
try:
|
||||
# Open image and set some variables
|
||||
img = Image.open(self.image)
|
||||
img_format = img.format
|
||||
max_size = (800, 800)
|
||||
thumb_size = (150, 150)
|
||||
file_ext = os_path.splitext(self.image.name)[1]
|
||||
random_str = token_urlsafe(12)
|
||||
img_name = f'{self.item.id}-{random_str}.%s{file_ext}'
|
||||
img_bytes = BytesIO()
|
||||
thumb_bytes = BytesIO()
|
||||
|
||||
# If image contains exif check for orientation and rotate
|
||||
for orientation in ExifTags.TAGS.keys():
|
||||
if ExifTags.TAGS[orientation] == 'Orientation':
|
||||
img_exif = img._getexif()
|
||||
if img_exif:
|
||||
if orientation in img_exif:
|
||||
image_orientation = img_exif[orientation]
|
||||
if image_orientation == 3:
|
||||
img = img.rotate(180, expand=True)
|
||||
if image_orientation == 6:
|
||||
img = img.rotate(-90, expand=True)
|
||||
if image_orientation == 8:
|
||||
img = img.rotate(90, expand=True)
|
||||
|
||||
# Store a copy of the image for thumbnail
|
||||
thumb = img.copy()
|
||||
|
||||
# Correct the image size to safe maximums
|
||||
img.thumbnail(max_size, Image.ANTIALIAS)
|
||||
img.save(img_bytes, format=img_format, quality=80)
|
||||
self.image = InMemoryUploadedFile(
|
||||
img_bytes,
|
||||
'ImageField',
|
||||
img_name % 'full',
|
||||
self.image.file.content_type,
|
||||
img.size,
|
||||
self.image.file.charset
|
||||
)
|
||||
|
||||
# Create thumbnail from image
|
||||
thumb.thumbnail(thumb_size, Image.ANTIALIAS)
|
||||
thumb.save(thumb_bytes, format=img_format, quality=80)
|
||||
self.thumbnail = InMemoryUploadedFile(
|
||||
thumb_bytes,
|
||||
'ImageField',
|
||||
img_name % 'thumbnail',
|
||||
self.image.file.content_type,
|
||||
img.size,
|
||||
self.image.file.charset
|
||||
)
|
||||
|
||||
thumb.close()
|
||||
img.close()
|
||||
except:
|
||||
raise Exception('Unable to correct image size')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id} - {self.item.name} - {self.id}"
|
@ -0,0 +1,111 @@
|
||||
from secrets import token_urlsafe
|
||||
from django.test.utils import setup_test_environment
|
||||
from django.test import TestCase, Client
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.paginator import Page
|
||||
from django.urls import reverse
|
||||
from items.models import Item, ItemImage
|
||||
|
||||
|
||||
class ItemsTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.test_user_username = 'tester'
|
||||
self.test_user_password = token_urlsafe(32)
|
||||
self.test_user = User.objects.create_user(
|
||||
self.test_user_username,
|
||||
password=self.test_user_password
|
||||
)
|
||||
self.test_item = Item.objects.create(
|
||||
owner=self.test_user,
|
||||
name='Test Item',
|
||||
description='Test item',
|
||||
ask_price_xmr=0.3
|
||||
)
|
||||
|
||||
def login(self):
|
||||
self.client.login(
|
||||
username=self.test_user_username,
|
||||
password=self.test_user_password
|
||||
)
|
||||
|
||||
def logout(self):
|
||||
self.client.logout()
|
||||
|
||||
def test_list_items_should_allow_anonymous(self):
|
||||
response = self.client.get(reverse('list_items'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_get_item_should_allow_anonymous(self):
|
||||
response = self.client.get(reverse('get_item', args=[self.test_item.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_list_items_returns_page(self):
|
||||
response = self.client.get(reverse('list_items'))
|
||||
items = response.context['items']
|
||||
self.assertTrue(isinstance(items, Page))
|
||||
|
||||
def test_create_item_should_require_auth(self):
|
||||
no_auth_response = self.client.get(reverse('create_item'))
|
||||
self.login()
|
||||
auth_response = self.client.get(reverse('create_item'))
|
||||
self.logout()
|
||||
self.assertEqual(no_auth_response.status_code, 302)
|
||||
self.assertTrue(no_auth_response.url.startswith('/accounts/login'))
|
||||
self.assertEqual(auth_response.status_code, 200)
|
||||
|
||||
def test_edit_item_should_require_auth(self):
|
||||
no_auth_response = self.client.get(reverse('edit_item', args=[self.test_item.id]))
|
||||
self.login()
|
||||
auth_response = self.client.get(reverse('edit_item', args=[self.test_item.id]))
|
||||
self.logout()
|
||||
self.assertEqual(no_auth_response.status_code, 302)
|
||||
self.assertTrue(no_auth_response.url.startswith('/accounts/login'))
|
||||
self.assertEqual(auth_response.status_code, 200)
|
||||
|
||||
def test_edit_item_should_require_active_user_is_owner(self):
|
||||
new_user = User.objects.create_user(
|
||||
'tester2',
|
||||
password=token_urlsafe(24)
|
||||
)
|
||||
new_item = Item.objects.create(
|
||||
owner=new_user,
|
||||
name='Test Item 2',
|
||||
description='Test item 2',
|
||||
ask_price_xmr=0.3
|
||||
)
|
||||
self.login()
|
||||
test_item_edit_response = self.client.get(reverse('edit_item', args=[self.test_item.id]))
|
||||
new_item_edit_response = self.client.get(reverse('edit_item', args=[new_item.id]))
|
||||
self.logout()
|
||||
self.assertEqual(test_item_edit_response.status_code, 200)
|
||||
self.assertEqual(new_item_edit_response.status_code, 302)
|
||||
new_item.delete()
|
||||
new_user.delete()
|
||||
|
||||
def test_delete_item_should_require_auth(self):
|
||||
no_auth_response = self.client.get(reverse('delete_item', args=[self.test_item.id]))
|
||||
self.login()
|
||||
auth_response = self.client.get(reverse('delete_item', args=[self.test_item.id]))
|
||||
self.logout()
|
||||
self.assertEqual(no_auth_response.status_code, 302)
|
||||
self.assertTrue(no_auth_response.url.startswith('/accounts/login'))
|
||||
|
||||
def test_delete_item_should_require_active_user_is_owner(self):
|
||||
new_user = User.objects.create_user(
|
||||
'tester3',
|
||||
password=token_urlsafe(24)
|
||||
)
|
||||
new_item = Item.objects.create(
|
||||
owner=new_user,
|
||||
name='Test Item 3',
|
||||
description='Test item 3',
|
||||
ask_price_xmr=0.3
|
||||
)
|
||||
self.login()
|
||||
test_delete_item_response = self.client.get(reverse('delete_item', args=[self.test_item.id]))
|
||||
new_delete_item_response = self.client.get(reverse('delete_item', args=[new_item.id]))
|
||||
self.logout()
|
||||
self.assertEqual(test_delete_item_response.status_code, 302)
|
||||
self.assertEqual(new_delete_item_response.status_code, 302)
|
||||
new_item.delete()
|
||||
new_user.delete()
|
@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
from items import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.list_items, name='list_items'),
|
||||
path('create/', views.create_item, name='create_item'),
|
||||
path('<int:item_id>/', views.get_item, name='get_item'),
|
||||
path('<int:item_id>/edit/', views.edit_item, name='edit_item'),
|
||||
path('<int:item_id>/delete/', views.delete_item, name='delete_item'),
|
||||
]
|
@ -0,0 +1,144 @@
|
||||
from django.shortcuts import render, HttpResponseRedirect, reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib import messages
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.forms import inlineformset_factory
|
||||
from items.forms import CreateItemForm
|
||||
from items.models import Item, ItemImage
|
||||
from bids.models import ItemBid
|
||||
from sales.models import ItemSale
|
||||
|
||||
|
||||
def list_items(request):
|
||||
page_query = request.GET.get('page', 1)
|
||||
user_query = request.GET.get('user', 0)
|
||||
item_list = Item.objects.all().order_by('-list_date')
|
||||
|
||||
# If the user query string resolves to real user, show their items, otherwise show all
|
||||
if user_query:
|
||||
user = User.objects.filter(id=user_query).first()
|
||||
if user:
|
||||
item_list = Item.objects.filter(owner=user).order_by('-list_date')
|
||||
|
||||
paginator = Paginator(item_list, 20)
|
||||
|
||||
try:
|
||||
items = paginator.page(page_query)
|
||||
except PageNotAnInteger:
|
||||
items = paginator.page(1)
|
||||
except EmptyPage:
|
||||
items = paginator.page(paginator.num_pages)
|
||||
|
||||
context = {
|
||||
'items': items
|
||||
}
|
||||
|
||||
return render(request, 'items/list_items.html', context)
|
||||
|
||||
def get_item(request, item_id):
|
||||
item = Item.objects.get(id=item_id)
|
||||
item_images = item.images.all()
|
||||
item_bids = item.bids.all().order_by('-bid_price_xmr')
|
||||
|
||||
context = {
|
||||
'item': item,
|
||||
'item_images': item_images,
|
||||
'item_bids': item_bids
|
||||
}
|
||||
|
||||
return render(request, 'items/get_item.html', context)
|
||||
|
||||
@login_required
|
||||
def create_item(request):
|
||||
ItemImageFormSet = inlineformset_factory(Item, ItemImage, fields=('image',))
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CreateItemForm(request.POST)
|
||||
if form.is_valid():
|
||||
new_item = form.save(commit=False)
|
||||
new_item.owner = request.user
|
||||
formset = ItemImageFormSet(request.POST, request.FILES, instance=new_item)
|
||||
if formset.is_valid():
|
||||
new_item.save()
|
||||
formset.save()
|
||||
return HttpResponseRedirect(reverse('get_item', args=[new_item.id]))
|
||||
else:
|
||||
messages.error(request, "Unable to save images.")
|
||||
for err in formset.errors:
|
||||
messages.error(request, err)
|
||||
return HttpResponseRedirect(reverse('create_item'))
|
||||
else:
|
||||
form_errors = form.errors.get_json_data()
|
||||
for err in form_errors:
|
||||
err_data = form_errors[err][0]
|
||||
messages.error(request, f'{err}: {err_data["message"]}')
|
||||
return HttpResponseRedirect(reverse('create_item'))
|
||||
|
||||
context = {
|
||||
'form': CreateItemForm(request.POST or None),
|
||||
'formset': ItemImageFormSet()
|
||||
}
|
||||
|
||||
return render(request, 'items/create_item.html', context)
|
||||
|
||||
@login_required
|
||||
def edit_item(request, item_id):
|
||||
item = Item.objects.get(id=item_id)
|
||||
ItemImageFormSet = inlineformset_factory(Item, ItemImage, fields=('image',))
|
||||
|
||||
# Do not allow editing if current user is not the owner
|
||||
if request.user != item.owner:
|
||||
messages.error(request, "You can't edit an item you didn't post.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[item_id]))
|
||||
|
||||
# Do not allow editing if item is not available
|
||||
if item.available is False:
|
||||
messages.error(request, "You can't edit an item that is pending sale.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[item_id]))
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CreateItemForm(request.POST, instance=item)
|
||||
if form.is_valid():
|
||||
saved_item = form.save(commit=False)
|
||||
formset = ItemImageFormSet(request.POST, request.FILES, instance=saved_item)
|
||||
if formset.is_valid():
|
||||
saved_item.save()
|
||||
formset.save()
|
||||
return HttpResponseRedirect(reverse('get_item', args=[item_id]))
|
||||
else:
|
||||
messages.error(request, "Unable to save images.")
|
||||
for err in formset.errors:
|
||||
messages.error(request, err)
|
||||
return HttpResponseRedirect(reverse('get_item', args=[item_id]))
|
||||
else:
|
||||
form_errors = form.errors.get_json_data()
|
||||
for err in form_errors:
|
||||
err_data = form_errors[err][0]
|
||||
messages.error(request, f'{err}: {err_data["message"]}')
|
||||
return HttpResponseRedirect(reverse('get_item', args=[item_id]))
|
||||
else:
|
||||
context = {
|
||||
'form': CreateItemForm(instance=item),
|
||||
'formset': ItemImageFormSet(instance=item)
|
||||
}
|
||||
|
||||
return render(request, 'items/edit_item.html', context)
|
||||
|
||||
@login_required
|
||||
def delete_item(request, item_id):
|
||||
item = Item.objects.get(id=item_id)
|
||||
|
||||
# Do not allow deleting if current user is not the owner
|
||||
if request.user != item.owner:
|
||||
messages.error(request, "You can't delete an item you didn't post.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[item_id]))
|
||||
|
||||
# Do not allow deleting if item is not available
|
||||
if item.available is False:
|
||||
messages.error(request, "You can't delete an item that is pending sale.")
|
||||
return HttpResponseRedirect(reverse('get_item', args=[item_id]))
|
||||
|
||||
item.delete()
|
||||
messages.success(request, f"Item #{item_id}, \"{item.name}\", deleted!")
|
||||
return HttpResponseRedirect(reverse('list_items'))
|
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'xmrauctions.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,10 @@
|
||||
Django==2.2.7
|
||||
Pillow==6.2.1
|
||||
django-redis==4.11.0
|
||||
django-registration==3.0.1
|
||||
django-storages==1.8.0
|
||||
huey==2.1.3
|
||||
monero==0.6.2
|
||||
psycopg2==2.8.4
|
||||
pysha3==1.0.2
|
||||
redis==3.3.11
|
@ -0,0 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from sales.models import ItemSale
|
||||
|
||||
|
||||
admin.site.register(ItemSale)
|
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SalesConfig(AppConfig):
|
||||
name = 'sales'
|
@ -0,0 +1,44 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-20 05:26
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('bids', '0001_initial'),
|
||||
('items', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ItemSale',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('escrow_address', models.CharField(max_length=96)),
|
||||
('escrow_account_index', models.IntegerField()),
|
||||
('agreed_price_xmr', models.FloatField()),
|
||||
('platform_fee_xmr', models.FloatField()),
|
||||
('expected_payment_xmr', models.FloatField()),
|
||||
('received_payment_xmr', models.FloatField(default=0.0)),
|
||||
('escrow_period_days', models.PositiveSmallIntegerField(default=30)),
|
||||
('buyer_notified', models.BooleanField(default=False)),
|
||||
('payment_received', models.BooleanField(default=False)),
|
||||
('seller_notified', models.BooleanField(default=False)),
|
||||
('payment_refunded', models.BooleanField(default=False)),
|
||||
('item_shipped', models.BooleanField(default=False)),
|
||||
('item_received', models.BooleanField(default=False)),
|
||||
('buyer_disputed', models.BooleanField(default=False)),
|
||||
('seller_disputed', models.BooleanField(default=False)),
|
||||
('escrow_complete', models.BooleanField(default=False)),
|
||||
('seller_paid', models.BooleanField(default=False)),
|
||||
('platform_paid', models.BooleanField(default=False)),
|
||||
('sale_finalized', models.BooleanField(default=False)),
|
||||
('bid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bids', to='bids.ItemBid')),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sales', to='items.Item')),
|
||||
],
|
||||
),
|
||||
]
|
@ -0,0 +1,32 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
from items.models import Item
|
||||
from bids.models import ItemBid
|
||||
|
||||
|
||||
class ItemSale(models.Model):
|
||||
item = models.ForeignKey(Item, related_name='sales', on_delete=models.CASCADE)
|
||||
bid = models.ForeignKey(ItemBid, related_name='bids', on_delete=models.CASCADE)
|
||||
escrow_address = models.CharField(max_length=96)
|
||||
escrow_account_index = models.IntegerField()
|
||||
agreed_price_xmr = models.FloatField()
|
||||
platform_fee_xmr = models.FloatField()
|
||||
expected_payment_xmr = models.FloatField()
|
||||
received_payment_xmr = models.FloatField(default=0.0)
|
||||
escrow_period_days = models.PositiveSmallIntegerField(default=settings.ESCROW_PERIOD_DAYS)
|
||||
buyer_notified = models.BooleanField(default=False)
|
||||
payment_received = models.BooleanField(default=False)
|
||||
seller_notified = models.BooleanField(default=False)
|
||||
payment_refunded = models.BooleanField(default=False)
|
||||
item_shipped = models.BooleanField(default=False)
|
||||
item_received = models.BooleanField(default=False)
|
||||
buyer_disputed = models.BooleanField(default=False)
|
||||
seller_disputed = models.BooleanField(default=False)
|
||||
escrow_complete = models.BooleanField(default=False)
|
||||
seller_paid = models.BooleanField(default=False)
|
||||
platform_paid = models.BooleanField(default=False)
|
||||
sale_finalized = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id} - {self.item.name} - {self.bid.bidder} > {self.item.owner}"
|
@ -0,0 +1,93 @@
|
||||
from decimal import Decimal
|
||||
from huey import crontab
|
||||
from huey.contrib.djhuey import periodic_task
|
||||
from django.conf import settings
|
||||
from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from core.monero import AuctionWallet
|
||||
from sales.models import ItemSale
|
||||
|
||||
|
||||
class EmailTemplate:
|
||||
def __init__(self, item, role):
|
||||
context = {
|
||||
'sale': item,
|
||||
'site_name': settings.SITE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
'sale_path': reverse('get_sale', args=[item.bid.id])
|
||||
}
|
||||
subject = render_to_string(
|
||||
template_name=f'sales/notify/{role}/subject.txt',
|
||||
context=context,
|
||||
request=None
|
||||
)
|
||||
body = render_to_string(
|
||||
template_name=f'sales/notify/{role}/body.txt',
|
||||
context=context,
|
||||
request=None
|
||||
)
|
||||
self.subject = ''.join(subject.splitlines())
|
||||
self.body = body
|
||||
|
||||
|
||||
@periodic_task(crontab(minute='*/3'))
|
||||
def notify_buyer_of_pending_sale():
|
||||
item_sales = ItemSale.objects.filter(buyer_notified=False)
|
||||
for sale in item_sales:
|
||||
email_template = EmailTemplate(
|
||||
item=sale,
|
||||
role='buyer'
|
||||
)
|
||||
sent = send_mail(
|
||||
email_template.subject,
|
||||
email_template.body,
|
||||
settings.EMAIL_FROM,
|
||||
[sale.bid.bidder.email]
|
||||
)
|
||||
if sent == 1:
|
||||
sale.buyer_notified = True
|
||||
sale.save()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@periodic_task(crontab(minute='*/2'))
|
||||
def notify_seller_of_funds_received():
|
||||
item_sales = ItemSale.objects.filter(seller_notified=False, buyer_notified=True, payment_received=True)
|
||||
for sale in item_sales:
|
||||
email_template = EmailTemplate(
|
||||
item=sale,
|
||||
role='seller'
|
||||
)
|
||||
sent = send_mail(
|
||||
email_template.subject,
|
||||
email_template.body,
|
||||
settings.EMAIL_FROM,
|
||||
[sale.item.owner.email]
|
||||
)
|
||||
if sent == 1:
|
||||
sale.seller_notified = True
|
||||
sale.save()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@periodic_task(crontab(minute='*/10'))
|
||||
def poll_for_buyer_escrow_payments():
|
||||
aw = AuctionWallet()
|
||||
item_sales = ItemSale.objects.filter(payment_received=False)
|
||||
for sale in item_sales:
|
||||
sale_account = aw.wallet.accounts[sale.escrow_account_index]
|
||||
|
||||
sale.received_payment_xmr = sale_account.balance()
|
||||
|
||||
if sale_account.balance() >= Decimal(str(sale.expected_payment_xmr)):
|
||||
sale.payment_received = True
|
||||
|
||||
sale.save()
|
||||
|
||||
if settings.DEBUG:
|
||||
print('[+] Sale: #{} - Balance: {} - Payment Received: {}'.format(
|
||||
sale.id, sale.received_payment_xmr, sale.payment_received
|
||||
))
|
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('<int:bid_id>/', views.get_sale, name='get_sale'),
|
||||
]
|
@ -0,0 +1,22 @@
|
||||
from django.shortcuts import render, HttpResponseRedirect, reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from bids.models import ItemBid
|
||||
from sales.models import ItemSale
|
||||
|
||||
|
||||
@login_required
|
||||
def get_sale(request, bid_id):
|
||||
bid = ItemBid.objects.get(id=bid_id)
|
||||
sale = ItemSale.objects.get(bid=bid)
|
||||
|
||||
# Do not proceed unless current user is a buyer or seller
|
||||
if request.user != bid.bidder and request.user != sale.item.owner:
|
||||
messages.error(request, "You can't view a sale you are not involved in.")
|
||||
return HttpResponseRedirect(reverse('home'))
|
||||
|
||||
context = {
|
||||
'sale': sale
|
||||
}
|
||||
|
||||
return render(request, 'sales/get_sale.html', context)
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,7 @@
|
||||
<component lightWeight="true">
|
||||
<attach event="onpropertychange" onevent="handlePropertychange()" />
|
||||
<attach event="ondetach" onevent="restore()" />
|
||||
<attach event="onresize" for="window" onevent="handleResize()" />
|
||||
<script type="text/javascript">
|
||||
var rsrc=/url\(["']?(.*?)["']?\)/,positions={top:0,left:0,bottom:1,right:1,center:0.5},doc=element.document;init(); function init(){var b=doc.createElement("div"),a=doc.createElement("img"),c,d;b.style.position="absolute";b.style.zIndex=-1;b.style.top=0;b.style.right=0;b.style.left=0;b.style.bottom=0;b.style.overflow="hidden";a.style.position="absolute";a.style.width=a.style.width="auto";b.appendChild(a);element.insertBefore(b,element.firstChild);d=[element.currentStyle.backgroundPositionX,element.currentStyle.backgroundPositionY];element.bgsExpando=c={wrapper:b,img:a,backgroundSize:element.currentStyle["background-size"], backgroundPositionX:positions[d[0]]||parseFloat(d[0])/100,backgroundPositionY:positions[d[1]]||parseFloat(d[1])/100};"auto"==element.currentStyle.zIndex&&(element.style.zIndex=0);"static"==element.currentStyle.position&&(element.style.position="relative");refreshDisplay(element,c)&&(refreshDimensions(element,c),refreshBackgroundImage(element,c,function(){updateBackground(element,c)}))} function refreshDisplay(b,a){var c=b.currentStyle.display;c!=a.display&&(a.display=c,a.somethingChanged=!0);return"none"!=c}function refreshDimensions(b,a){var c=b.offsetWidth-(parseFloat(b.currentStyle.borderLeftWidth)||0)-(parseFloat(b.currentStyle.borderRightWidth)||0),d=b.offsetHeight-(parseFloat(b.currentStyle.borderTopWidth)||0)-(parseFloat(b.currentStyle.borderBottomWidth)||0);if(c!=a.innerWidth||d!=a.innerHeight)a.innerWidth=c,a.innerHeight=d,a.somethingChanged=!0} function refreshBackgroundImage(b,a,c){var d=a.img,e=(rsrc.exec(b.currentStyle.backgroundImage)||[])[1];if(e&&e!=a.backgroundSrc){a.backgroundSrc=e;a.somethingChanged=!0;d.onload=function(){var b=d.width,e=d.height;1==b&&1==e||(a.imgWidth=b,a.imgHeight=e,a.constrain=!1,c(),d.style.visibility="visible",d.onload=null)};d.style.visibility="hidden";d.src=a.backgroundSrc;if(d.readyState||d.complete)d.src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==",d.src=a.backgroundSrc;a.ignoreNextPropertyChange= !0;b.style.backgroundImage="none"}else c()} function updateBackground(b,a){if(a.somethingChanged){var c=a.img,d=a.innerWidth/a.innerHeight,e=a.imgWidth/a.imgHeight,f=a.constrain;"contain"==a.backgroundSize?e>d?(a.constrain=d="width",e=Math.floor((a.innerHeight-a.innerWidth/e)*a.backgroundPositionY),c.style.top=e+"px",d!=f&&(c.style.width="100%",c.style.height="auto",c.style.left=0)):(a.constrain=d="height",e=Math.floor((a.innerWidth-a.innerHeight*e)*a.backgroundPositionX),c.style.left=e+"px",d!=f&&(c.style.width="auto",c.style.height="100%", c.style.top=0)):"cover"==a.backgroundSize&&(e>d?(a.constrain=d="height",e=Math.floor((a.innerHeight*e-a.innerWidth)*a.backgroundPositionX),c.style.left=-e+"px",d!=f&&(c.style.width="auto",c.style.height="100%",c.style.top=0)):(a.constrain=d="width",e=Math.floor((a.innerWidth/e-a.innerHeight)*a.backgroundPositionY),c.style.top=-e+"px",d!=f&&(c.style.width="100%",c.style.height="auto",c.style.left=0)));a.somethingChanged=!1}} function handlePropertychange(){var b=element.bgsExpando;b.ignoreNextPropertyChange?b.ignoreNextPropertyChange=!1:refreshDisplay(element,b)&&(refreshDimensions(element,b),refreshBackgroundImage(element,b,function(){updateBackground(element,b)}))}function handleResize(){var b=element.bgsExpando;"none"!=b.display&&(refreshDimensions(element,b),updateBackground(element,b))} function restore(){var b=element.bgsExpando;try{element.style.backgroundImage="url('"+b.backgroundSrc+"')",element.removeChild(b.wrapper),element.bgsExpando=null}catch(a){}};
|
||||
</script>
|
@ -0,0 +1,8 @@
|
||||
/*
|
||||
HTML5 Shiv v3.6.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
|
||||
*/
|
||||
(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a, |