diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
new file mode 100644
index 00000000..fd0a2102
--- /dev/null
+++ b/.github/workflows/openapi.yml
@@ -0,0 +1,35 @@
+name: OpenAPI Validation & HTTP Test
+
+on:
+ push:
+ branches:
+ - master
+ - next
+ pull_request:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-20.04
+ strategy:
+ max-parallel: 4
+ matrix:
+ python-version: ["3.11.9"]
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: 'pip'
+ - name: Install Dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ pip install -r openapi/requirements.txt
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 18
+ - run: npm i -g dredd
+ - run: dredd --config openapi/dredd.yml
diff --git a/openapi/dredd.yml b/openapi/dredd.yml
new file mode 100644
index 00000000..aace6d9e
--- /dev/null
+++ b/openapi/dredd.yml
@@ -0,0 +1,32 @@
+color: true
+dry-run: null
+hookfiles: ["openapi/dredd_hook.py"]
+language: python
+require: null
+server: "python3 manage.py testserver stregsystem/fixtures/openapi-fixture.json"
+server-wait: 5
+init: false
+custom: {}
+names: false
+only: []
+reporter: apiary
+output: []
+header: []
+sorted: false
+user: null
+inline-errors: false
+details: false
+method: []
+loglevel: warn
+path: []
+hooks-worker-timeout: 5000
+hooks-worker-connect-timeout: 1500
+hooks-worker-connect-retry: 500
+hooks-worker-after-connect-wait: 100
+hooks-worker-term-timeout: 5000
+hooks-worker-term-retry: 500
+hooks-worker-handler-host: 127.0.0.1
+hooks-worker-handler-port: 61321
+config: ./dredd.yml
+blueprint: openapi/stregsystem.yaml
+endpoint: 'http://127.0.0.1:8000'
diff --git a/openapi/dredd_hook.py b/openapi/dredd_hook.py
new file mode 100644
index 00000000..f294b5a5
--- /dev/null
+++ b/openapi/dredd_hook.py
@@ -0,0 +1,42 @@
+import dredd_hooks as hooks
+import json
+from utils import update_query_parameter_values, update_dictionary_values
+
+not_found_parameter_values = {
+ 'room_id': 1,
+ 'member_id': 1,
+ 'username': "404_user",
+}
+
+skipped_endpoints = [
+ "GET (400) /api/member/payment/qr?username=kresten" # Skipped: test can't be implemented properly in OpenAPI
+]
+
+@hooks.before_each
+def skip_endpoint(transaction):
+ if transaction['id'] in skipped_endpoints:
+ print(f"Skipping endpoint: {transaction['id']}")
+ transaction['skip'] = True
+
+
+# https://dredd.org/en/latest/data-structures.html#transaction-object
+@hooks.before_each
+def replace_4xx_parameter_values(transaction):
+ """
+ It isn't possible to specify individual parameter example values for each response type in OpenAPI.
+ To properly test the return value of not-found parameters, replace all parameters.
+ """
+ if transaction['expected']['statusCode'][0] == '4':
+ new_path = update_query_parameter_values(transaction['fullPath'], not_found_parameter_values)
+ print(f"Update endpoint path, from '{transaction['fullPath']}' to '{new_path}'")
+ transaction['fullPath'] = new_path
+ transaction['request']['uri'] = new_path
+
+
+@hooks.before_each
+def replace_body_in_post_requests(transaction):
+ if transaction['expected']['statusCode'][0] == '4' and transaction['id'].startswith("POST"):
+ body = json.loads(transaction['request']['body'])
+ update_dictionary_values(body, not_found_parameter_values)
+
+ transaction['request']['body'] = json.dumps(body)
diff --git a/openapi/requirements.txt b/openapi/requirements.txt
new file mode 100644
index 00000000..2af23796
--- /dev/null
+++ b/openapi/requirements.txt
@@ -0,0 +1 @@
+dredd_hooks
\ No newline at end of file
diff --git a/openapi/stregsystem.yaml b/openapi/stregsystem.yaml
index 5f146d02..ba6a660b 100644
--- a/openapi/stregsystem.yaml
+++ b/openapi/stregsystem.yaml
@@ -190,6 +190,7 @@ components:
required: true
schema:
$ref: '#/components/schemas/member_id'
+ example: 321
room_id_param:
name: room_id
in: query
@@ -197,6 +198,7 @@ components:
required: true
schema:
$ref: '#/components/schemas/room_id'
+ example: 10
username_param:
name: username
in: query
@@ -204,6 +206,7 @@ components:
required: true
schema:
$ref: '#/components/schemas/username'
+ example: kresten
amount_param:
name: amount
in: query
@@ -211,6 +214,9 @@ components:
required: false
schema:
$ref: '#/components/schemas/stregoere_balance'
+ examples:
+ normalBalance:
+ value: 20000
schemas:
memberNotFoundMessage:
type: string
@@ -230,7 +236,7 @@ components:
missingRoomIdMessage:
type: string
example: "Parameter missing: room_id"
- missingMemberUsername:
+ missingMemberUsernameMessage:
type: string
example: "Parameter missing: username"
balance:
@@ -264,14 +270,17 @@ components:
stregoere_price:
type: integer
example: 600
+ stregoere_price_three_beers:
+ type: integer
+ example: 1800
stregoere_balance:
type: integer
example: 20000
- named_products_example:
- type: object
- properties:
- beer:
- $ref: '#/components/schemas/product_id'
+ stregkroner_balance:
+ description: Stregbalance in kroner, only used in API-Sale
+ type: number
+ format: float
+ example: 182.00
sale_input:
type: object
properties:
@@ -288,36 +297,26 @@ components:
$ref: '#/components/schemas/product_name'
price:
$ref: '#/components/schemas/stregoere_price'
- active_products_example:
- type: object
- properties:
- 123:
- $ref: '#/components/schemas/active_product'
buystring:
type: string
example: "kresten beer:3"
- category_name:
+ product_category_name:
type: string
example: "Alcohol"
- category_id:
+ product_category_id:
type: integer
example: 11
- category:
+ product_category:
type: object
properties:
category_id:
- $ref: '#/components/schemas/category_id'
+ $ref: '#/components/schemas/product_category_id'
category_name:
- $ref: '#/components/schemas/category_name'
- category_mapping:
+ $ref: '#/components/schemas/product_category_name'
+ product_category_mapping:
type: array
items:
- $ref: '#/components/schemas/category'
- category_mappings_example:
- type: object
- properties:
- 123:
- $ref: '#/components/schemas/category_mapping'
+ $ref: '#/components/schemas/product_category'
created_on:
type: string
format: date
@@ -330,13 +329,15 @@ components:
type: boolean
example: false
bp_minutes:
+ description: Ballmer Peak minutes
type: integer
nullable: true
- example: 2
+ example: null
bp_seconds:
+ description: Ballmer Peak seconds
type: integer
nullable: true
- example: 30
+ example: null
caffeine:
type: integer
example: 2
@@ -354,7 +355,7 @@ components:
example: true
sale_hints:
type: string
- example: "kresten beer:3"
+ example: "kresten 123:3"
member_has_low_balance:
type: boolean
example: false
@@ -374,6 +375,10 @@ components:
type: array
items:
$ref: '#/components/schemas/product_id'
+ example:
+ - 123
+ - 123
+ - 123
promille:
$ref: '#/components/schemas/promille'
is_ballmer_peaking:
@@ -391,7 +396,7 @@ components:
is_coffee_master:
$ref: '#/components/schemas/is_coffee_master'
cost:
- $ref: '#/components/schemas/stregoere_price'
+ $ref: '#/components/schemas/stregoere_price_three_beers'
give_multibuy_hint:
$ref: '#/components/schemas/give_multibuy_hint'
sale_hints:
@@ -399,7 +404,7 @@ components:
member_has_low_balance:
$ref: '#/components/schemas/member_has_low_balance'
member_balance:
- $ref: '#/components/schemas/stregoere_balance'
+ $ref: '#/components/schemas/stregkroner_balance'
sale:
type: object
properties:
@@ -472,19 +477,25 @@ components:
content:
application/json:
example:
- $ref: '#/components/schemas/named_products_example'
+ beer: 123
ActiveProducts:
description: Dictionary of all activated products, with their name and price (in stregører).
content:
application/json:
example:
- $ref: '#/components/schemas/active_products_example'
+ 123:
+ name: Beer
+ price: 600
CategoryMappings:
description: Dictionary of all activated products, with their mapped categories (both category name and ID).
content:
application/json:
example:
- $ref: '#/components/schemas/category_mappings_example'
+ 123:
+ - category_id:
+ 11
+ category_name:
+ "Alcohol"
SaleSuccess:
description: An object containing various statistics and info regarding the purchase.
content:
@@ -509,18 +520,18 @@ components:
InvalidQRInputResponse:
description: Invalid input has been provided.
content:
- text/html:
+ text/html; charset=utf-8:
schema:
type: string
example: Invalid input for MobilePay QR code generation
MemberUsernameParameter_BadResponse:
description: Member does not exist, or missing parameter.
content:
- text/html:
+ text/html; charset=utf-8:
schema:
oneOf:
- $ref: '#/components/schemas/memberNotFoundMessage'
- - $ref: '#/components/schemas/missingMemberUsername'
+ - $ref: '#/components/schemas/missingMemberUsernameMessage'
examples:
memberNotFound:
$ref: '#/components/examples/MemberNotFoundExample'
@@ -529,7 +540,7 @@ components:
MemberIdParameter_BadResponse:
description: Member does not exist, invalid member ID, or missing parameter.
content:
- text/html:
+ text/html; charset=utf-8:
schema:
oneOf:
- $ref: '#/components/schemas/memberNotFoundMessage'
@@ -545,7 +556,7 @@ components:
RoomIdParameter_BadResponse:
description: Room does not exist, invalid room ID, or missing parameter.
content:
- text/html:
+ text/html; charset=utf-8:
schema:
oneOf:
- $ref: '#/components/schemas/roomNotFoundMessage'
@@ -561,7 +572,7 @@ components:
Member_RoomIdParameter_BadResponse:
description: Room or member does not exist, invalid room or member ID, or missing parameter.
content:
- text/html:
+ text/html; charset=utf-8:
schema:
oneOf:
- $ref: '#/components/schemas/memberNotFoundMessage'
diff --git a/openapi/utils.py b/openapi/utils.py
new file mode 100644
index 00000000..8f01facb
--- /dev/null
+++ b/openapi/utils.py
@@ -0,0 +1,31 @@
+from typing import Any
+from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
+
+
+def update_dictionary_values(original: dict[Any, Any], replacement: dict[Any, Any]) -> None:
+ """
+ Same as dict.update(...) but doesn't add new keys.
+ Mutates 'original'
+ :param original: The dictionary to mutate.
+ :param replacement: The dictionary with values to update the original with.
+ """
+ original.update({k: v for k, v in replacement.items() if k in original})
+
+
+def update_query_parameter_values(url_string: str, new_parameter_values: dict[str, Any]) -> str:
+ """
+ Updates query parameters with new parameter values from new_parameter_values.
+ :param url_string: The URL path of which to modify query parameters.
+ :param new_parameter_values: The dictionary with new query parameter values.
+ :return: The URL with updated query parameter.
+ """
+ parsed_url = urlparse(url_string)
+
+ qs_dict = parse_qs(parsed_url.query, keep_blank_values=True)
+ qs_dict_flattened = {key: value[0] for key, value in qs_dict.items()}
+ update_dictionary_values(qs_dict_flattened, new_parameter_values)
+ updated_qs = urlencode(qs_dict_flattened, doseq=False)
+
+ parsed_url = parsed_url._replace(query=updated_qs)
+
+ return str(urlunparse(parsed_url))
diff --git a/razzia/migrations/0002_alter_razzia_options.py b/razzia/migrations/0002_alter_razzia_options.py
new file mode 100644
index 00000000..d814bbd4
--- /dev/null
+++ b/razzia/migrations/0002_alter_razzia_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.1.13 on 2024-11-29 22:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("razzia", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="razzia",
+ options={"permissions": (("browse_razzia", "Can browse razzias"),)},
+ ),
+ ]
diff --git a/razzia/models.py b/razzia/models.py
index 83edf387..cb81b18c 100644
--- a/razzia/models.py
+++ b/razzia/models.py
@@ -6,7 +6,7 @@
class Razzia(models.Model):
class Meta:
- permissions = (("host_razzia", "Can host a foobar, fnugfald or bread razzia"),)
+ permissions = (("browse_razzia", "Can browse razzias"),)
name = models.CharField(max_length=20)
turns_per_member = models.IntegerField(default=0)
diff --git a/razzia/views.py b/razzia/views.py
index 07d11cef..80214878 100644
--- a/razzia/views.py
+++ b/razzia/views.py
@@ -1,6 +1,7 @@
import datetime
-from django.contrib.auth.decorators import permission_required
+from django.contrib.auth.decorators import permission_required, login_required
+from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect, render, get_object_or_404
from django.utils import timezone
@@ -9,7 +10,7 @@
# Create your views here.
-@permission_required("stregreport.host_razzia")
+@permission_required("razzia.view_razzia")
def razzia(request, razzia_id):
if request.method == 'POST':
return razzia_view_single(request, razzia_id, request.POST['username'])
@@ -17,7 +18,7 @@ def razzia(request, razzia_id):
return razzia_view_single(request, razzia_id, None)
-@permission_required("stregreport.host_razzia")
+@permission_required("razzia.view_razzia")
def razzia_view_single(request, razzia_id, queryname, title=None):
razzia = get_object_or_404(Razzia, pk=razzia_id)
@@ -53,13 +54,21 @@ def razzia_view_single(request, razzia_id, queryname, title=None):
return render(request, template, locals())
-@permission_required("stregreport.host_razzia")
+@login_required
def razzia_menu(request, new_text=None, title=None):
razzias = Razzia.objects.order_by('-pk')[:3]
+
+ if not request.user.has_perm("razzia.browse_razzia"):
+ if len(razzias) == 0:
+ # In case no razzias are available, default to no permission
+ raise PermissionDenied
+
+ return redirect('razzia_view', razzia_id=razzias[0].pk)
+
return render(request, 'menu.html', locals())
-@permission_required("stregreport.host_razzia")
+@permission_required("razzia.add_razzia")
def new_razzia(request):
razzia = Razzia(name="Foobar V2", turn_interval=datetime.timedelta(minutes=30))
razzia.save()
@@ -67,7 +76,7 @@ def new_razzia(request):
return redirect('razzia_view', razzia_id=razzia.pk)
-@permission_required("stregreport.host_razzia")
+@permission_required("razzia.view_razziaentry")
def razzia_members(request, razzia_id, title=None):
razzia = get_object_or_404(Razzia, pk=razzia_id)
unique_members = razzia.members.all().distinct().count()
diff --git a/stregreport/tests.py b/stregreport/tests.py
index 29646a46..e2d16c00 100644
--- a/stregreport/tests.py
+++ b/stregreport/tests.py
@@ -6,6 +6,8 @@
from stregreport.models import BreadRazzia
from freezegun import freeze_time
+from stregsystem.models import Category
+
class ParseIdStringTests(TestCase):
def test_parse_id_string_success(self):
@@ -26,6 +28,36 @@ def test_parse_id_string_unicode(self):
self.assertSequenceEqual([11, 13], res)
+class CategoryRankReportTests(TestCase):
+ fixtures = ["initial_data"]
+
+ def setUp(self):
+ Category.objects.all().delete()
+
+ def test_no_categories(self):
+ self.client.login(username="tester", password="treotreo")
+ response = self.client.get(reverse("report_categoryranks"), {}, follow=True)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed("admin/stregsystem/report/ranks.html")
+
+ self.assertEqual(len(response.context['stat_lists']), 0)
+
+ def test_single_category_shown(self):
+ self.client.login(username="tester", password="treotreo")
+ catA = Category.objects.create(name="Category A")
+ catA.save()
+
+ response = self.client.get(reverse("report_categoryranks"), {}, follow=True)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed("admin/stregsystem/report/ranks.html")
+
+ stat_lists = response.context['stat_lists']
+
+ self.assertEqual(stat_lists[0][0], catA.name)
+
+
class SalesReportTests(TestCase):
fixtures = ["initial_data"]
diff --git a/stregreport/urls.py b/stregreport/urls.py
index da36a6c4..9ace114c 100644
--- a/stregreport/urls.py
+++ b/stregreport/urls.py
@@ -34,7 +34,7 @@
re_path(r'^admin/stregsystem/razzia/wizard_guide/$', views.razzia_wizard),
re_path(r'^admin/stregsystem/razzia/wizard/$', views.razzia_view, name="razzia_view"),
re_path(r'^admin/stregsystem/report/sales/$', views.sales, name="salesreporting"),
- re_path(r'^admin/stregsystem/report/ranks/$', views.ranks),
+ re_path(r'^admin/stregsystem/report/ranks/$', views.ranks, name="report_categoryranks"),
re_path(r'^admin/stregsystem/report/daily/$', views.daily),
re_path(r'^admin/stregsystem/report/ranks/(?P\d+)$', views.ranks),
re_path(r'^admin/stregsystem/report/$', views.reports),
diff --git a/stregreport/views.py b/stregreport/views.py
index 2766b9ee..319e0afe 100644
--- a/stregreport/views.py
+++ b/stregreport/views.py
@@ -303,61 +303,24 @@ def sales_product(request, ids, from_time, to_time, error=None):
# both at 10 o'clock
@permission_required("stregsystem.access_sales_reports")
def ranks_for_year(request, year):
- if year <= 1900 or year > 9999:
- return render(request, 'admin/stregsystem/report/error_ranksnotfound.html', locals())
- milk = [2, 3, 4, 5, 6, 7, 8, 9, 10, 16, 17, 18, 19, 20, 24, 25, 43, 44, 45, 1865]
- caffeine = [11, 12, 30, 34, 37, 1787, 1790, 1791, 1795, 1799, 1800, 1803, 1804, 1837, 1864]
- beer = [
- 13,
- 14,
- 29,
- 42,
- 47,
- 54,
- 65,
- 66,
- 1773,
- 1776,
- 1777,
- 1779,
- 1780,
- 1783,
- 1793,
- 1794,
- 1807,
- 1808,
- 1809,
- 1820,
- 1822,
- 1840,
- 1844,
- 1846,
- 1847,
- 1853,
- 1855,
- 1856,
- 1858,
- 1859,
- ]
- coffee = [32, 35, 36, 39]
- vitamin = [1850, 1851, 1852, 1863, 1880]
-
FORMAT = '%d/%m/%Y kl. %H:%M'
- last_year = year - 1
- next_year = year + 1
from_time = fjule_party(year - 1)
to_time = fjule_party(year)
+
kr_stat_list = sale_money_rank(from_time, to_time)
- beer_stat_list = sale_product_rank(beer, from_time, to_time)
- caffeine_stat_list = sale_product_rank(caffeine, from_time, to_time)
- milk_stat_list = sale_product_rank(milk, from_time, to_time)
- coffee_stat_list = sale_product_rank(coffee, from_time, to_time)
- vitamin_stat_list = sale_product_rank(vitamin, from_time, to_time)
+
+ stat_lists = []
+ for cat in Category.objects.all():
+ stat_lists.append((cat.name, sale_product_rank(get_product_ids_from_category(cat), from_time, to_time)))
+
from_time_string = from_time.strftime(FORMAT)
to_time_string = to_time.strftime(FORMAT)
current_date = timezone.now()
show_next_year = year < current_date.year
is_ongoing = current_date > from_time and current_date <= to_time
+ last_year = year - 1
+ next_year = year + 1
+
return render(request, 'admin/stregsystem/report/ranks.html', locals())
@@ -383,6 +346,10 @@ def sale_money_rank(from_time, to_time, rank_limit=10):
return stat_list
+def get_product_ids_from_category(category):
+ return category.product_set.values_list('id', flat=True)
+
+
# year of the last fjuleparty
def last_fjule_party_year():
current_date = timezone.now()
diff --git a/stregsystem/admin.py b/stregsystem/admin.py
index e87225ef..8b4620d7 100644
--- a/stregsystem/admin.py
+++ b/stregsystem/admin.py
@@ -17,6 +17,7 @@
NamedProduct,
PendingSignup,
Theme,
+ ProductNote,
)
from stregsystem.templatetags.stregsystem_extras import money
from stregsystem.utils import make_active_productlist_query, make_inactive_productlist_query
@@ -362,6 +363,16 @@ def force_hide(modeladmin, request, queryset):
actions = [force_unset, force_show, force_hide]
+class ProductNoteAdmin(admin.ModelAdmin):
+ search_fields = ('active', 'text')
+ list_display = (
+ 'active',
+ 'text',
+ )
+
+ actions = [toggle_active_selected_products]
+
+
admin.site.register(LogEntry, LogEntryAdmin)
admin.site.register(Sale, SaleAdmin)
admin.site.register(Member, MemberAdmin)
@@ -374,3 +385,4 @@ def force_hide(modeladmin, request, queryset):
admin.site.register(MobilePayment, MobilePaymentAdmin)
admin.site.register(PendingSignup)
admin.site.register(Theme, ThemeAdmin)
+admin.site.register(ProductNote, ProductNoteAdmin)
diff --git a/stregsystem/fixtures/openapi-fixture.json b/stregsystem/fixtures/openapi-fixture.json
new file mode 100644
index 00000000..6f721a84
--- /dev/null
+++ b/stregsystem/fixtures/openapi-fixture.json
@@ -0,0 +1,80 @@
+[
+ {
+ "model": "stregsystem.member",
+ "pk": 321,
+ "fields": {
+ "active": true,
+ "username": "kresten",
+ "year": "2025",
+ "firstname": "Kresten",
+ "lastname": "Laust",
+ "gender": "M",
+ "email": "",
+ "want_spam": true,
+ "balance": 20000,
+ "undo_count": 0,
+ "notes": "",
+ "signup_due_paid": true
+ }
+ },
+ {
+ "model": "stregsystem.category",
+ "pk": 11,
+ "fields": {
+ "name": "Alcohol"
+ }
+ },
+ {
+ "model": "stregsystem.room",
+ "pk": 10,
+ "fields": {
+ "name": "Jægerstuen",
+ "description": "Rummet i Jægerstuen",
+ "notes": ""
+ }
+ },
+ {
+ "model": "stregsystem.product",
+ "pk": 123,
+ "fields": {
+ "name": "Beer",
+ "price": 600,
+ "active": true,
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0.2,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 11
+ ],
+ "rooms": []
+ }
+ },
+ {
+ "model": "stregsystem.namedproduct",
+ "pk": 1,
+ "fields": {
+ "name": "beer",
+ "product": 123
+ }
+ },
+ {
+ "model": "auth.user",
+ "pk": 1,
+ "fields": {
+ "password": "pbkdf2_sha256$390000$Q10Z7hLXnYeBvVtJTZNL79$7rDj2xcFEtmAGof2vVHgAEl6/wRlfUMBkp7P60F9Vz0=",
+ "last_login": "2025-01-09T17:57:31.949Z",
+ "is_superuser": true,
+ "username": "tester",
+ "first_name": "",
+ "last_name": "",
+ "email": "",
+ "is_staff": true,
+ "is_active": true,
+ "date_joined": "2025-01-09T17:56:49.184Z",
+ "groups": [],
+ "user_permissions": []
+ }
+ }
+]
\ No newline at end of file
diff --git a/stregsystem/fixtures/testdata.json b/stregsystem/fixtures/testdata.json
index cb19fc9c..455a8f35 100644
--- a/stregsystem/fixtures/testdata.json
+++ b/stregsystem/fixtures/testdata.json
@@ -1,94 +1,74 @@
[
- {
- "model": "auth.user",
- "pk": 1,
- "fields": {
- "password": "pbkdf2_sha256$30000$ivRA8qABWClG$pxi2R4uyotlGpwa66RQuDOXFaO9JElQutMFJqXmnOdI=",
- "is_superuser": true,
- "username": "tester",
- "first_name": "test",
- "last_name": "testsen",
- "is_staff": true
- }
- },
- {
- "model": "auth.user",
- "pk": 2,
- "fields": {
- "password": "pbkdf2_sha256$30000$ivRA8qABWClG$pxi2R4uyotlGpwa66RQuDOXFaO9JElQutMFJqXmnOdI=",
- "is_superuser": true,
- "username": "q",
- "first_name": "q",
- "last_name": "q",
- "is_staff": true
- }
- },
- {
- "model": "auth.user",
- "pk": 3,
- "fields": {
- "password": "pbkdf2_sha256$30000$ivRA8qABWClG$pxi2R4uyotlGpwa66RQuDOXFaO9JElQutMFJqXmnOdI=",
- "username": "nodough",
- "first_name": "no",
- "last_name": "dough"
- }
- },
- {
- "model": "auth.user",
- "pk": 4,
- "fields": {
- "password": "pbkdf2_sha256$30000$ivRA8qABWClG$pxi2R4uyotlGpwa66RQuDOXFaO9JElQutMFJqXmnOdI=",
- "username": "lowdough",
- "first_name": "low",
- "last_name": "dough"
- }
- },
{
"model": "stregsystem.member",
"pk": 1,
"fields": {
+ "active": true,
"username": "tester",
+ "year": "2025",
"firstname": "Test",
"lastname": "Testsen",
+ "gender": "",
"email": "plznospam@fklub.dk",
+ "want_spam": true,
"balance": 13366037,
- "notes": "This is a test user."
+ "undo_count": 0,
+ "notes": "This is a test user.",
+ "signup_due_paid": true
}
},
{
"model": "stregsystem.member",
"pk": 2,
"fields": {
+ "active": true,
"username": "q",
+ "year": "2025",
"firstname": "q",
"lastname": "q",
+ "gender": "",
"email": "plznospam@fklub.dk",
+ "want_spam": true,
"balance": 13366037,
- "notes": "This is a test user with a short name for maximum testing speed."
+ "undo_count": 0,
+ "notes": "This is a test user with a short name for maximum testing speed.",
+ "signup_due_paid": true
}
},
{
"model": "stregsystem.member",
"pk": 3,
"fields": {
+ "active": true,
"username": "nodough",
+ "year": "2025",
"firstname": "No",
"lastname": "Dough",
+ "gender": "",
"email": "plznospam@fklub.dk",
+ "want_spam": true,
"balance": 0,
- "notes": "This is a test user with no stregdollars."
+ "undo_count": 0,
+ "notes": "This is a test user with no stregdollars.",
+ "signup_due_paid": true
}
},
{
"model": "stregsystem.member",
"pk": 4,
"fields": {
+ "active": true,
"username": "lowdough",
+ "year": "2025",
"firstname": "Low",
"lastname": "Dough",
+ "gender": "",
"email": "plznospam@fklub.dk",
+ "want_spam": true,
"balance": 1500,
- "notes": "This is a test user with only 15 stregdollars."
+ "undo_count": 0,
+ "notes": "This is a test user with only 15 stregdollars.",
+ "signup_due_paid": true
}
},
{
@@ -217,6 +197,24 @@
"name": "Te"
}
},
+ {
+ "model": "stregsystem.room",
+ "pk": 1,
+ "fields": {
+ "name": "testrummet",
+ "description": "127.0.0.1",
+ "notes": ""
+ }
+ },
+ {
+ "model": "stregsystem.room",
+ "pk": 2,
+ "fields": {
+ "name": "maymayrummet",
+ "description": "svedigt",
+ "notes": ""
+ }
+ },
{
"model": "stregsystem.product",
"pk": 11,
@@ -224,8 +222,17 @@
"name": "Stregdollar",
"price": 100,
"active": true,
- "rooms": [1],
- "categories": [14]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 14
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -235,8 +242,17 @@
"name": "Øl(Grøn, Classic, Gulddamer) (excl. pant)",
"price": 750,
"active": true,
- "rooms": [1],
- "categories": [3]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 3
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -246,8 +262,17 @@
"name": "Cocio (Mellem)",
"price": 1818,
"active": true,
- "rooms": [1],
- "categories": [12]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 12
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -257,7 +282,15 @@
"name": "Pant B (plastflasker under 1 liter)",
"price": 150,
"active": true,
- "rooms": [1]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -267,9 +300,17 @@
"name": "Voksenvand (1 kop)",
"price": 150,
"active": true,
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
"caffeine_content_mg": 70,
- "rooms": [1],
- "categories": [6]
+ "categories": [
+ 6
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -279,8 +320,15 @@
"name": "Limfjordsporter (excl. pant)",
"price": 1000,
"active": true,
- "rooms": [1],
- "categories": []
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -290,8 +338,17 @@
"name": "Søm",
"price": 300,
"active": true,
- "rooms": [1],
- "categories": [13]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 13
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -301,8 +358,17 @@
"name": "Futtetyr
",
"price": 33300,
"active": true,
- "rooms": [1],
- "categories": [14]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 14
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -312,8 +378,17 @@
"name": "Mambo No. 8",
"price": 800,
"active": true,
- "rooms": [1],
- "categories": [7]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 7
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -323,8 +398,17 @@
"name": "FLAN billet
",
"price": 4269,
"active": true,
- "rooms": [1],
- "categories": [5]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 5
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -334,8 +418,17 @@
"name": "Life potion (excl. pant)",
"price": 1316,
"active": true,
- "rooms": [1],
- "categories": [11]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 11
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -345,8 +438,17 @@
"name": "Kildevæld uden pant (excl. brus)\"",
"price": 706,
"active": true,
- "rooms": [1],
- "categories": [2]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 2
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -356,7 +458,15 @@
"name": "Pant A (glasflasker og metaldåser under 1 liter)",
"price": 100,
"active": true,
- "rooms": [1]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -366,8 +476,17 @@
"name": "Grimbergen (excl. pant)",
"price": 1900,
"active": true,
- "rooms": [1],
- "categories": [3]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 3
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -377,8 +496,17 @@
"name": "Somersby (incl. pant)",
"price": 1540,
"active": true,
- "rooms": [1],
- "categories": [17]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 17
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -388,8 +516,17 @@
"name": "Apache attack helicopter",
"price": 1280,
"active": true,
- "rooms": [1],
- "categories": [10]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 10
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -399,8 +536,17 @@
"name": "Kakao (0,46 kop)",
"price": 69,
"active": true,
- "rooms": [1],
- "categories": [12]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 12
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -410,8 +556,17 @@
"name": "Sportycola (excl. pant)",
"price": 600,
"active": true,
- "rooms": [1],
- "categories": [2]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 2
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -421,8 +576,17 @@
"name": "Kinley (excl. pant)",
"price": 680,
"active": true,
- "rooms": [1],
- "categories": [1]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 1
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -432,8 +596,17 @@
"name": "Bober Øl (excl. pant x2)",
"price": 650,
"active": true,
- "rooms": [1],
- "categories": [3]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 3
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -443,8 +616,17 @@
"name": "Månedens Øl
September: Bonk Beer",
"price": 1460,
"active": true,
- "rooms": [1],
- "categories": [4]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 4
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -454,8 +636,17 @@
"name": "Tebrev (Partibestemt smag)",
"price": 90,
"active": true,
- "rooms": [1],
- "categories": [18]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 18
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -465,8 +656,17 @@
"name": "Ramløse/ vand med brus (excl. pant)",
"price": 808,
"active": true,
- "rooms": [1],
- "categories": [2]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 2
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -476,8 +676,17 @@
"name": "Juice (Solita, Rynkeby)",
"price": 1131,
"active": true,
- "rooms": [1],
- "categories": [16]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 16
+ ],
+ "rooms": [
+ 1
+ ]
}
},
{
@@ -487,8 +696,51 @@
"name": "Faxe Kondi Pro",
"price": 1600,
"active": true,
- "rooms": [1],
- "categories": [1]
+ "start_date": null,
+ "quantity": 0,
+ "deactivate_date": null,
+ "alcohol_content_ml": 0,
+ "caffeine_content_mg": 0,
+ "categories": [
+ 1
+ ],
+ "rooms": [
+ 1
+ ]
+ }
+ },
+ {
+ "model": "stregsystem.productnote",
+ "pk": 1,
+ "fields": {
+ "text": "\r\n",
+ "active": true,
+ "background_color": "Yellow",
+ "text_color": "",
+ "start_date": "2025-01-14",
+ "end_date": "2125-01-14",
+ "comment": "",
+ "products": [
+ 1903
+ ],
+ "rooms": []
+ }
+ },
+ {
+ "model": "stregsystem.productnote",
+ "pk": 2,
+ "fields": {
+ "text": "Udgår!",
+ "active": true,
+ "background_color": "",
+ "text_color": "",
+ "start_date": "2025-01-14",
+ "end_date": "2125-01-14",
+ "comment": "",
+ "products": [
+ 1879
+ ],
+ "rooms": []
}
},
{
@@ -715,7 +967,6 @@
"product": 1891
}
},
-
{
"model": "stregsystem.namedproduct",
"pk": 18912,
@@ -804,22 +1055,6 @@
"product": 1912
}
},
- {
- "model": "stregsystem.room",
- "pk": 1,
- "fields": {
- "name": "testrummet",
- "description": "127.0.0.1"
- }
- },
- {
- "model": "stregsystem.room",
- "pk": 2,
- "fields": {
- "name": "maymayrummet",
- "description": "svedigt"
- }
- },
{
"model": "stregsystem.sale",
"pk": 1,
@@ -827,7 +1062,7 @@
"member": 1,
"product": 11,
"room": 1,
- "timestamp": "2017-03-13T12:52:52.142+00:00",
+ "timestamp": "2017-03-13T12:52:52.142Z",
"price": 100
}
},
@@ -838,7 +1073,7 @@
"member": 1,
"product": 32,
"room": 1,
- "timestamp": "2017-03-13T12:54:12.423+00:00",
+ "timestamp": "2017-03-13T12:54:12.423Z",
"price": 200
}
},
@@ -847,21 +1082,1231 @@
"pk": 3,
"fields": {
"member": 1,
- "product": 14,
+ "product": 42,
"room": 1,
- "timestamp": "2017-03-13T13:38:10.573+00:00",
+ "timestamp": "2017-03-13T13:38:10.573Z",
"price": 300
}
},
{
- "model": "stregsystem.sale",
+ "model": "admin.logentry",
+ "pk": 1,
+ "fields": {
+ "action_time": "2025-01-13T23:27:21.879Z",
+ "user": 1,
+ "content_type": 13,
+ "object_id": "1",
+ "object_repr": "\r\n