From 423d6af6bae4971175b8838dde4d6a00c9ed956f Mon Sep 17 00:00:00 2001 From: Jakob Topholt Jensen <74545500+JakobTopholt@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:17:09 +0100 Subject: [PATCH 01/10] Add gender to user info page (#523) Added gender --- stregsystem/templates/stregsystem/menu_userinfo.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stregsystem/templates/stregsystem/menu_userinfo.html b/stregsystem/templates/stregsystem/menu_userinfo.html index fe6e84a0..38dae22b 100644 --- a/stregsystem/templates/stregsystem/menu_userinfo.html +++ b/stregsystem/templates/stregsystem/menu_userinfo.html @@ -83,6 +83,10 @@

STREGFORBUD!

Årgang (indmeldelsesår) {{member.year}} + + Køn + {{member.gender}} + Forbrug {{stats.total_amount|money}} 𝓕$ / {{stats.total_purchases}} køb From 844b3fcca9d9d6f81604be739b45256e27211eb3 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 31 Oct 2024 10:05:24 +0100 Subject: [PATCH 02/10] Improve advent candle theme (#522) Co-authored-by: Kresten Laust --- stregsystem/fixtures/testdata-themes.json | 2 +- .../static/stregsystem/stregsystem.css | 40 -- .../themes/adventcandle/adventcandle.css | 75 +++ .../themes/adventcandle/adventcandle.js | 37 -- .../themes/adventcandle/adventcandle.html | 566 +++++------------- .../templatetags/stregsystem_extras.py | 6 + stregsystem/themes.json | 2 +- 7 files changed, 222 insertions(+), 506 deletions(-) create mode 100644 stregsystem/static/stregsystem/themes/adventcandle/adventcandle.css delete mode 100644 stregsystem/static/stregsystem/themes/adventcandle/adventcandle.js diff --git a/stregsystem/fixtures/testdata-themes.json b/stregsystem/fixtures/testdata-themes.json index f5aee7ca..ac1e59cf 100644 --- a/stregsystem/fixtures/testdata-themes.json +++ b/stregsystem/fixtures/testdata-themes.json @@ -1001,7 +1001,7 @@ "fields": { "name": "adventcandle", "html": "adventcandle.html", - "js": "adventcandle.js", + "css": "adventcandle.css", "begin_month": 12, "end_month": 12, "end_day": 24 diff --git a/stregsystem/static/stregsystem/stregsystem.css b/stregsystem/static/stregsystem/stregsystem.css index 45302f7d..ec9fd7d1 100644 --- a/stregsystem/static/stregsystem/stregsystem.css +++ b/stregsystem/static/stregsystem/stregsystem.css @@ -133,46 +133,6 @@ table h2, table h3 { fill: none !important; } -#advent_candle { - position: fixed; - bottom: 0; - right: 0; - max-height: 75%; - max-width: 400px; -} - -.flame-main { - animation-name: flameWobble; - animation-duration: 1.5s; - animation-timing-function: linear; - animation-iteration-count: infinite; -} -.flame-main.one { - animation-duration: 2s; - animation-delay: .5s; -} -.flame-main.two { - animation-duration: 1.5s; - animation-delay: 1s; -} -.flame-main.three { - animation-duration: 1.05s; - animation-delay: 1.5s; -} -.flame-main.four { - animation-duration: 1.6s; - animation-delay: 2s; -} -.flame-main.five { - animation-duration: 1.25s; - animation-delay: 2.5s; -} -@keyframes flameWobble { - 50% { - transform: scale(1,1) translate(0, -2px) rotate(.2deg); - } -} - /* Ensures that bats/"flakes" don't interfere with qr-code scanning */ .qr-code { position: relative; diff --git a/stregsystem/static/stregsystem/themes/adventcandle/adventcandle.css b/stregsystem/static/stregsystem/themes/adventcandle/adventcandle.css new file mode 100644 index 00000000..dca2899e --- /dev/null +++ b/stregsystem/static/stregsystem/themes/adventcandle/adventcandle.css @@ -0,0 +1,75 @@ +@property --advent-candle-start-day { + syntax: ""; + inherits: true; + initial-value: 0; +} +@property --advent-candle-day { + syntax: ""; + inherits: true; + initial-value: 0; +} + +#advent-candle { + position: fixed; + bottom: 0; + right: 0; + max-height: 75%; + max-width: 400px; + pointer-events: none; + animation: advent-candle-burn-down 2073600s linear + calc(var(--advent-candle-start-day) * -86400s) forwards; + + @media not screen { + display: none; + } + + & .top, + #advent-candle-trunk-height > rect { + translate: 0 calc(9px * var(--advent-candle-day)); + } + + & .flame > path { + animation: advent-candle-flame-wobble linear infinite; + + @media (prefers-reduced-motion) { + animation-play-state: paused !important; + } + + &:nth-of-type(1) { + animation-duration: 1.5s; + } + &:nth-of-type(2) { + animation-duration: 2s; + animation-delay: -0.5s; + } + &:nth-of-type(3) { + animation-duration: 1.5s; + animation-delay: -1s; + } + &:nth-of-type(4) { + animation-duration: 1.05s; + animation-delay: -1.5s; + } + &:nth-of-type(5) { + animation-duration: 1.6s; + animation-delay: -2s; + } + &:nth-of-type(6) { + animation-duration: 1.25s; + animation-delay: -2.5s; + } + } +} +@keyframes advent-candle-flame-wobble { + 50% { + transform: translate(0, -2px) rotate(0.2deg); + } +} +@keyframes advent-candle-burn-down { + 0% { + --advent-candle-day: 0; + } + 100% { + --advent-candle-day: 24; + } +} diff --git a/stregsystem/static/stregsystem/themes/adventcandle/adventcandle.js b/stregsystem/static/stregsystem/themes/adventcandle/adventcandle.js deleted file mode 100644 index 5a8574af..00000000 --- a/stregsystem/static/stregsystem/themes/adventcandle/adventcandle.js +++ /dev/null @@ -1,37 +0,0 @@ -function modifyCandle(date) { - var dayOfMonth = date.getDate(); - - dayOfMonth = Math.min(25, dayOfMonth); - - for (var i = 1; i < dayOfMonth; i++) { - var dateLabel = document.getElementById("day_" + i); - dateLabel.textContent = ""; - } - - var hour = new Date().getHours(); - var heightPerDay = 9; - - // Will not be continuous because of splitting the day into 48 pieces - // instead of 24. I did so to show some of the current day on the candle - // during the hours where the system is used - var missingHeight = ((dayOfMonth - 1) * heightPerDay) + (10/48 * hour); - var trunkInitialHeight = heightPerDay * 24 + 10; - var newHeight = trunkInitialHeight - missingHeight; - - - var trunk = document.getElementById("trunk"); - trunk.style.height = newHeight; - - // 74 is a constant from the SVG because inkscape places objects - // weirdly. y = 0 in inkscape does not mean y = 0 in the svg... - trunk.setAttribute('y', 74 + missingHeight); - - var topOfCandle = document.getElementById("top"); - - topOfCandle.setAttribute('transform', "translate(0, " + missingHeight + ")"); -} - -setInterval(function(){ - modifyCandle(new Date()); -}, 600000); //Runs every 10th minute -modifyCandle(new Date()); diff --git a/stregsystem/templates/stregsystem/themes/adventcandle/adventcandle.html b/stregsystem/templates/stregsystem/themes/adventcandle/adventcandle.html index e1c856f0..e450b858 100644 --- a/stregsystem/templates/stregsystem/themes/adventcandle/adventcandle.html +++ b/stregsystem/templates/stregsystem/themes/adventcandle/adventcandle.html @@ -1,429 +1,141 @@ - +{% load themes %} +{% load stregsystem_extras %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 24 - 23 - 22 - 21 - 20 - 19 - 18 - 17 - 16 - 15 - 14 - 13 - 12 - 11 - 10 - 9 - 8 - 7 - 6 - 5 - 4 - 3 - 2 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + id="advent-candle" + width="400" + height="600" + viewBox="0 0 200 300" +> + + + + + + + + + + + + + + + + + + + + 24 + 23 + 22 + 21 + 20 + 19 + 18 + 17 + 16 + 15 + 14 + 13 + 12 + 11 + 10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file diff --git a/stregsystem/templatetags/stregsystem_extras.py b/stregsystem/templatetags/stregsystem_extras.py index 9dac3d5a..6d0a1804 100644 --- a/stregsystem/templatetags/stregsystem_extras.py +++ b/stregsystem/templatetags/stregsystem_extras.py @@ -54,6 +54,12 @@ def day_of_month(): return datetime.now().day +@register.simple_tag +def fractional_day_of_month(): + now = datetime.now() + return now.day - 1 + (now.hour / 24) + (now.minute / 1440) + (now.second / 86400) + + @register.simple_tag def random(min, max): return randint(min, max) diff --git a/stregsystem/themes.json b/stregsystem/themes.json index 52cb1594..37af6741 100644 --- a/stregsystem/themes.json +++ b/stregsystem/themes.json @@ -40,7 +40,7 @@ { "name": "adventcandle", "html": "adventcandle.html", - "js": "adventcandle.js", + "css": "adventcandle.css", "begin": { "month": 12 }, From a85b159f80035ab8d6899e5933b1aaae68aa3d0c Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 7 Nov 2024 12:31:22 +0100 Subject: [PATCH 03/10] Better form id (#524) --- stregsystem/templates/stregsystem/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stregsystem/templates/stregsystem/index.html b/stregsystem/templates/stregsystem/index.html index ef032861..0266e9e3 100644 --- a/stregsystem/templates/stregsystem/index.html +++ b/stregsystem/templates/stregsystem/index.html @@ -22,7 +22,7 @@ margin-bottom: .3em; } } - #focusform { + #quickbuy-form { margin-top: 1.5em; margin-bottom: 1.5em; } @@ -47,7 +47,7 @@ -
{% csrf_token %} + {% csrf_token %} {% block saleform %}

From 28f50426e78eb227a90253aee96cbcf959c50c7c Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 12 Nov 2024 13:18:04 +0100 Subject: [PATCH 04/10] Improve menu form (#527) --- stregsystem/templates/stregsystem/menu.html | 96 ++++++++++--------- .../stregsystem/purchase_heatmap.html | 4 +- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/stregsystem/templates/stregsystem/menu.html b/stregsystem/templates/stregsystem/menu.html index a09de04e..2d77810e 100644 --- a/stregsystem/templates/stregsystem/menu.html +++ b/stregsystem/templates/stregsystem/menu.html @@ -12,8 +12,21 @@ padding-top: .8em; padding-bottom: 1em; } - #productlist { + #product-buy-form { padding-bottom: 1em; + + & button { + background: none; + border: none; + cursor: pointer; + font: inherit; + text-decoration: underline; + color: LinkText; + + &:active{ + color: ActiveText; + } + } } {% endblock %} @@ -76,58 +89,51 @@

Du {% endblock %} -
- {% block products %} - {% autoescape off %} - {% if product_list %} - - - - - - {% for product in product_list|partition:"2"|first %} - - - - - {% endfor %} -
ProduktPris
- - {% csrf_token %} - {{product.name}} - - - - - {{product.price|money}} kr
- {% if product_list|partition:"2"|last %} + {% block products %} + {% autoescape off %} + {% if product_list %} +
+ {% csrf_token %} + + +
- {% for product in product_list|partition:"2"|last %} - - - - + {% for product in product_list|partition:"2"|first %} + + + + {% endfor %}
Produkt Pris
- - {% csrf_token %} - {{product.name}} - - - - - {{product.price|money}} kr
+ + {{product.price|money}} kr
- {% endif %} - {% else %} -

Ingen produkter.

+ {% if product_list|partition:"2"|last %} + + + + + + {% for product in product_list|partition:"2"|last %} + + + + + {% endfor %} +
ProduktPris
+ + {{product.price|money}} kr
+
+ {% endif %} - {% endautoescape %} - {% endblock %} -
+ {% else %} +

Ingen produkter.

+ {% endif %} + {% endautoescape %} + {% endblock %}
{% include "stregsystem/purchase_heatmap.html" %} diff --git a/stregsystem/templates/stregsystem/purchase_heatmap.html b/stregsystem/templates/stregsystem/purchase_heatmap.html index ded5dc4b..588c88d5 100644 --- a/stregsystem/templates/stregsystem/purchase_heatmap.html +++ b/stregsystem/templates/stregsystem/purchase_heatmap.html @@ -152,7 +152,7 @@

Forbrugsoversigt (Antal)

} function updateAllProductNumbers(hide=false){ - document.querySelectorAll("input[name='product_id']") + document.querySelectorAll("button[name='product_id']") .forEach(p => changeProductCountElement(p.parentElement, 0, hide)) } @@ -184,7 +184,7 @@

Forbrugsoversigt (Antal)

} for (const [product_id, count] of Object.entries(product_counts)){ - const productCountElement = document.querySelector(`input[name='product_id'][value='${product_id}']`)?.parentElement; + const productCountElement = document.querySelector(`button[name='product_id'][value='${product_id}']`)?.parentElement; if (productCountElement === undefined){ console.warn(`Product doesn't exist in tabel: ${product_id}`); From 3d2d2eb2780318d09dfd02e2ef0b4ddc2afb14d2 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 21 Nov 2024 12:34:02 +0100 Subject: [PATCH 05/10] Add a footer (#525) Co-authored-by: Kresten Laust --- .../static/stregsystem/stregsystem.css | 28 +++++++++++++++++-- stregsystem/templates/stregsystem/base.html | 26 +++++++++-------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/stregsystem/static/stregsystem/stregsystem.css b/stregsystem/static/stregsystem/stregsystem.css index ec9fd7d1..54a3d9e9 100644 --- a/stregsystem/static/stregsystem/stregsystem.css +++ b/stregsystem/static/stregsystem/stregsystem.css @@ -1,6 +1,10 @@ body { - color: black; - background-color: white; + color: black; + background-color: white; + margin: 0; + display: flex; + flex-direction: column; + min-height: 100vh; } header { @@ -10,6 +14,7 @@ header { column-gap: 4vw; text-align: center; place-items: center; + border-bottom: solid grey 1.5px; & .left { justify-self: start; @@ -19,6 +24,10 @@ header { } } +#base-content { + flex: 1; +} + main { padding: 1em; @@ -140,6 +149,21 @@ table h2, table h3 { image-rendering: pixelated; } +footer { + text-align: center; + border-top: solid grey 1.5px; + + #footer-notes { + & li { + display: inline; + + & ~ li::before { + content: '• '; + } + } + } +} + @keyframes blink-frame { 0%, 100% { opacity: 1; diff --git a/stregsystem/templates/stregsystem/base.html b/stregsystem/templates/stregsystem/base.html index 1895f873..8204a912 100644 --- a/stregsystem/templates/stregsystem/base.html +++ b/stregsystem/templates/stregsystem/base.html @@ -40,19 +40,23 @@

-
- +
{% block content %}{% endblock %} - -
- -
-{% block news %} -{% if news %} -{{news.text}} -{% endif %} -{% endblock %}
+
+
+ {% block news %} + {% if news %} + {{news.text}} + {% endif %} + {% endblock %} +
+ +
{% theme_content %} From 949f252bc62233b9e8bd714fc87d6c2e961f5be2 Mon Sep 17 00:00:00 2001 From: Kresten Laust Date: Thu, 28 Nov 2024 15:57:30 +0100 Subject: [PATCH 06/10] Document API with OpenAPI spec (API 1.1) (#420) * Add initial OpenAPI spec * Add endpoint categories * Update info and add version * Define API endpoint * Finish Get Member Info-endpoint * Finish Get Member Balance-endpoint * Correct descriptions and add missing descriptions * Update schemas to include member_id and username * Finish Get Member ID-endpoint * Bump version to fit version in #421 * Rename operation IDs to URL names * Finish Get Member Active-endpoint * Finish Get Member Sales-endpoint * Rename product_price schema * Finish Get Payment QR-Endpoint * Add NamedProducts-Endpoint * Add response for missing param, refactor res-value * Rename stregdollar to stregoere This was done to make it more clear, what the value actually is * Refactor named_products schemas * Add Active Products-endpoint * Draft category mapping-Endpoint * Finish Dump Category Mapping-endpoint * Progress related to documenting Sale-API * Expand Sale-API response * Add Ballmer-peak measures * Fix invalid response format * Fix wrong keywords * Reduce 200-example * Make api/member-endpoint valid * Make multiple api/member/*-endpoints valid * fixup! Make multiple api/member/*-endpoints valid * Make endpoints requiring room_id valid * Make api/sale-endpoint valid * Make 'api/member/payment/qr'-endpoint valid * Remove old individual responses * Make '/api/member/get_id'-endpoint valid * Change description to be more intuitive * Add request body to 'api/sale'-endpoint * Made request-example coherent with response-example * Add description to 'api/sale'-endpoint * Add local endpoint as server * Rename file to stregsystem.yaml * Rename parameters to unique IDs * Convert examples as schemas into examples * Rename operationIds to fit changes in #421 --- openapi/stregsystem.yaml | 585 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100644 openapi/stregsystem.yaml diff --git a/openapi/stregsystem.yaml b/openapi/stregsystem.yaml new file mode 100644 index 00000000..5f146d02 --- /dev/null +++ b/openapi/stregsystem.yaml @@ -0,0 +1,585 @@ +openapi: 3.0.3 +info: + title: Stregsystem + description: |- + This is the API implemented in the Stregsystem Django App. + The API describes ways to interact with Stregsystem without using the web-interface. + Existing client software utilizing the API include Stregsystem-CLI (STS) and Fappen (F-Club Web App). + + Disclaimer - The implementation is not generated using this specification, therefore they can get out of sync if changes are made directly to the codebase without updating the OpenAPI specification file accordingly. + version: "1.1" +externalDocs: + description: Find out more about Stregsystemet at GitHub. + url: https://github.com/f-klubben/stregsystemet/ +servers: + - url: https://stregsystem.fklub.dk + - url: http://127.0.0.1:8000 +tags: + - name: Member + description: Related to the individual member page. + - name: Products + description: Related to the products. + - name: Sale + description: Related to performing a sale. +paths: + /api/member: + get: + tags: + - Member + summary: Get member info + description: Gets a member's balance, username, active-status and name. + operationId: api_member_info + parameters: + - $ref: '#/components/parameters/member_id_param' + responses: + '200': + $ref: '#/components/responses/MemberFound' + '400': + $ref: '#/components/responses/MemberIdParameter_BadResponse' + /api/member/balance: + get: + tags: + - Member + summary: Get member balance + description: Gets a member's balance. + operationId: api_member_balance + parameters: + - $ref: '#/components/parameters/member_id_param' + responses: + '200': + $ref: '#/components/responses/MemberFound_Balance' + '400': + $ref: '#/components/responses/MemberIdParameter_BadResponse' + /api/member/active: + get: + tags: + - Member + summary: Get member active-status + description: Gets whether a member is active. + operationId: api_member_active + parameters: + - $ref: '#/components/parameters/member_id_param' + responses: + '200': + $ref: '#/components/responses/MemberFound_Active' + '400': + $ref: '#/components/responses/MemberIdParameter_BadResponse' + /api/member/sales: + get: + tags: + - Member + summary: Get member sales + description: Gets a list of a member's purchases. + operationId: api_member_sales + parameters: + - $ref: '#/components/parameters/member_id_param' + responses: + '200': + $ref: '#/components/responses/MemberFound_Sales' + '400': + $ref: '#/components/responses/MemberIdParameter_BadResponse' + /api/member/get_id: + get: + tags: + - Member + summary: Get member ID + description: Gets a member's ID from their username. + operationId: api_member_id + parameters: + - $ref: '#/components/parameters/username_param' + responses: + '200': + $ref: '#/components/responses/MemberFound_ID' + '400': + $ref: '#/components/responses/MemberUsernameParameter_BadResponse' + /api/member/payment/qr: + get: + tags: + - Member + summary: Get payment QR code + description: Returns a QR code for payment for a member. + operationId: api_payment_qr + parameters: + - $ref: '#/components/parameters/username_param' + - $ref: '#/components/parameters/amount_param' + responses: + '200': + $ref: '#/components/responses/QRCodeGenerated' + '400': + $ref: '#/components/responses/InvalidQRInputResponse' + /api/products/named_products: + get: + tags: + - Products + summary: Gets dictionary of named products + description: Returns a dictionary of all named products with their product ID. A named product is a shorthand associated with a product ID. + operationId: api_named_products + responses: + '200': + $ref: '#/components/responses/NamedProducts' + /api/products/active_products: + get: + tags: + - Products + summary: Gets dictionary of products that are active + description: Dictionary of products, key is product ID, value is product name and product price. + operationId: api_active_products + parameters: + - $ref: '#/components/parameters/room_id_param' + responses: + '200': + $ref: '#/components/responses/ActiveProducts' + '400': + $ref: '#/components/responses/RoomIdParameter_BadResponse' + /api/products/category_mappings: + get: + tags: + - Products + summary: Gets a dictionary of products with categories + description: Dictionary of product IDs with category ID and category name as value. + operationId: api_category_mappings + responses: + '200': + $ref: '#/components/responses/CategoryMappings' + /api/sale: + post: + tags: + - Sale + summary: Posts a sale using a buy-string + description: Performs a sale by a buy-string, room and member id, then returns info regarding the purchase. + operationId: api_sale + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/sale_input' + responses: + '200': + $ref: '#/components/responses/SaleSuccess' + '400': + $ref: '#/components/responses/Member_RoomIdParameter_BadResponse' +components: + examples: + MemberNotFoundExample: + summary: Member does not exist + value: "Member not found" + InvalidMemberIdExample: + summary: Member ID is invalid + value: "Parameter invalid: member_id" + MissingMemberIdExample: + summary: No member ID given + value: "Parameter missing: member_id" + RoomNotFoundExample: + summary: Room does not exist + value: "Room not found" + InvalidRoomIdExample: + summary: Room ID is invalid + value: "Parameter invalid: room_id" + MissingRoomIdExample: + summary: No room ID given + value: "Parameter missing: room_id" + MissingMemberUsernameExample: + summary: No username given + value: "Parameter missing: username" + parameters: + member_id_param: + name: member_id + in: query + description: ID of the member to retrieve. + required: true + schema: + $ref: '#/components/schemas/member_id' + room_id_param: + name: room_id + in: query + description: ID of the room to retrieve. + required: true + schema: + $ref: '#/components/schemas/room_id' + username_param: + name: username + in: query + description: Username of the member. + required: true + schema: + $ref: '#/components/schemas/username' + amount_param: + name: amount + in: query + description: Amount of money in streg-oere. + required: false + schema: + $ref: '#/components/schemas/stregoere_balance' + schemas: + memberNotFoundMessage: + type: string + example: Member not found + invalidMemberIdMessage: + type: string + example: "Parameter invalid: member_id" + missingMemberIdMessage: + type: string + example: "Parameter missing: member_id" + roomNotFoundMessage: + type: string + example: Room not found + invalidRoomIdMessage: + type: string + example: "Parameter invalid: room_id" + missingRoomIdMessage: + type: string + example: "Parameter missing: room_id" + missingMemberUsername: + type: string + example: "Parameter missing: username" + balance: + type: integer + example: 20000 + username: + type: string + example: kresten + active: + type: boolean + example: true + name: + type: string + example: Kresten Laust + member_id: + type: integer + example: 321 + room_id: + type: integer + example: 10 + timestamp: + type: string + format: date-time + example: 2004-01-07T15:30:55Z + product_name: + type: string + example: Beer + product_id: + type: integer + example: 123 + stregoere_price: + type: integer + example: 600 + stregoere_balance: + type: integer + example: 20000 + named_products_example: + type: object + properties: + beer: + $ref: '#/components/schemas/product_id' + sale_input: + type: object + properties: + member_id: + $ref: '#/components/schemas/member_id' + buystring: + $ref: '#/components/schemas/buystring' + room: + $ref: '#/components/schemas/room_id' + active_product: + type: object + properties: + name: + $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: + type: string + example: "Alcohol" + category_id: + type: integer + example: 11 + category: + type: object + properties: + category_id: + $ref: '#/components/schemas/category_id' + category_name: + $ref: '#/components/schemas/category_name' + category_mapping: + type: array + items: + $ref: '#/components/schemas/category' + category_mappings_example: + type: object + properties: + 123: + $ref: '#/components/schemas/category_mapping' + created_on: + type: string + format: date + example: "2024-05-12T18:26:09.508Z" + promille: + type: number + format: float + example: 0.2 + is_ballmer_peaking: + type: boolean + example: false + bp_minutes: + type: integer + nullable: true + example: 2 + bp_seconds: + type: integer + nullable: true + example: 30 + caffeine: + type: integer + example: 2 + cups: + type: integer + example: 4 + product_contains_caffeine: + type: boolean + example: true + is_coffee_master: + type: boolean + example: false + give_multibuy_hint: + type: boolean + example: true + sale_hints: + type: string + example: "kresten beer:3" + member_has_low_balance: + type: boolean + example: false + sale_values_result_example: + type: object + properties: + order: + type: object + properties: + room: + $ref: '#/components/schemas/room_id' + member: + $ref: '#/components/schemas/member_id' + created_on: + $ref: '#/components/schemas/created_on' + items: + type: array + items: + $ref: '#/components/schemas/product_id' + promille: + $ref: '#/components/schemas/promille' + is_ballmer_peaking: + $ref: '#/components/schemas/is_ballmer_peaking' + bp_minutes: + $ref: '#/components/schemas/bp_minutes' + bp_seconds: + $ref: '#/components/schemas/bp_seconds' + caffeine: + $ref: '#/components/schemas/caffeine' + cups: + $ref: '#/components/schemas/cups' + product_contains_caffeine: + $ref: '#/components/schemas/product_contains_caffeine' + is_coffee_master: + $ref: '#/components/schemas/is_coffee_master' + cost: + $ref: '#/components/schemas/stregoere_price' + give_multibuy_hint: + $ref: '#/components/schemas/give_multibuy_hint' + sale_hints: + $ref: '#/components/schemas/sale_hints' + member_has_low_balance: + $ref: '#/components/schemas/member_has_low_balance' + member_balance: + $ref: '#/components/schemas/stregoere_balance' + sale: + type: object + properties: + timestamp: + $ref: '#/components/schemas/timestamp' + product: + $ref: '#/components/schemas/product_name' + price: + $ref: '#/components/schemas/stregoere_price' + sales: + type: array + items: + $ref: '#/components/schemas/sale' + MemberInfo: + type: object + properties: + balance: + $ref: '#/components/schemas/balance' + username: + $ref: '#/components/schemas/username' + active: + $ref: '#/components/schemas/active' + name: + $ref: '#/components/schemas/name' + responses: + MemberFound: + description: Member found. + content: + application/json: + schema: + $ref: '#/components/schemas/MemberInfo' + MemberFound_ID: + description: Member found. + content: + application/json: + schema: + type: object + properties: + member_id: + $ref: '#/components/schemas/member_id' + MemberFound_Balance: + description: Member found. + content: + application/json: + schema: + type: object + properties: + balance: + $ref: '#/components/schemas/balance' + MemberFound_Active: + description: Member found. + content: + application/json: + schema: + type: object + properties: + active: + $ref: '#/components/schemas/active' + MemberFound_Sales: + description: Member found. + content: + application/json: + schema: + type: object + properties: + sales: + $ref: '#/components/schemas/sales' + NamedProducts: + description: Dictionary of all named_product names. + content: + application/json: + example: + $ref: '#/components/schemas/named_products_example' + 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' + 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' + SaleSuccess: + description: An object containing various statistics and info regarding the purchase. + content: + application/json: + schema: + type: object + properties: + status: + type: integer + example: 200 + msg: + type: string + example: "OK" + values: + $ref: '#/components/schemas/sale_values_result_example' + QRCodeGenerated: + description: QR code with link to open MobilePay with the provided information. + content: + image/svg+xml: + schema: + type: string + InvalidQRInputResponse: + description: Invalid input has been provided. + content: + text/html: + schema: + type: string + example: Invalid input for MobilePay QR code generation + MemberUsernameParameter_BadResponse: + description: Member does not exist, or missing parameter. + content: + text/html: + schema: + oneOf: + - $ref: '#/components/schemas/memberNotFoundMessage' + - $ref: '#/components/schemas/missingMemberUsername' + examples: + memberNotFound: + $ref: '#/components/examples/MemberNotFoundExample' + missingMemberUsername: + $ref: '#/components/examples/MissingMemberUsernameExample' + MemberIdParameter_BadResponse: + description: Member does not exist, invalid member ID, or missing parameter. + content: + text/html: + schema: + oneOf: + - $ref: '#/components/schemas/memberNotFoundMessage' + - $ref: '#/components/schemas/invalidMemberIdMessage' + - $ref: '#/components/schemas/missingMemberIdMessage' + examples: + memberNotFound: + $ref: '#/components/examples/MemberNotFoundExample' + invalidMemberId: + $ref: '#/components/examples/InvalidMemberIdExample' + missingMemberId: + $ref: '#/components/examples/MissingMemberIdExample' + RoomIdParameter_BadResponse: + description: Room does not exist, invalid room ID, or missing parameter. + content: + text/html: + schema: + oneOf: + - $ref: '#/components/schemas/roomNotFoundMessage' + - $ref: '#/components/schemas/invalidRoomIdMessage' + - $ref: '#/components/schemas/missingRoomIdMessage' + examples: + roomNotFound: + $ref: '#/components/examples/RoomNotFoundExample' + invalidRoomId: + $ref: '#/components/examples/InvalidRoomIdExample' + missingRoomId: + $ref: '#/components/examples/MissingRoomIdExample' + Member_RoomIdParameter_BadResponse: + description: Room or member does not exist, invalid room or member ID, or missing parameter. + content: + text/html: + schema: + oneOf: + - $ref: '#/components/schemas/memberNotFoundMessage' + - $ref: '#/components/schemas/invalidMemberIdMessage' + - $ref: '#/components/schemas/missingMemberIdMessage' + - $ref: '#/components/schemas/roomNotFoundMessage' + - $ref: '#/components/schemas/invalidRoomIdMessage' + - $ref: '#/components/schemas/missingRoomIdMessage' + examples: + memberNotFound: + $ref: '#/components/examples/MemberNotFoundExample' + invalidMemberId: + $ref: '#/components/examples/InvalidMemberIdExample' + missingMemberId: + $ref: '#/components/examples/MissingMemberIdExample' + roomNotFound: + $ref: '#/components/examples/RoomNotFoundExample' + invalidRoomId: + $ref: '#/components/examples/InvalidRoomIdExample' + missingRoomId: + $ref: '#/components/examples/MissingRoomIdExample' From 8dea5259b89ea05ae37c8c1bbb588c19b9ce6938 Mon Sep 17 00:00:00 2001 From: Kresten Laust Date: Fri, 29 Nov 2024 19:19:34 +0100 Subject: [PATCH 07/10] Correct razzia-overhaul breadcrumps (#528) * Correct razzia-overhaul breadcrumps * Use actual razzia name * Update title to 'V2' * Update title of razzia search screen --- razzia/templates/members.html | 4 ++-- razzia/templates/menu.html | 4 ++-- razzia/templates/razzia.html | 3 ++- razzia/templates/razzia_base.html | 2 +- razzia/templates/razzia_search.html | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/razzia/templates/members.html b/razzia/templates/members.html index 49be2cb6..e4b64fc7 100644 --- a/razzia/templates/members.html +++ b/razzia/templates/members.html @@ -1,7 +1,7 @@ {% extends "admin/stregsystem/razzia/razzia.html" %} -{% block title %}{{title|default:"Razzia"}}{% endblock %} -{% block breadcrumbs %}{% endblock %} +{% block title %}{{razzia.name}} - Members{% endblock %} +{% block breadcrumbs %}{% endblock %} {% block razzia_content %}

Back to razzia

diff --git a/razzia/templates/menu.html b/razzia/templates/menu.html index d18ee249..f5b35fe3 100644 --- a/razzia/templates/menu.html +++ b/razzia/templates/menu.html @@ -1,8 +1,8 @@ {% extends "razzia_base.html" %} -{% block title %}{{title|default:"Razzia"}}{% endblock %} +{% block title %}{{title|default:"Razzia V2"}}{% endblock %} -{% block breadcrumbs %}{% endblock %} +{% block breadcrumbs %}{% endblock %} {% block razzia_content %} diff --git a/razzia/templates/razzia.html b/razzia/templates/razzia.html index 4bcf6c6a..655f2539 100644 --- a/razzia/templates/razzia.html +++ b/razzia/templates/razzia.html @@ -1,6 +1,7 @@ {% extends "razzia_search.html" %} -{% block breadcrumbs %}{% endblock %} +{% block breadcrumbs %}{% endblock %} {% block member_present %}
diff --git a/razzia/templates/razzia_base.html b/razzia/templates/razzia_base.html index 78ab72ec..b276670c 100644 --- a/razzia/templates/razzia_base.html +++ b/razzia/templates/razzia_base.html @@ -1,6 +1,6 @@ {% extends "admin/base_site.html" %} -{% block title %}{{title|default:"Razzia"}}{% endblock %} +{% block title %}{{razzia.name}} - Razzia{% endblock %} {% block content %} {% load static %} diff --git a/razzia/templates/razzia_search.html b/razzia/templates/razzia_search.html index 9c7ad884..a143aa52 100644 --- a/razzia/templates/razzia_search.html +++ b/razzia/templates/razzia_search.html @@ -3,7 +3,7 @@ {% block razzia_content %}
-

{{title|default:"Razzia"}}!

+

{{razzia.name}} - Razzia

{% csrf_token %}
From 0da745617bfa66cd9873e09af9117e270baaa308 Mon Sep 17 00:00:00 2001 From: Thomas Jensen <32744171+Mast3rwaf1z@users.noreply.github.com> Date: Fri, 29 Nov 2024 20:18:02 +0100 Subject: [PATCH 08/10] [Feature] Nix Flake (#459) * added initial nix flake implementation * updated readme * added mailhog to packages * added documentation * added black to devshell * initial default package * added shellhook * added shellhook * added shellhook * removed shellhook, added envvars * not possible lol * fixed versioning * rewrote the functionality of the nix flake, removed hashes from requirements * removed extra comment * updated readme * updated readme * specify python version more accurately * fixing some python dependencies * update flake lock --- .gitignore | 5 ++++ README.md | 11 ++++++++- flake.lock | 26 ++++++++++++++++++++ flake.nix | 44 ++++++++++++++++++++++++++++++++++ nix-support/default.nix | 8 +++++++ nix-support/django-select2.nix | 20 ++++++++++++++++ 6 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix-support/default.nix create mode 100644 nix-support/django-select2.nix diff --git a/.gitignore b/.gitignore index 80ad0b34..b9bcaad6 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,8 @@ stregsystem/*-tokens.json.bak # ignore default log file location stregsystem.log + +# nix +.direnv +.envrc +result \ No newline at end of file diff --git a/README.md b/README.md index 7fc41d27..2b052929 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,16 @@ For Ubuntu with virtual envs: 5. ??? 6. Profit +For systems running the Nix package manager: +1. Configure Nix to use nix-command and flakes + - `echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf` +2. Start shell + - `nix develop` +2. Or run the system + - `nix run . testserver stregsystem/fixtures/testdata.json` +3. ??? +4. Profit + For Mac users with virtual envs: 1. Install python3.11 with pip - `brew install python@3.11` @@ -43,7 +53,6 @@ For Mac users with virtual envs: 5. ??? 6. Profit - Using Testdata -------- In order to simplify development for all, we have included a test fixture. diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..fb974cce --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1732749044, + "narHash": "sha256-T38FQOg0BV5M8FN1712fovzNakSOENEYs+CSkg31C9Y=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0c5b4ecbed5b155b705336aa96d878e55acd8685", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-24.05", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..4bfed7bb --- /dev/null +++ b/flake.nix @@ -0,0 +1,44 @@ +{ + description = "F-klubbens Stregsystem"; + + # define nixpkgs version to use + inputs = { + nixpkgs.url = "nixpkgs/nixos-24.05"; + }; + + outputs = { self, nixpkgs }: let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + + dependencies = import ./nix-support { inherit pkgs; }; + + in { + # Define the shell, here we're just setting the packages required for the devshell + devShells.${system}.default = pkgs.mkShell { + packages = (dependencies pkgs.python311Packages) ++ [pkgs.mailhog pkgs.black]; + }; + + # Default package for the stregsystem + packages.${system}.default = let + env = pkgs.python311.withPackages dependencies; + in pkgs.stdenv.mkDerivation { + pname = "stregsystemet"; + version = "latest"; + src = ./.; + + installPhase = '' + mkdir -p $out/bin + mkdir -p $out/share/stregsystemet + + cp local.cfg.skel local.cfg + echo "${env.interpreter} $out/share/stregsystemet/manage.py \$@" > $out/bin/stregsystemet + sed -i '1 i #!${pkgs.bash}/bin/bash' $out/bin/stregsystemet + chmod +x $out/bin/stregsystemet + + sed -i '1d' manage.py + + cp ./* $out/share/stregsystemet -r + ''; + }; + }; +} diff --git a/nix-support/default.nix b/nix-support/default.nix new file mode 100644 index 00000000..01c78be4 --- /dev/null +++ b/nix-support/default.nix @@ -0,0 +1,8 @@ +{pkgs}: let + # Based on requirements.txt, determine dependencies to fetch from nixpkgs + requirements = ../requirements.txt; + # Read file and split it into lines + lines = pkgs.lib.lists.remove "" (pkgs.lib.strings.splitString "\n" (builtins.readFile requirements)); + packages = map (pkg: builtins.elemAt (pkgs.lib.strings.splitString "==" pkg) 0) lines; + filteredPackages = builtins.filter (pkg: pkg != "Django-Select2") packages; +in py: [(pkgs.callPackage ./django-select2.nix { inherit pkgs py; })] ++ map (pkg: py.${pkgs.lib.toLower pkg}) filteredPackages \ No newline at end of file diff --git a/nix-support/django-select2.nix b/nix-support/django-select2.nix new file mode 100644 index 00000000..f3fc5283 --- /dev/null +++ b/nix-support/django-select2.nix @@ -0,0 +1,20 @@ +{pkgs, py}: + +py.buildPythonPackage { + pname = "Django-Select2"; + version = "8.1.2"; + src = pkgs.fetchurl { + url = "https://files.pythonhosted.org/packages/7f/af/2fc371e1dea6686b588ab9cd445e55b63248a525f8c90550767a42dd77bb/django_select2-8.1.2.tar.gz"; + sha256 = "sha256-9EaF7hw5CQqt4B4+vCVnAvBWIPPHijwmhECtmmYHCHY="; + }; + format = "pyproject"; + doCheck = false; + buildInputs = []; + checkInputs = []; + nativeBuildInputs = []; + propagatedBuildInputs = [ + py.django-appconf + py.flit-scm + ]; +} + From 4d76a80498a7b4fc730b8ed764f0b6f65108e6d8 Mon Sep 17 00:00:00 2001 From: Kresten Laust Date: Fri, 29 Nov 2024 22:24:38 +0100 Subject: [PATCH 09/10] Razzia correct member stats (#529) * Add unique member count to members page * Correct term on menu page * Fix tests by updating expected result * Add testing for unique members --- razzia/templates/members.html | 2 +- razzia/templates/menu.html | 2 +- razzia/tests.py | 50 +++++++++++++++++++++++++++++++++-- razzia/views.py | 1 + 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/razzia/templates/members.html b/razzia/templates/members.html index e4b64fc7..bb210e4e 100644 --- a/razzia/templates/members.html +++ b/razzia/templates/members.html @@ -5,7 +5,7 @@ {% block razzia_content %}

Back to razzia

-

Registered members at razzia {{ razzia.start_date | date:"D d M Y" }} - {{ razzia.members.all | length }} fember(s)

+

Registered members at razzia {{ razzia.start_date | date:"D d M Y" }} - {{ razzia.members.all | length }} entries / {{ unique_members }} fember(s)

{% for entry in razzia.razziaentry_set.all %}
  • {{ entry.member.firstname }} {{ entry.member.lastname }} ({{ entry.member.username }}) at {{entry.time}}
  • diff --git a/razzia/templates/menu.html b/razzia/templates/menu.html index f5b35fe3..a201e2d8 100644 --- a/razzia/templates/menu.html +++ b/razzia/templates/menu.html @@ -19,7 +19,7 @@

    Previous razzias

    -

    {{ razzia.start_date | date:"D d M Y" }} - {{razzia.members.all | length}} fember(s)

    +

    {{ razzia.start_date | date:"D d M Y" }} - {{razzia.members.all | length}} entries

    diff --git a/razzia/tests.py b/razzia/tests.py index 06f28d86..db5fe88c 100644 --- a/razzia/tests.py +++ b/razzia/tests.py @@ -60,8 +60,8 @@ def test_razzia_member_can_register_multiple_times(self): # Note: Different interface, should be fixed later. # self.assertNotContains(response_add_1, "last checked in at", status_code=200) # self.assertContains(response_add_2, "last checked in at", status_code=200) - self.assertContains(response_members_0, "0 fember(s)", status_code=200) - self.assertContains(response_members_2, "2 fember(s)", status_code=200) + self.assertContains(response_members_0, "0 entr", status_code=200) + self.assertContains(response_members_2, "2 entr", status_code=200) def test_razzia_registered_member_is_in_member_list(self): self.client.login(username="tester", password="treotreo") @@ -75,3 +75,49 @@ def test_razzia_registered_member_is_in_member_list(self): self.assertEqual(response_add.status_code, 200) self.assertTemplateUsed(response_members, "members.html") self.assertContains(response_members, "jokke", status_code=200) + + def test_razzia_unique_members_unique(self): + self.client.login(username="tester", password="treotreo") + response = self.client.get(reverse("razzia_new"), follow=True) + razzia_url, _ = response.redirect_chain[-1] + + response_add = self.client.post(razzia_url, {"username": "jokke"}, follow=True) + self.assertEqual(response_add.status_code, 200) + + response_members = self.client.get(razzia_url + "members", follow=True) + + self.assertTemplateUsed(response_members, "members.html") + self.assertContains(response_members, "jokke", status_code=200) + + response_add_2 = self.client.post(razzia_url, {"username": "jan"}, follow=True) + self.assertEqual(response_add_2.status_code, 200) + + response_members_2 = self.client.get(razzia_url + "members", follow=True) + self.assertTemplateUsed(response_members, "members.html") + self.assertContains(response_members, "jan", status_code=200) + + self.assertContains(response_members_2, "2 entr", status_code=200) + self.assertContains(response_members_2, "2 fember(s)", status_code=200) + + def test_razzia_unique_members_same(self): + with freeze_time() as frozen_datetime: + self.client.login(username="tester", password="treotreo") + response = self.client.get(reverse("razzia_new"), follow=True) + razzia_url, _ = response.redirect_chain[-1] + + response_members_0 = self.client.get(razzia_url + "members", follow=True) + + response_add_1 = self.client.post(razzia_url, {"username": "jokke"}, follow=True) + frozen_datetime.tick(delta=datetime.timedelta(hours=1, minutes=1)) + response_add_2 = self.client.post(razzia_url, {"username": "jokke"}, follow=True) + + response_members_2 = self.client.get(razzia_url + "members", follow=True) + + self.assertEqual(response_add_1.status_code, 200) + self.assertEqual(response_add_2.status_code, 200) + self.assertTemplateUsed(response_add_1, "razzia.html") + self.assertTemplateUsed(response_add_2, "razzia.html") + self.assertContains(response_members_0, "0 entr", status_code=200) + self.assertContains(response_members_0, "0 fember(s)", status_code=200) + self.assertContains(response_members_2, "2 entr", status_code=200) + self.assertContains(response_members_2, "1 fember(s)", status_code=200) diff --git a/razzia/views.py b/razzia/views.py index b8f1d8aa..07d11cef 100644 --- a/razzia/views.py +++ b/razzia/views.py @@ -70,4 +70,5 @@ def new_razzia(request): @permission_required("stregreport.host_razzia") def razzia_members(request, razzia_id, title=None): razzia = get_object_or_404(Razzia, pk=razzia_id) + unique_members = razzia.members.all().distinct().count() return render(request, 'members.html', locals()) From a02bfdb887550f6feb01e849902ce90a6b2dc87b Mon Sep 17 00:00:00 2001 From: Kresten Laust Date: Fri, 29 Nov 2024 22:41:14 +0100 Subject: [PATCH 10/10] Improve API (API 1.1) (#421) * Streamline GetMemberID with other Member-API endpoints The other endpoints use 'Invalid ID' to signify either missing or invalid. This endpoint used to use 'Invalid Username' to signify that the member does not exist, while the other's would've used 'Member not found' for that. * Consistent member/user terminology There's an inconsistency in whether to call a member a user. Generally, users are the ones with admin access, while members are the customers. * Consistent view-method naming Even though it's a bool, I believe it's more appropriate to keep the naming consistent with non-bool return types. Also, it still returns an object. * Make get ID consistent as well I won't rename the actual endpoint because of backwards compatibility. * Rename Get Payment QR-endpoint * Refactor Get Payment QR * Rename named_products method * Change return text * Remove use of annotations from future * Object instead of array in Active Products-endpoint More meaningful response in Active Products-endpoint, now returns {"1": {"name": "Beer", "price": 600}}, instead of {"1": ["Beer", 600]} * TODO Check whether room exists * Rename URL name * Strongly typed return-object for category mapping * Make QR Form and helper function generic * Rename 'member' -> 'username'-parameter * Change minimum value for QR-code form to 0 * Fix incorrect 400 response body's * Make api/sale-endpoint responses more specific * Remove unused import * Rename view-functions * Change naming scheme --- stregsystem/forms.py | 2 +- stregsystem/urls.py | 20 +++++----- stregsystem/utils.py | 13 ++++++- stregsystem/views.py | 92 +++++++++++++++++++++++++------------------- 4 files changed, 75 insertions(+), 52 deletions(-) diff --git a/stregsystem/forms.py b/stregsystem/forms.py index 65e16b94..6ba947d7 100644 --- a/stregsystem/forms.py +++ b/stregsystem/forms.py @@ -72,7 +72,7 @@ def __init__(self, *args, **kwargs): class QRPaymentForm(forms.Form): member = forms.CharField(max_length=16) - amount = forms.DecimalField(min_value=50, decimal_places=2, required=False) + amount = forms.DecimalField(min_value=0, decimal_places=2, required=False) class PurchaseForm(forms.Form): diff --git a/stregsystem/urls.py b/stregsystem/urls.py index 135124ed..9fbbe9bb 100644 --- a/stregsystem/urls.py +++ b/stregsystem/urls.py @@ -33,14 +33,14 @@ re_path(r'^(?P\d+)/user/(?P\d+)/pay$', views.menu_userpay, name="userpay"), re_path(r'^(?P\d+)/user/(?P\d+)/rank$', views.menu_userrank, name="userrank"), re_path(r'^(?P\d+)/send_csv_mail/(?P\d+)/$', views.send_userdata, name="send_userdata"), - re_path(r'^api/member/payment/qr$', views.qr_payment, name="payment_qr"), - re_path(r'^api/member/active$', views.check_user_active, name="active_member"), - re_path(r'^api/member/sales$', views.get_user_sales, name="get_user_sales"), - re_path(r'^api/member/get_id$', views.convert_username_to_id, name="get_id"), - re_path(r'^api/member/balance$', views.get_user_balance, name="get_user_balance"), - re_path(r'^api/member$', views.get_user_info, name="get_user_transactions"), - re_path(r'^api/products/named_products$', views.dump_named_items, name="named_products"), - re_path(r'^api/products/active_products$', views.dump_active_items, name="active_products"), - re_path(r'^api/products/category_mappings$', views.dump_product_category_mappings, name="product_mappings"), - re_path(r'^api/sale$', views.api_sale, name="sale"), + re_path(r'^api/member/payment/qr$', views.get_payment_qr, name="api_payment_qr"), + re_path(r'^api/member/active$', views.get_member_active, name="api_member_active"), + re_path(r'^api/member/sales$', views.get_member_sales, name="api_member_sales"), + re_path(r'^api/member/get_id$', views.get_member_id, name="api_member_id"), + re_path(r'^api/member/balance$', views.get_member_balance, name="api_member_balance"), + re_path(r'^api/member$', views.get_member_info, name="api_member_info"), + re_path(r'^api/products/named_products$', views.get_named_products, name="api_named_products"), + re_path(r'^api/products/active_products$', views.get_active_items, name="api_active_products"), + re_path(r'^api/products/category_mappings$', views.get_product_category_mappings, name="api_product_mappings"), + re_path(r'^api/sale$', views.api_sale, name="api_sale"), ] diff --git a/stregsystem/utils.py b/stregsystem/utils.py index 10ad31e0..baafca0e 100644 --- a/stregsystem/utils.py +++ b/stregsystem/utils.py @@ -14,6 +14,8 @@ import qrcode import qrcode.image.svg +import urllib.parse + logger = logging.getLogger(__name__) @@ -161,7 +163,7 @@ def strip_emoji(text): ).strip() -def qr_code(data): +def qr_code(data) -> HttpResponse: response = HttpResponse(content_type="image/svg+xml") qr = qrcode.make(data, image_factory=qrcode.image.svg.SvgPathFillImage) qr.save(response) @@ -169,6 +171,15 @@ def qr_code(data): return response +def mobilepay_launch_uri(comment: str, amount: float) -> str: + query = {'phone': '90601', 'comment': comment} + + if amount is not None: + query['amount'] = amount + + return 'mobilepay://send?{}'.format(urllib.parse.urlencode(query)) + + class stregsystemTestRunner(DiscoverRunner): def __init__(self, *args, **kwargs): settings.TEST_MODE = True diff --git a/stregsystem/views.py b/stregsystem/views.py index c0f94279..90700ee3 100644 --- a/stregsystem/views.py +++ b/stregsystem/views.py @@ -1,7 +1,6 @@ import datetime import io import json -import urllib.parse from typing import List, Type import pytz @@ -20,6 +19,7 @@ from django.shortcuts import get_object_or_404, render, redirect from django.utils import timezone from django.views.decorators.csrf import csrf_exempt +from django_select2 import forms as s2forms from stregreport.views import fjule_party @@ -44,6 +44,7 @@ from stregsystem.utils import ( make_active_productlist_query, qr_code, + mobilepay_launch_uri, make_room_specific_query, make_unprocessed_mobilepayment_query, parse_csv_and_create_mobile_payments, @@ -563,18 +564,18 @@ def signup_tool(request): return render(request, "admin/stregsystem/approval_tools/signup_tool.html", data) -def qr_payment(request): +# API views + + +def get_payment_qr(request): form = QRPaymentForm(request.GET) if not form.is_valid(): return HttpResponseBadRequest("Invalid input for MobilePay QR code generation") - query = {'phone': '90601', 'comment': form.cleaned_data.get('member')} - - if form.cleaned_data.get("amount") is not None: - query['amount'] = form.cleaned_data.get("amount") + username = form.cleaned_data.get('username') + amount = form.cleaned_data.get('amount') - data = 'mobilepay://send?{}'.format(urllib.parse.urlencode(query)) - return qr_code(data) + return qr_code(mobilepay_launch_uri(username, amount)) def signup(request): @@ -620,26 +621,24 @@ def signup_status(request, signup_id): return render(request, "stregsystem/signup_status.html", locals()) -# API views - - -def dump_active_items(request): +def get_active_items(request): room_id = request.GET.get('room_id') or None if room_id is None: - return HttpResponseBadRequest("Missing room_id") + return HttpResponseBadRequest("Parameter missing: room_id") elif not room_id.isdigit(): - return HttpResponseBadRequest("Invalid room_id") + return HttpResponseBadRequest("Parameter invalid: room_id") + # TODO: Check whether room exists items = __get_productlist(room_id) - items_dict = {item.id: (item.name, item.price) for item in items} + items_dict = {item.id: {'name': item.name, 'price': item.price} for item in items} return JsonResponse(items_dict, json_dumps_params={'ensure_ascii': False}) -def check_user_active(request): +def get_member_active(request): member_id = request.GET.get('member_id') or None if member_id is None: - return HttpResponseBadRequest("Missing member_id") + return HttpResponseBadRequest("Parameter missing: member_id") elif not member_id.isdigit(): - return HttpResponseBadRequest("Invalid member_id") + return HttpResponseBadRequest("Parameter invalid: member_id") try: member = Member.objects.get(pk=member_id) except Member.DoesNotExist: @@ -647,27 +646,34 @@ def check_user_active(request): return JsonResponse({'active': member.active}) -def convert_username_to_id(request): +def get_member_id(request): username = request.GET.get('username') or None if username is None: - return HttpResponseBadRequest("Missing username") + return HttpResponseBadRequest("Parameter missing: username") + try: member = Member.objects.get(username=username) except Member.DoesNotExist: - return HttpResponseBadRequest("Invalid username") + return HttpResponseBadRequest("Member not found") + return JsonResponse({'member_id': member.id}) -def dump_product_category_mappings(request): - return JsonResponse({p.id: [(cat.id, cat.name) for cat in p.categories.all()] for p in Product.objects.all()}) +def get_product_category_mappings(request): + return JsonResponse( + { + p.id: [{'category_id': cat.id, 'category_name': cat.name} for cat in p.categories.all()] + for p in Product.objects.all() + } + ) -def get_user_sales(request): +def get_member_sales(request): member_id = request.GET.get('member_id') or None if member_id is None: - return HttpResponseBadRequest("Missing member_id") + return HttpResponseBadRequest("Parameter missing: member_id") elif not member_id.isdigit(): - return HttpResponseBadRequest("Invalid member_id") + return HttpResponseBadRequest("Parameter invalid: member_id") count = 10 if request.GET.get('count') is None else int(request.GET.get('count') or 10) sales = Sale.objects.filter(member=member_id).order_by('-timestamp')[:count] return JsonResponse( @@ -675,12 +681,12 @@ def get_user_sales(request): ) -def get_user_balance(request): +def get_member_balance(request): member_id = request.GET.get('member_id') or None if member_id is None: - return HttpResponseBadRequest("Missing member_id") + return HttpResponseBadRequest("Parameter missing: member_id") elif not member_id.isdigit(): - return HttpResponseBadRequest("Invalid member_id") + return HttpResponseBadRequest("Parameter invalid: member_id") try: member = Member.objects.get(pk=member_id) except Member.DoesNotExist: @@ -688,10 +694,12 @@ def get_user_balance(request): return JsonResponse({'balance': member.balance}) -def get_user_info(request): +def get_member_info(request): member_id = str(request.GET.get('member_id')) or None - if member_id is None or not member_id.isdigit(): - return HttpResponseBadRequest("Missing or invalid member_id") + if member_id is None: + return HttpResponseBadRequest("Parameter missing: member_id") + elif not member_id.isdigit(): + return HttpResponseBadRequest("Parameter invalid: member_id") member = find_user_from_id(int(member_id)) if member is None: @@ -714,7 +722,7 @@ def find_user_from_id(user_id: int): return None -def dump_named_items(request): +def get_named_products(request): items = NamedProduct.objects.all() items_dict = {item.name: item.product.id for item in items} return JsonResponse(items_dict, json_dumps_params={'ensure_ascii': False}) @@ -730,12 +738,16 @@ def api_sale(request): room = str(data['room']) or None member_id = str(data['member_id']) or None - if room is None or not room.isdigit(): - return HttpResponseBadRequest("Missing or invalid room") + if room is None: + return HttpResponseBadRequest("Parameter missing: room") + if not room.isdigit(): + return HttpResponseBadRequest("Parameter invalid: room") if buy_string is None: - return HttpResponseBadRequest("Missing buystring") - if member_id is None or not member_id.isdigit(): - return HttpResponseBadRequest("Missing or invalid member_id") + return HttpResponseBadRequest("Parameter missing: buystring") + if member_id is None: + return HttpResponseBadRequest("Parameter missing: member_id") + if not member_id.isdigit(): + return HttpResponseBadRequest("Parameter invalid: member_id") try: username, bought_ids = parser.parse(_pre_process(buy_string)) @@ -744,7 +756,7 @@ def api_sale(request): member = find_user_from_id(int(member_id)) if member is None: - return HttpResponseBadRequest("Invalid member_id") + return HttpResponseBadRequest("Parameter invalid: member_id") if not member.signup_due_paid: return HttpResponseBadRequest("Signup due not paid") @@ -761,7 +773,7 @@ def api_sale(request): try: room = Room.objects.get(pk=room) except Room.DoesNotExist: - return HttpResponseBadRequest("Invalid room") + return HttpResponseBadRequest("Parameter invalid: room") msg, status, ret_obj = api_quicksale(request, room, member, bought_ids) return JsonResponse( {'status': status, 'msg': msg, 'values': ret_obj}, json_dumps_params={'ensure_ascii': False}