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