aa-shop 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aa_shop-0.1.1/LICENSE +21 -0
- aa_shop-0.1.1/PKG-INFO +145 -0
- aa_shop-0.1.1/README.md +110 -0
- aa_shop-0.1.1/pyproject.toml +47 -0
- aa_shop-0.1.1/shop/__init__.py +3 -0
- aa_shop-0.1.1/shop/admin.py +12 -0
- aa_shop-0.1.1/shop/apps.py +7 -0
- aa_shop-0.1.1/shop/auth_hooks.py +48 -0
- aa_shop-0.1.1/shop/formatting.py +23 -0
- aa_shop-0.1.1/shop/forms.py +156 -0
- aa_shop-0.1.1/shop/management/__init__.py +0 -0
- aa_shop-0.1.1/shop/management/commands/__init__.py +0 -0
- aa_shop-0.1.1/shop/management/commands/shop_refresh_prices.py +45 -0
- aa_shop-0.1.1/shop/management/commands/shop_refresh_stock.py +42 -0
- aa_shop-0.1.1/shop/markdown.py +30 -0
- aa_shop-0.1.1/shop/market.py +72 -0
- aa_shop-0.1.1/shop/migrations/0001_initial.py +25 -0
- aa_shop-0.1.1/shop/migrations/0002_alter_general_options_shop.py +36 -0
- aa_shop-0.1.1/shop/migrations/0003_shop_enabled_sections.py +18 -0
- aa_shop-0.1.1/shop/migrations/0004_hulllisting_hulllistingsource_and_more.py +41 -0
- aa_shop-0.1.1/shop/migrations/0005_shopsource_delete_hulllistingsource.py +29 -0
- aa_shop-0.1.1/shop/migrations/0006_hulllisting_price_mode_hulllisting_price_pct_and_more.py +28 -0
- aa_shop-0.1.1/shop/migrations/0007_shopsource_price_mode_shopsource_price_pct.py +23 -0
- aa_shop-0.1.1/shop/migrations/0008_shopsource_container_name.py +18 -0
- aa_shop-0.1.1/shop/migrations/0009_hulllisting_persist.py +18 -0
- aa_shop-0.1.1/shop/migrations/0010_hulllisting_featured.py +18 -0
- aa_shop-0.1.1/shop/migrations/0011_shop_description.py +18 -0
- aa_shop-0.1.1/shop/migrations/0012_hulllisting_description.py +18 -0
- aa_shop-0.1.1/shop/migrations/0013_hulllisting_featured_at.py +18 -0
- aa_shop-0.1.1/shop/migrations/__init__.py +0 -0
- aa_shop-0.1.1/shop/models.py +210 -0
- aa_shop-0.1.1/shop/permissions.py +47 -0
- aa_shop-0.1.1/shop/sources.py +60 -0
- aa_shop-0.1.1/shop/static/shop/backoffice.css +66 -0
- aa_shop-0.1.1/shop/static/shop/storefront.css +2 -0
- aa_shop-0.1.1/shop/stock.py +47 -0
- aa_shop-0.1.1/shop/tasks.py +107 -0
- aa_shop-0.1.1/shop/templates/shop/_hull_listings.html +53 -0
- aa_shop-0.1.1/shop/templates/shop/_shop_table.html +46 -0
- aa_shop-0.1.1/shop/templates/shop/_source_picker.html +73 -0
- aa_shop-0.1.1/shop/templates/shop/base.html +8 -0
- aa_shop-0.1.1/shop/templates/shop/index.html +17 -0
- aa_shop-0.1.1/shop/templates/shop/manage.html +359 -0
- aa_shop-0.1.1/shop/templates/shop/shop_confirm_delete.html +12 -0
- aa_shop-0.1.1/shop/templates/shop/shop_form.html +23 -0
- aa_shop-0.1.1/shop/templates/storefront/_hulls_featured.html +32 -0
- aa_shop-0.1.1/shop/templates/storefront/_hulls_grid.html +32 -0
- aa_shop-0.1.1/shop/templates/storefront/_hulls_table.html +42 -0
- aa_shop-0.1.1/shop/templates/storefront/base.html +33 -0
- aa_shop-0.1.1/shop/templates/storefront/detail.html +71 -0
- aa_shop-0.1.1/shop/templates/storefront/item_detail.html +54 -0
- aa_shop-0.1.1/shop/tests/__init__.py +0 -0
- aa_shop-0.1.1/shop/tests/test_commands.py +68 -0
- aa_shop-0.1.1/shop/tests/test_formatting.py +24 -0
- aa_shop-0.1.1/shop/tests/test_forms.py +68 -0
- aa_shop-0.1.1/shop/tests/test_markdown.py +66 -0
- aa_shop-0.1.1/shop/tests/test_market.py +39 -0
- aa_shop-0.1.1/shop/tests/test_models.py +38 -0
- aa_shop-0.1.1/shop/tests/test_models_hulls.py +46 -0
- aa_shop-0.1.1/shop/tests/test_permissions.py +77 -0
- aa_shop-0.1.1/shop/tests/test_pricing.py +235 -0
- aa_shop-0.1.1/shop/tests/test_smoke.py +7 -0
- aa_shop-0.1.1/shop/tests/test_sources.py +57 -0
- aa_shop-0.1.1/shop/tests/test_stock.py +118 -0
- aa_shop-0.1.1/shop/tests/test_tasks.py +116 -0
- aa_shop-0.1.1/shop/tests/test_views_backoffice.py +99 -0
- aa_shop-0.1.1/shop/tests/test_views_manage.py +410 -0
- aa_shop-0.1.1/shop/tests/test_views_storefront.py +49 -0
- aa_shop-0.1.1/shop/tests/test_views_storefront_hulls.py +143 -0
- aa_shop-0.1.1/shop/urls/__init__.py +0 -0
- aa_shop-0.1.1/shop/urls/backoffice.py +22 -0
- aa_shop-0.1.1/shop/urls/storefront.py +12 -0
- aa_shop-0.1.1/shop/views/__init__.py +0 -0
- aa_shop-0.1.1/shop/views/backoffice.py +251 -0
- aa_shop-0.1.1/shop/views/storefront.py +52 -0
aa_shop-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Onisoru
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
aa_shop-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aa-shop
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A public asset shop plugin for Alliance Auth.
|
|
5
|
+
Keywords: allianceauth,eveonline,django
|
|
6
|
+
Author-email: Daniel Onisoru <daniel.onisoru@gmail.com>
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: Django
|
|
12
|
+
Classifier: Framework :: Django :: 4.2
|
|
13
|
+
Classifier: Framework :: Django :: 5.2
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: allianceauth>=5,<6
|
|
25
|
+
Requires-Dist: aa-memberaudit>=5
|
|
26
|
+
Requires-Dist: markdown
|
|
27
|
+
Requires-Dist: coverage ; extra == "test"
|
|
28
|
+
Requires-Dist: factory_boy ; extra == "test"
|
|
29
|
+
Project-URL: Changelog, https://gitlab.com/daniel.onisoru/aa-shop/-/blob/master/CHANGELOG.md
|
|
30
|
+
Project-URL: Homepage, https://gitlab.com/daniel.onisoru/aa-shop
|
|
31
|
+
Project-URL: Issues, https://gitlab.com/daniel.onisoru/aa-shop/-/issues
|
|
32
|
+
Project-URL: Source, https://gitlab.com/daniel.onisoru/aa-shop
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
|
|
35
|
+
# aa-shop
|
|
36
|
+
|
|
37
|
+
An [Alliance Auth](https://gitlab.com/allianceauth/allianceauth) plugin that lets registered
|
|
38
|
+
auth members run a publicly available shop stocked from their own (or their corporation's)
|
|
39
|
+
assets.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- Members create and manage **personal** shops; authorized users manage **corporation** shops.
|
|
44
|
+
- A member **back-office** at `/shop-backoffice/` (lists your shops, create/edit/delete).
|
|
45
|
+
- Each published shop gets a **public storefront** at `/shop/<slug>/` — no login required.
|
|
46
|
+
Unpublished shops are visible only to their managers (preview).
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
- Alliance Auth >= 5
|
|
51
|
+
- [aa-memberaudit](https://pypi.org/project/aa-memberaudit/) >= 5, installed and syncing
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
1. Install into your Alliance Auth virtual environment:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install aa-shop
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
2. Add `"shop"` to `INSTALLED_APPS` in your auth project's `local.py`:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
INSTALLED_APPS += ["shop"]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
3. Allow the storefront to expose its public (non-gated) views — **required**, or anonymous
|
|
68
|
+
visitors are redirected to login:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
APPS_WITH_PUBLIC_VIEWS = ["storefront"]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
4. Run migrations and collect static files, then restart your AA services:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
python manage.py migrate
|
|
78
|
+
python manage.py collectstatic
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
5. **EVE universe data:** the Hulls picker lists ship types from
|
|
82
|
+
[django-eveuniverse](https://gitlab.com/ErikKalkoken/django-eveuniverse), so make sure
|
|
83
|
+
your eveuniverse data is populated with ship types (load the full EVE universe, or at
|
|
84
|
+
least the Ship category, using eveuniverse's load tooling). Ships that aren't loaded
|
|
85
|
+
won't be selectable.
|
|
86
|
+
|
|
87
|
+
6. Grant permissions (Django admin, or via groups/states):
|
|
88
|
+
|
|
89
|
+
| Permission | Grants |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `shop.basic_access` | Access the back-office; create/manage your personal shops |
|
|
92
|
+
| `shop.manage_corporation_shops` | Create/manage your corporation's shops |
|
|
93
|
+
|
|
94
|
+
### Periodic price refresh
|
|
95
|
+
|
|
96
|
+
Hulls priced as **% Jita Buy** / **% Jita Sell** are refreshed by a Celery task
|
|
97
|
+
every 6 hours. Add to your `local.py`:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from celery.schedules import crontab
|
|
101
|
+
|
|
102
|
+
CELERYBEAT_SCHEDULE["shop_refresh_pct_prices"] = {
|
|
103
|
+
"task": "shop.tasks.refresh_pct_prices",
|
|
104
|
+
"schedule": crontab(minute=0, hour="*/6"),
|
|
105
|
+
}
|
|
106
|
+
CELERYBEAT_SCHEDULE["shop_refresh_all_stock"] = {
|
|
107
|
+
"task": "shop.tasks.refresh_all_stock",
|
|
108
|
+
"schedule": crontab(minute=0), # hourly
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Stock is recomputed from memberaudit assets (no ESI). Refresh on demand with
|
|
113
|
+
`python manage.py shop_refresh_stock` (`--async` to enqueue the per-shop tasks).
|
|
114
|
+
|
|
115
|
+
The task uses the public ESI market endpoint (no token) at low priority
|
|
116
|
+
(override with `SHOP_TASKS_PRIORITY`, 1=urgent .. 9=idle; default 7). Prices are
|
|
117
|
+
best-of-book at Jita 4-4 (highest buy / lowest sell), the same as Janice.
|
|
118
|
+
|
|
119
|
+
To refresh on demand (e.g. after setting up %-pricing), run inline:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python manage.py shop_refresh_prices # synchronous, prints results
|
|
123
|
+
python manage.py shop_refresh_prices --async # enqueue Celery tasks instead
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Usage
|
|
127
|
+
|
|
128
|
+
- **Members:** open **Shop** in the auth sidebar (`/shop-backoffice/`) to create a shop —
|
|
129
|
+
set a slug, name, type (personal/corporation), and publish it.
|
|
130
|
+
- **Public:** share `/shop/<slug>/`; published shops are browsable by anyone.
|
|
131
|
+
|
|
132
|
+
## Contributing
|
|
133
|
+
|
|
134
|
+
Design docs and implementation plans live in `docs/superpowers/`. The storefront stylesheet
|
|
135
|
+
ships compiled (`shop/static/shop/storefront.css`); to rebuild it after changing storefront
|
|
136
|
+
templates, use the standalone Tailwind v4 CLI + daisyUI (no Node required):
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
bash scripts/build-css.sh # add --watch during development
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
145
|
+
|
aa_shop-0.1.1/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# aa-shop
|
|
2
|
+
|
|
3
|
+
An [Alliance Auth](https://gitlab.com/allianceauth/allianceauth) plugin that lets registered
|
|
4
|
+
auth members run a publicly available shop stocked from their own (or their corporation's)
|
|
5
|
+
assets.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Members create and manage **personal** shops; authorized users manage **corporation** shops.
|
|
10
|
+
- A member **back-office** at `/shop-backoffice/` (lists your shops, create/edit/delete).
|
|
11
|
+
- Each published shop gets a **public storefront** at `/shop/<slug>/` — no login required.
|
|
12
|
+
Unpublished shops are visible only to their managers (preview).
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Alliance Auth >= 5
|
|
17
|
+
- [aa-memberaudit](https://pypi.org/project/aa-memberaudit/) >= 5, installed and syncing
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
1. Install into your Alliance Auth virtual environment:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install aa-shop
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. Add `"shop"` to `INSTALLED_APPS` in your auth project's `local.py`:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
INSTALLED_APPS += ["shop"]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. Allow the storefront to expose its public (non-gated) views — **required**, or anonymous
|
|
34
|
+
visitors are redirected to login:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
APPS_WITH_PUBLIC_VIEWS = ["storefront"]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
4. Run migrations and collect static files, then restart your AA services:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python manage.py migrate
|
|
44
|
+
python manage.py collectstatic
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
5. **EVE universe data:** the Hulls picker lists ship types from
|
|
48
|
+
[django-eveuniverse](https://gitlab.com/ErikKalkoken/django-eveuniverse), so make sure
|
|
49
|
+
your eveuniverse data is populated with ship types (load the full EVE universe, or at
|
|
50
|
+
least the Ship category, using eveuniverse's load tooling). Ships that aren't loaded
|
|
51
|
+
won't be selectable.
|
|
52
|
+
|
|
53
|
+
6. Grant permissions (Django admin, or via groups/states):
|
|
54
|
+
|
|
55
|
+
| Permission | Grants |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `shop.basic_access` | Access the back-office; create/manage your personal shops |
|
|
58
|
+
| `shop.manage_corporation_shops` | Create/manage your corporation's shops |
|
|
59
|
+
|
|
60
|
+
### Periodic price refresh
|
|
61
|
+
|
|
62
|
+
Hulls priced as **% Jita Buy** / **% Jita Sell** are refreshed by a Celery task
|
|
63
|
+
every 6 hours. Add to your `local.py`:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from celery.schedules import crontab
|
|
67
|
+
|
|
68
|
+
CELERYBEAT_SCHEDULE["shop_refresh_pct_prices"] = {
|
|
69
|
+
"task": "shop.tasks.refresh_pct_prices",
|
|
70
|
+
"schedule": crontab(minute=0, hour="*/6"),
|
|
71
|
+
}
|
|
72
|
+
CELERYBEAT_SCHEDULE["shop_refresh_all_stock"] = {
|
|
73
|
+
"task": "shop.tasks.refresh_all_stock",
|
|
74
|
+
"schedule": crontab(minute=0), # hourly
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Stock is recomputed from memberaudit assets (no ESI). Refresh on demand with
|
|
79
|
+
`python manage.py shop_refresh_stock` (`--async` to enqueue the per-shop tasks).
|
|
80
|
+
|
|
81
|
+
The task uses the public ESI market endpoint (no token) at low priority
|
|
82
|
+
(override with `SHOP_TASKS_PRIORITY`, 1=urgent .. 9=idle; default 7). Prices are
|
|
83
|
+
best-of-book at Jita 4-4 (highest buy / lowest sell), the same as Janice.
|
|
84
|
+
|
|
85
|
+
To refresh on demand (e.g. after setting up %-pricing), run inline:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python manage.py shop_refresh_prices # synchronous, prints results
|
|
89
|
+
python manage.py shop_refresh_prices --async # enqueue Celery tasks instead
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
- **Members:** open **Shop** in the auth sidebar (`/shop-backoffice/`) to create a shop —
|
|
95
|
+
set a slug, name, type (personal/corporation), and publish it.
|
|
96
|
+
- **Public:** share `/shop/<slug>/`; published shops are browsable by anyone.
|
|
97
|
+
|
|
98
|
+
## Contributing
|
|
99
|
+
|
|
100
|
+
Design docs and implementation plans live in `docs/superpowers/`. The storefront stylesheet
|
|
101
|
+
ships compiled (`shop/static/shop/storefront.css`); to rebuild it after changing storefront
|
|
102
|
+
templates, use the standalone Tailwind v4 CLI + daisyUI (no Node required):
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
bash scripts/build-css.sh # add --watch during development
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aa-shop"
|
|
7
|
+
dynamic = ["version", "description"]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = {file = "LICENSE"}
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Daniel Onisoru", email = "daniel.onisoru@gmail.com" },
|
|
13
|
+
]
|
|
14
|
+
keywords = ["allianceauth", "eveonline", "django"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Web Environment",
|
|
18
|
+
"Framework :: Django",
|
|
19
|
+
"Framework :: Django :: 4.2",
|
|
20
|
+
"Framework :: Django :: 5.2",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: End Users/Desktop",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"allianceauth>=5,<6",
|
|
33
|
+
"aa-memberaudit>=5",
|
|
34
|
+
"markdown",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
test = ["coverage", "factory_boy"]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://gitlab.com/daniel.onisoru/aa-shop"
|
|
42
|
+
Source = "https://gitlab.com/daniel.onisoru/aa-shop"
|
|
43
|
+
Issues = "https://gitlab.com/daniel.onisoru/aa-shop/-/issues"
|
|
44
|
+
Changelog = "https://gitlab.com/daniel.onisoru/aa-shop/-/blob/master/CHANGELOG.md"
|
|
45
|
+
|
|
46
|
+
[tool.flit.module]
|
|
47
|
+
name = "shop"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Admin registrations."""
|
|
2
|
+
|
|
3
|
+
from django.contrib import admin
|
|
4
|
+
|
|
5
|
+
from .models import Shop
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@admin.register(Shop)
|
|
9
|
+
class ShopAdmin(admin.ModelAdmin):
|
|
10
|
+
list_display = ("name", "slug", "shop_type", "is_published", "corporation", "created_by")
|
|
11
|
+
list_filter = ("shop_type", "is_published")
|
|
12
|
+
search_fields = ("name", "slug")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from django.utils.translation import gettext_lazy as _
|
|
2
|
+
|
|
3
|
+
from allianceauth import hooks
|
|
4
|
+
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
|
5
|
+
|
|
6
|
+
from .urls import backoffice as backoffice_urls
|
|
7
|
+
from .urls import storefront as storefront_urls
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ShopMenuItem(MenuItemHook):
|
|
11
|
+
"""Sidebar entry into the back-office for authorized users."""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
MenuItemHook.__init__(
|
|
15
|
+
self,
|
|
16
|
+
_("Shop"),
|
|
17
|
+
"fas fa-store fa-fw",
|
|
18
|
+
"backoffice:index",
|
|
19
|
+
navactive=["backoffice:"],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def render(self, request):
|
|
23
|
+
if request.user.has_perm("shop.basic_access"):
|
|
24
|
+
return MenuItemHook.render(self, request)
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@hooks.register("menu_item_hook")
|
|
29
|
+
def register_menu():
|
|
30
|
+
return ShopMenuItem()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@hooks.register("url_hook")
|
|
34
|
+
def register_backoffice_urls():
|
|
35
|
+
return UrlHook(backoffice_urls, "backoffice", r"^shop-backoffice/")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@hooks.register("url_hook")
|
|
39
|
+
def register_storefront_urls():
|
|
40
|
+
return UrlHook(
|
|
41
|
+
storefront_urls,
|
|
42
|
+
"storefront",
|
|
43
|
+
r"^shop/",
|
|
44
|
+
excluded_views=[
|
|
45
|
+
"shop.views.storefront.detail",
|
|
46
|
+
"shop.views.storefront.item_detail",
|
|
47
|
+
],
|
|
48
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Number formatting helpers."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def format_isk(amount) -> str:
|
|
5
|
+
"""Abbreviate an ISK amount EVE-style: K / M / B / T.
|
|
6
|
+
|
|
7
|
+
Up to 2 decimals, trailing zeros stripped. Examples:
|
|
8
|
+
23_000_000 -> "23M ISK"
|
|
9
|
+
1_500_000_000 -> "1.5B ISK"
|
|
10
|
+
149_000_000_000 -> "149B ISK"
|
|
11
|
+
750 -> "750 ISK"
|
|
12
|
+
"""
|
|
13
|
+
amount = int(amount)
|
|
14
|
+
for unit, suffix in (
|
|
15
|
+
(1_000_000_000_000, "T"),
|
|
16
|
+
(1_000_000_000, "B"),
|
|
17
|
+
(1_000_000, "M"),
|
|
18
|
+
(1_000, "K"),
|
|
19
|
+
):
|
|
20
|
+
if amount >= unit:
|
|
21
|
+
value = f"{amount / unit:.2f}".rstrip("0").rstrip(".")
|
|
22
|
+
return f"{value}{suffix} ISK"
|
|
23
|
+
return f"{amount:,} ISK"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Forms."""
|
|
2
|
+
|
|
3
|
+
from django import forms
|
|
4
|
+
|
|
5
|
+
from allianceauth.eveonline.models import EveCorporationInfo
|
|
6
|
+
from eveuniverse.models import EveType
|
|
7
|
+
|
|
8
|
+
from .models import (
|
|
9
|
+
FIXED_PRICE, FIXED_STOCK, PCT_MODES, PRICE_MODE_CHOICES, SECTION_CHOICES,
|
|
10
|
+
STOCK_MODE_CHOICES, Shop,
|
|
11
|
+
)
|
|
12
|
+
from .sources import owner_characters
|
|
13
|
+
|
|
14
|
+
SHIP_CATEGORY_ID = 6
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ship_type_queryset():
|
|
18
|
+
return EveType.objects.filter(
|
|
19
|
+
eve_group__eve_category_id=SHIP_CATEGORY_ID, published=True
|
|
20
|
+
).order_by("name")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _resolve_corp(corporation_id):
|
|
24
|
+
try:
|
|
25
|
+
return EveCorporationInfo.objects.get(corporation_id=corporation_id)
|
|
26
|
+
except EveCorporationInfo.DoesNotExist:
|
|
27
|
+
return EveCorporationInfo.objects.create_corporation(corporation_id)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ShopForm(forms.ModelForm):
|
|
31
|
+
enabled_sections = forms.MultipleChoiceField(
|
|
32
|
+
choices=SECTION_CHOICES,
|
|
33
|
+
required=False,
|
|
34
|
+
widget=forms.CheckboxSelectMultiple,
|
|
35
|
+
label="Sections",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
class Meta:
|
|
39
|
+
model = Shop
|
|
40
|
+
fields = ["name", "slug", "shop_type", "is_published", "enabled_sections", "description"]
|
|
41
|
+
widgets = {
|
|
42
|
+
"description": forms.Textarea(attrs={"rows": 6}),
|
|
43
|
+
}
|
|
44
|
+
help_texts = {
|
|
45
|
+
"description": "Markdown supported — bold, italic, headings, lists, links.",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def __init__(self, *args, user=None, **kwargs):
|
|
49
|
+
self.user = user
|
|
50
|
+
super().__init__(*args, **kwargs)
|
|
51
|
+
# shop_type is fixed after creation
|
|
52
|
+
if self.instance and self.instance.pk:
|
|
53
|
+
self.fields["shop_type"].disabled = True
|
|
54
|
+
|
|
55
|
+
def clean_shop_type(self):
|
|
56
|
+
shop_type = self.cleaned_data["shop_type"]
|
|
57
|
+
if shop_type == Shop.ShopType.CORPORATION:
|
|
58
|
+
if not (self.user and self.user.has_perm("shop.manage_corporation_shops")):
|
|
59
|
+
raise forms.ValidationError(
|
|
60
|
+
"You do not have permission to create corporation shops."
|
|
61
|
+
)
|
|
62
|
+
main = getattr(getattr(self.user, "profile", None), "main_character", None)
|
|
63
|
+
if main is None or not main.corporation_id:
|
|
64
|
+
raise forms.ValidationError(
|
|
65
|
+
"Your main character has no corporation set."
|
|
66
|
+
)
|
|
67
|
+
return shop_type
|
|
68
|
+
|
|
69
|
+
def save(self, commit=True):
|
|
70
|
+
creating = self.instance.pk is None
|
|
71
|
+
shop = super().save(commit=False)
|
|
72
|
+
if creating:
|
|
73
|
+
shop.created_by = self.user
|
|
74
|
+
if shop.shop_type == Shop.ShopType.CORPORATION:
|
|
75
|
+
corp_id = self.user.profile.main_character.corporation_id
|
|
76
|
+
shop.corporation = _resolve_corp(corp_id)
|
|
77
|
+
if commit:
|
|
78
|
+
shop.save()
|
|
79
|
+
return shop
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AddStockroomForm(forms.Form):
|
|
83
|
+
character_id = forms.IntegerField()
|
|
84
|
+
location_id = forms.IntegerField()
|
|
85
|
+
container_item_id = forms.IntegerField(required=False)
|
|
86
|
+
price_mode = forms.ChoiceField(choices=PRICE_MODE_CHOICES)
|
|
87
|
+
price_pct = forms.DecimalField(
|
|
88
|
+
min_value=0, max_digits=5, decimal_places=2, required=False
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def __init__(self, *args, owner=None, **kwargs):
|
|
92
|
+
self.owner = owner
|
|
93
|
+
super().__init__(*args, **kwargs)
|
|
94
|
+
|
|
95
|
+
def clean_character_id(self):
|
|
96
|
+
cid = self.cleaned_data["character_id"]
|
|
97
|
+
if not owner_characters(self.owner).filter(id=cid).exists():
|
|
98
|
+
raise forms.ValidationError("Not one of your characters.")
|
|
99
|
+
return cid
|
|
100
|
+
|
|
101
|
+
def clean(self):
|
|
102
|
+
cleaned = super().clean()
|
|
103
|
+
if cleaned.get("price_mode") in PCT_MODES and not cleaned.get("price_pct"):
|
|
104
|
+
self.add_error("price_pct", "Enter a percentage for %-based pricing.")
|
|
105
|
+
return cleaned
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class EditStockroomForm(forms.Form):
|
|
109
|
+
price_mode = forms.ChoiceField(choices=PRICE_MODE_CHOICES)
|
|
110
|
+
price_pct = forms.DecimalField(
|
|
111
|
+
min_value=0, max_digits=5, decimal_places=2, required=False
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def clean(self):
|
|
115
|
+
cleaned = super().clean()
|
|
116
|
+
if cleaned.get("price_mode") in PCT_MODES and not cleaned.get("price_pct"):
|
|
117
|
+
self.add_error("price_pct", "Enter a percentage for %-based pricing.")
|
|
118
|
+
return cleaned
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class HullModeFieldsMixin(forms.Form):
|
|
122
|
+
stock_mode = forms.ChoiceField(choices=STOCK_MODE_CHOICES)
|
|
123
|
+
stock = forms.IntegerField(min_value=0, required=False)
|
|
124
|
+
price_mode = forms.ChoiceField(choices=PRICE_MODE_CHOICES)
|
|
125
|
+
price = forms.IntegerField(min_value=0, required=False) # ISK, fixed mode
|
|
126
|
+
price_pct = forms.DecimalField(
|
|
127
|
+
min_value=0, max_digits=5, decimal_places=2, required=False
|
|
128
|
+
)
|
|
129
|
+
persist = forms.BooleanField(required=False) # keep listing at 0 stock
|
|
130
|
+
description = forms.CharField(
|
|
131
|
+
required=False, widget=forms.Textarea(attrs={"rows": 4}),
|
|
132
|
+
help_text="Markdown supported.",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def clean(self):
|
|
136
|
+
cleaned = super().clean()
|
|
137
|
+
if cleaned.get("stock_mode") == FIXED_STOCK and cleaned.get("stock") is None:
|
|
138
|
+
self.add_error("stock", "Enter a stock quantity for fixed stock.")
|
|
139
|
+
if cleaned.get("price_mode") == FIXED_PRICE and cleaned.get("price") is None:
|
|
140
|
+
self.add_error("price", "Enter a price (0 = Ask) for fixed price.")
|
|
141
|
+
if cleaned.get("price_mode") in PCT_MODES and not cleaned.get("price_pct"):
|
|
142
|
+
self.add_error("price_pct", "Enter a percentage for %-based pricing.")
|
|
143
|
+
return cleaned
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class EditHullForm(HullModeFieldsMixin):
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class AddHullForm(HullModeFieldsMixin):
|
|
151
|
+
eve_type = forms.ModelChoiceField(queryset=EveType.objects.none(), label="Ship")
|
|
152
|
+
|
|
153
|
+
def __init__(self, *args, **kwargs):
|
|
154
|
+
super().__init__(*args, **kwargs)
|
|
155
|
+
self.fields["eve_type"].queryset = ship_type_queryset()
|
|
156
|
+
self.fields["eve_type"].widget.attrs["class"] = "form-select"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Refresh %-based hull prices from Jita on demand.
|
|
2
|
+
|
|
3
|
+
Runs inline (synchronous) by default so you see results immediately; pass
|
|
4
|
+
--async to enqueue the Celery per-type tasks instead (needs a worker).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django.core.management.base import BaseCommand
|
|
8
|
+
|
|
9
|
+
from shop.tasks import _apply_prices_for_type, _enqueue_pct_refreshes, pct_type_ids
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Command(BaseCommand):
|
|
13
|
+
help = "Refresh %-priced hull listings from live Jita prices (Jita 4-4)."
|
|
14
|
+
|
|
15
|
+
def add_arguments(self, parser):
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--async",
|
|
18
|
+
action="store_true",
|
|
19
|
+
dest="use_async",
|
|
20
|
+
help="Enqueue Celery per-type tasks instead of running inline.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def handle(self, *args, **options):
|
|
24
|
+
if options["use_async"]:
|
|
25
|
+
type_ids = _enqueue_pct_refreshes()
|
|
26
|
+
self.stdout.write(
|
|
27
|
+
self.style.SUCCESS(f"Enqueued {len(type_ids)} per-type refresh task(s).")
|
|
28
|
+
)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
type_ids = pct_type_ids()
|
|
32
|
+
if not type_ids:
|
|
33
|
+
self.stdout.write("No %-priced hull listings to refresh.")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
total = 0
|
|
37
|
+
for type_id in type_ids:
|
|
38
|
+
changed = _apply_prices_for_type(type_id)
|
|
39
|
+
total += changed
|
|
40
|
+
self.stdout.write(f" type {type_id}: {changed} listing(s) updated")
|
|
41
|
+
self.stdout.write(
|
|
42
|
+
self.style.SUCCESS(
|
|
43
|
+
f"Done — {total} listing(s) updated across {len(type_ids)} type(s)."
|
|
44
|
+
)
|
|
45
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Recompute stored hull stock for shops with auto-stock listings.
|
|
2
|
+
|
|
3
|
+
Runs inline (synchronous) by default; pass --async to enqueue the Celery
|
|
4
|
+
per-shop tasks instead (needs a worker).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from django.core.management.base import BaseCommand
|
|
8
|
+
|
|
9
|
+
from shop.models import AUTO_STOCK, Shop
|
|
10
|
+
from shop.stock import recompute_shop_stock
|
|
11
|
+
from shop.tasks import _enqueue_stock_refreshes
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Command(BaseCommand):
|
|
15
|
+
help = "Recompute stored hull stock for shops with auto-stock listings."
|
|
16
|
+
|
|
17
|
+
def add_arguments(self, parser):
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--async",
|
|
20
|
+
action="store_true",
|
|
21
|
+
dest="use_async",
|
|
22
|
+
help="Enqueue Celery per-shop tasks instead of running inline.",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def handle(self, *args, **options):
|
|
26
|
+
if options["use_async"]:
|
|
27
|
+
shop_ids = _enqueue_stock_refreshes()
|
|
28
|
+
self.stdout.write(
|
|
29
|
+
self.style.SUCCESS(f"Enqueued {len(shop_ids)} shop refresh task(s).")
|
|
30
|
+
)
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
shops = list(
|
|
34
|
+
Shop.objects.filter(hull_listings__stock_mode=AUTO_STOCK).distinct()
|
|
35
|
+
)
|
|
36
|
+
if not shops:
|
|
37
|
+
self.stdout.write("No shops with auto-stock listings.")
|
|
38
|
+
return
|
|
39
|
+
for shop in shops:
|
|
40
|
+
recompute_shop_stock(shop)
|
|
41
|
+
self.stdout.write(f" {shop.slug}: recomputed")
|
|
42
|
+
self.stdout.write(self.style.SUCCESS(f"Done — {len(shops)} shop(s)."))
|