Merge branch 'main' into fix_multiple_images
This commit is contained in:
commit
2ecfc4f33b
10 changed files with 63 additions and 88 deletions
8
crud.py
8
crud.py
|
|
@ -258,7 +258,7 @@ async def create_product(merchant_id: str, data: PartialProduct) -> Product:
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO nostrmarket.products (merchant_id, id, stall_id, name, image, price, quantity, category_list, meta)
|
INSERT INTO nostrmarket.products (merchant_id, id, stall_id, name, price, quantity, image_urls, category_list, meta)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
|
|
@ -266,9 +266,9 @@ async def create_product(merchant_id: str, data: PartialProduct) -> Product:
|
||||||
product_id,
|
product_id,
|
||||||
data.stall_id,
|
data.stall_id,
|
||||||
data.name,
|
data.name,
|
||||||
data.image,
|
|
||||||
data.price,
|
data.price,
|
||||||
data.quantity,
|
data.quantity,
|
||||||
|
json.dumps(data.images),
|
||||||
json.dumps(data.categories),
|
json.dumps(data.categories),
|
||||||
json.dumps(data.config.dict()),
|
json.dumps(data.config.dict()),
|
||||||
),
|
),
|
||||||
|
|
@ -283,14 +283,14 @@ async def update_product(merchant_id: str, product: Product) -> Product:
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
UPDATE nostrmarket.products set name = ?, image = ?, price = ?, quantity = ?, category_list = ?, meta = ?
|
UPDATE nostrmarket.products set name = ?, price = ?, quantity = ?, image_urls = ?, category_list = ?, meta = ?
|
||||||
WHERE merchant_id = ? AND id = ?
|
WHERE merchant_id = ? AND id = ?
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
product.name,
|
product.name,
|
||||||
product.image,
|
|
||||||
product.price,
|
product.price,
|
||||||
product.quantity,
|
product.quantity,
|
||||||
|
json.dumps(product.images),
|
||||||
json.dumps(product.categories),
|
json.dumps(product.categories),
|
||||||
json.dumps(product.config.dict()),
|
json.dumps(product.config.dict()),
|
||||||
merchant_id,
|
merchant_id,
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ async def m001_initial(db):
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
stall_id TEXT NOT NULL,
|
stall_id TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
image TEXT,
|
image_urls TEXT DEFAULT '[]',
|
||||||
price REAL NOT NULL,
|
price REAL NOT NULL,
|
||||||
quantity INTEGER NOT NULL,
|
quantity INTEGER NOT NULL,
|
||||||
category_list TEXT DEFAULT '[]',
|
category_list TEXT DEFAULT '[]',
|
||||||
|
|
|
||||||
24
models.py
24
models.py
|
|
@ -217,30 +217,11 @@ class PartialProduct(BaseModel):
|
||||||
stall_id: str
|
stall_id: str
|
||||||
name: str
|
name: str
|
||||||
categories: List[str] = []
|
categories: List[str] = []
|
||||||
image: Optional[str]
|
images: List[str] = []
|
||||||
price: float
|
price: float
|
||||||
quantity: int
|
quantity: int
|
||||||
config: ProductConfig = ProductConfig()
|
config: ProductConfig = ProductConfig()
|
||||||
|
|
||||||
def validate_product(self):
|
|
||||||
if self.image:
|
|
||||||
image_is_url = self.image.startswith("https://") or self.image.startswith(
|
|
||||||
"http://"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not image_is_url:
|
|
||||||
|
|
||||||
def size(b64string):
|
|
||||||
return int((len(b64string) * 3) / 4 - b64string.count("=", -2))
|
|
||||||
|
|
||||||
image_size = size(self.image) / 1024
|
|
||||||
if image_size > 100:
|
|
||||||
raise ValueError(
|
|
||||||
f"""
|
|
||||||
Image size is too big, {int(image_size)}Kb.
|
|
||||||
Max: 100kb, Compress the image at https://tinypng.com, or use an URL."""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Product(PartialProduct, Nostrable):
|
class Product(PartialProduct, Nostrable):
|
||||||
id: str
|
id: str
|
||||||
|
|
@ -251,7 +232,7 @@ class Product(PartialProduct, Nostrable):
|
||||||
"stall_id": self.stall_id,
|
"stall_id": self.stall_id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"description": self.config.description,
|
"description": self.config.description,
|
||||||
"image": self.image,
|
"images": self.images,
|
||||||
"currency": self.config.currency,
|
"currency": self.config.currency,
|
||||||
"price": self.price,
|
"price": self.price,
|
||||||
"quantity": self.quantity,
|
"quantity": self.quantity,
|
||||||
|
|
@ -285,6 +266,7 @@ class Product(PartialProduct, Nostrable):
|
||||||
def from_row(cls, row: Row) -> "Product":
|
def from_row(cls, row: Row) -> "Product":
|
||||||
product = cls(**dict(row))
|
product = cls(**dict(row))
|
||||||
product.config = ProductConfig(**json.loads(row["meta"]))
|
product.config = ProductConfig(**json.loads(row["meta"]))
|
||||||
|
product.images = json.loads(row["image_urls"])
|
||||||
product.categories = json.loads(row["category_list"])
|
product.categories = json.loads(row["category_list"])
|
||||||
return product
|
return product
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,8 @@ class NostrClient:
|
||||||
await self.send_req_queue.put(["EVENT", e.dict()])
|
await self.send_req_queue.put(["EVENT", e.dict()])
|
||||||
|
|
||||||
async def subscribe_to_direct_messages(self, public_key: str, since: int):
|
async def subscribe_to_direct_messages(self, public_key: str, since: int):
|
||||||
in_messages_filter = {"kind": 4, "#p": [public_key]}
|
in_messages_filter = {"kinds": [4], "#p": [public_key]}
|
||||||
out_messages_filter = {"kind": 4, "authors": [public_key]}
|
out_messages_filter = {"kinds": [4], "authors": [public_key]}
|
||||||
if since and since != 0:
|
if since and since != 0:
|
||||||
in_messages_filter["since"] = since
|
in_messages_filter["since"] = since
|
||||||
out_messages_filter["since"] = since
|
out_messages_filter["since"] = since
|
||||||
|
|
@ -82,8 +82,8 @@ class NostrClient:
|
||||||
)
|
)
|
||||||
|
|
||||||
async def subscribe_to_merchant_events(self, public_key: str, since: int):
|
async def subscribe_to_merchant_events(self, public_key: str, since: int):
|
||||||
stall_filter = {"kind": 30017, "authors": [public_key]}
|
stall_filter = {"kinds": [30017], "authors": [public_key]}
|
||||||
product_filter = {"kind": 30018, "authors": [public_key]}
|
product_filter = {"kinds": [30018], "authors": [public_key]}
|
||||||
|
|
||||||
await self.send_req_queue.put(
|
await self.send_req_queue.put(
|
||||||
["REQ", f"stall-events:{public_key}", stall_filter]
|
["REQ", f"stall-events:{public_key}", stall_filter]
|
||||||
|
|
@ -93,7 +93,7 @@ class NostrClient:
|
||||||
)
|
)
|
||||||
|
|
||||||
async def subscribe_to_user_profile(self, public_key: str, since: int):
|
async def subscribe_to_user_profile(self, public_key: str, since: int):
|
||||||
profile_filter = {"kind": 0, "authors": [public_key]}
|
profile_filter = {"kinds": [0], "authors": [public_key]}
|
||||||
if since and since != 0:
|
if since and since != 0:
|
||||||
profile_filter["since"] = since + 1
|
profile_filter["since"] = since + 1
|
||||||
|
|
||||||
|
|
|
||||||
14
services.py
14
services.py
|
|
@ -315,6 +315,11 @@ async def _handle_dirrect_message(
|
||||||
incoming=True,
|
incoming=True,
|
||||||
)
|
)
|
||||||
await create_direct_message(merchant_id, dm)
|
await create_direct_message(merchant_id, dm)
|
||||||
|
await websocketUpdater(
|
||||||
|
merchant_id,
|
||||||
|
json.dumps({"type": "new-direct-message", "customerPubkey": from_pubkey}),
|
||||||
|
)
|
||||||
|
|
||||||
if order:
|
if order:
|
||||||
order["public_key"] = from_pubkey
|
order["public_key"] = from_pubkey
|
||||||
order["merchant_public_key"] = merchant_public_key
|
order["merchant_public_key"] = merchant_public_key
|
||||||
|
|
@ -322,11 +327,6 @@ async def _handle_dirrect_message(
|
||||||
order["event_created_at"] = event_created_at
|
order["event_created_at"] = event_created_at
|
||||||
return await _handle_new_order(PartialOrder(**order))
|
return await _handle_new_order(PartialOrder(**order))
|
||||||
|
|
||||||
await websocketUpdater(
|
|
||||||
merchant_id,
|
|
||||||
json.dumps({"type": "new-direct-message", "customerPubkey": from_pubkey}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning(ex)
|
logger.warning(ex)
|
||||||
|
|
@ -355,10 +355,6 @@ async def _handle_new_customer(event, merchant):
|
||||||
merchant.id, Customer(merchant_id=merchant.id, public_key=event.pubkey)
|
merchant.id, Customer(merchant_id=merchant.id, public_key=event.pubkey)
|
||||||
)
|
)
|
||||||
await nostr_client.subscribe_to_user_profile(event.pubkey, 0)
|
await nostr_client.subscribe_to_user_profile(event.pubkey, 0)
|
||||||
await websocketUpdater(
|
|
||||||
merchant.id,
|
|
||||||
json.dumps({"type": "new-customer"}),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _handle_customer_profile_update(event: NostrEvent):
|
async def _handle_customer_profile_update(event: NostrEvent):
|
||||||
|
|
|
||||||
|
|
@ -226,43 +226,28 @@
|
||||||
label="Categories (Hit Enter to add)"
|
label="Categories (Hit Enter to add)"
|
||||||
placeholder="crafts,robots,etc"
|
placeholder="crafts,robots,etc"
|
||||||
></q-select>
|
></q-select>
|
||||||
<q-toggle
|
|
||||||
:label="`${productDialog.url ? 'Insert image URL' : 'Upload image file'}`"
|
|
||||||
v-model="productDialog.url"
|
|
||||||
></q-toggle>
|
|
||||||
<q-input
|
<q-input
|
||||||
v-if="productDialog.url"
|
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model.trim="productDialog.data.image"
|
v-model.trim="productDialog.data.image"
|
||||||
|
@keydown.enter="addProductImage"
|
||||||
type="url"
|
type="url"
|
||||||
label="Image URL"
|
label="Image URL"
|
||||||
></q-input>
|
|
||||||
<q-file
|
|
||||||
v-else
|
|
||||||
class="q-pr-md"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
capture="environment"
|
|
||||||
accept="image/jpeg, image/png"
|
|
||||||
:max-file-size="3*1024**2"
|
|
||||||
label="Small image (optional)"
|
|
||||||
clearable
|
|
||||||
@input="imageAdded"
|
|
||||||
@clear="imageCleared"
|
|
||||||
>
|
>
|
||||||
<template v-if="productDialog.data.image" v-slot:before>
|
<q-btn @click="addProductImage" dense flat icon="add"></q-btn
|
||||||
<img style="height: 1em" :src="productDialog.data.image" />
|
></q-input>
|
||||||
</template>
|
|
||||||
<template v-if="productDialog.data.image" v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
name="cancel"
|
|
||||||
@click.stop.prevent="imageCleared"
|
|
||||||
class="cursor-pointer"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-file>
|
|
||||||
|
|
||||||
|
<q-chip
|
||||||
|
v-for="imageUrl in productDialog.data.images"
|
||||||
|
:key="imageUrl"
|
||||||
|
removable
|
||||||
|
@remove="removeProductImage(imageUrl)"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
>
|
||||||
|
<span v-text="imageUrl.split('/').pop()"></span>
|
||||||
|
</q-chip>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ async function stallDetails(path) {
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
categories: [],
|
categories: [],
|
||||||
|
images: [],
|
||||||
image: null,
|
image: null,
|
||||||
price: 0,
|
price: 0,
|
||||||
|
|
||||||
|
|
@ -170,22 +171,23 @@ async function stallDetails(path) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
imageAdded(file) {
|
addProductImage: function () {
|
||||||
const image = new Image()
|
if (!isValidImageUrl(this.productDialog.data.image)) {
|
||||||
image.src = URL.createObjectURL(file)
|
this.$q.notify({
|
||||||
image.onload = async () => {
|
type: 'warning',
|
||||||
let fit = imgSizeFit(image)
|
message: 'Not a valid image URL',
|
||||||
let canvas = document.createElement('canvas')
|
timeout: 5000
|
||||||
canvas.setAttribute('width', fit.width)
|
})
|
||||||
canvas.setAttribute('height', fit.height)
|
return
|
||||||
output = await pica.resize(image, canvas)
|
|
||||||
this.productDialog.data.image = output.toDataURL('image/jpeg', 0.4)
|
|
||||||
this.productDialog = {...this.productDialog}
|
|
||||||
}
|
}
|
||||||
},
|
this.productDialog.data.images.push(this.productDialog.data.image)
|
||||||
imageCleared() {
|
|
||||||
this.productDialog.data.image = null
|
this.productDialog.data.image = null
|
||||||
this.productDialog = {...this.productDialog}
|
},
|
||||||
|
removeProductImage: function (imageUrl) {
|
||||||
|
const index = this.productDialog.data.images.indexOf(imageUrl)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.productDialog.data.images.splice(index, 1)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getProducts: async function () {
|
getProducts: async function () {
|
||||||
try {
|
try {
|
||||||
|
|
@ -205,7 +207,7 @@ async function stallDetails(path) {
|
||||||
id: this.productDialog.data.id,
|
id: this.productDialog.data.id,
|
||||||
name: this.productDialog.data.name,
|
name: this.productDialog.data.name,
|
||||||
|
|
||||||
image: this.productDialog.data.image,
|
images: this.productDialog.data.images,
|
||||||
price: this.productDialog.data.price,
|
price: this.productDialog.data.price,
|
||||||
quantity: this.productDialog.data.quantity,
|
quantity: this.productDialog.data.quantity,
|
||||||
categories: this.productDialog.data.categories,
|
categories: this.productDialog.data.categories,
|
||||||
|
|
@ -294,6 +296,7 @@ async function stallDetails(path) {
|
||||||
description: '',
|
description: '',
|
||||||
categories: [],
|
categories: [],
|
||||||
image: null,
|
image: null,
|
||||||
|
images: [],
|
||||||
price: 0,
|
price: 0,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
config: {
|
config: {
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ const merchant = async () => {
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Merchant Created!'
|
message: 'Merchant Created!'
|
||||||
})
|
})
|
||||||
|
this.waitForNotifications()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +109,7 @@ const merchant = async () => {
|
||||||
this.orderPubkey = customerPubkey
|
this.orderPubkey = customerPubkey
|
||||||
},
|
},
|
||||||
waitForNotifications: async function () {
|
waitForNotifications: async function () {
|
||||||
|
if (!this.merchant) return
|
||||||
try {
|
try {
|
||||||
const scheme = location.protocol === 'http:' ? 'ws' : 'wss'
|
const scheme = location.protocol === 'http:' ? 'ws' : 'wss'
|
||||||
const port = location.port ? `:${location.port}` : ''
|
const port = location.port ? `:${location.port}` : ''
|
||||||
|
|
@ -122,7 +124,6 @@ const merchant = async () => {
|
||||||
message: 'New Order'
|
message: 'New Order'
|
||||||
})
|
})
|
||||||
await this.$refs.orderListRef.addOrder(data)
|
await this.$refs.orderListRef.addOrder(data)
|
||||||
} else if (data.type === 'new-customer') {
|
|
||||||
} else if (data.type === 'new-direct-message') {
|
} else if (data.type === 'new-direct-message') {
|
||||||
await this.$refs.directMessagesRef.handleNewMessage(data)
|
await this.$refs.directMessagesRef.handleNewMessage(data)
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +132,7 @@ const merchant = async () => {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Failed to watch for updated',
|
message: 'Failed to watch for updates',
|
||||||
caption: `${error}`
|
caption: `${error}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,3 +111,13 @@ function timeFromNow(time) {
|
||||||
// Return time from now data
|
// Return time from now data
|
||||||
return `${tfn.time} ${tfn.unitOfTime}`
|
return `${tfn.time} ${tfn.unitOfTime}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidImageUrl(string) {
|
||||||
|
let url
|
||||||
|
try {
|
||||||
|
url = new URL(string)
|
||||||
|
} catch (_) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return url.protocol === 'http:' || url.protocol === 'https:'
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -518,7 +518,6 @@ async def api_create_product(
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Product:
|
) -> Product:
|
||||||
try:
|
try:
|
||||||
data.validate_product()
|
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, "Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
|
|
||||||
|
|
@ -557,7 +556,6 @@ async def api_update_product(
|
||||||
if product_id != product.id:
|
if product_id != product.id:
|
||||||
raise ValueError("Bad product ID")
|
raise ValueError("Bad product ID")
|
||||||
|
|
||||||
product.validate_product()
|
|
||||||
merchant = await get_merchant_for_user(wallet.wallet.user)
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
assert merchant, "Merchant cannot be found"
|
assert merchant, "Merchant cannot be found"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue