Add purchase types
This commit is contained in:
parent
2eaccc57b0
commit
e4c6e9e414
94
CHANGELOG.md
94
CHANGELOG.md
|
@ -1,5 +1,99 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## New
|
||||||
|
* Add stat for finished this year's games
|
||||||
|
* Add purchase types:
|
||||||
|
* Game (previously all of them were this type)
|
||||||
|
* DLC
|
||||||
|
* Season Pass
|
||||||
|
* Battle Pass
|
||||||
|
|
||||||
|
## 1.4.0 / 2023-11-09 21:01+01:00
|
||||||
|
|
||||||
|
### New
|
||||||
|
* More fields are now optional. This is to make it easier to add new items in bulk.
|
||||||
|
* Game: Wikidata ID
|
||||||
|
* Edition: Platform, Year
|
||||||
|
* Purchase: Platform
|
||||||
|
* Platform: Group
|
||||||
|
* Session: Device
|
||||||
|
* New fields:
|
||||||
|
* Game: Year Released
|
||||||
|
* To record original year of release
|
||||||
|
* Upon migration, this will be set to a year of any of the game's edition that has it set
|
||||||
|
* Purchase: Date Finished
|
||||||
|
* Editions are now unique combination of name and platform
|
||||||
|
* Add more stats:
|
||||||
|
* All finished games
|
||||||
|
* All finished 2023 games
|
||||||
|
* All finished games that were purchased this year
|
||||||
|
* Sessions (count)
|
||||||
|
* Days played
|
||||||
|
* Finished (count)
|
||||||
|
* Unfinished (count)
|
||||||
|
* Refunded (count)
|
||||||
|
* Backlog Decrease (count)
|
||||||
|
* New workflow:
|
||||||
|
* Adding Game, Edition, Purchase, and Session in a row is now much faster
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
* game overview: simplify playtime range display
|
||||||
|
* new session: order devices alphabetically
|
||||||
|
* ignore English articles when sorting names
|
||||||
|
* added a new sort_name field that gets automatically created
|
||||||
|
* automatically fill certain values in forms:
|
||||||
|
* new game: name and sort name after typing
|
||||||
|
* new edition: name, sort name, and year when selecting game
|
||||||
|
* new purchase: platform when selecting edition
|
||||||
|
|
||||||
|
## 1.3.0 / 2023-11-05 15:09+01:00
|
||||||
|
|
||||||
|
### New
|
||||||
|
* Add Stats to the main navigation
|
||||||
|
* Allow selecting year on the Stats page
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
* Make some pages redirect back instead to session list
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
* Make navigation more compact
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Correctly limit sessions to a single year for stats
|
||||||
|
|
||||||
|
## 1.2.0 / 2023-11-01 20:18+01:00
|
||||||
|
|
||||||
|
### New
|
||||||
|
* Add yearly stats page (https://git.kucharczyk.xyz/lukas/timetracker/issues/15)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
* Add a button to start session from game overview
|
||||||
|
|
||||||
|
## 1.1.2 / 2023-10-13 16:30+02:00
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
* Durations are formatted in a consisent manner across all pages
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
* Game Overview: display duration when >1 hour instead of displaying 0
|
||||||
|
|
||||||
|
## 1.1.1 / 2023-10-09 20:52+02:00
|
||||||
|
|
||||||
|
### New
|
||||||
|
* Add notes section to game overview
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
* Make it possible to add any data on the game overview page
|
||||||
|
|
||||||
|
## 1.1.0 / 2023-10-09 00:01+02:00
|
||||||
|
|
||||||
|
### New
|
||||||
|
* Add game overview page (https://git.kucharczyk.xyz/lukas/timetracker/issues/8)
|
||||||
|
* Add helper buttons next to datime fields
|
||||||
|
* Add copy button on Add session page to copy times between fields
|
||||||
|
* Change fonts to IBM Plex
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
* Improve form appearance
|
* Improve form appearance
|
||||||
* Add helper buttons next to datime fields
|
* Add helper buttons next to datime fields
|
||||||
* Change recent session view to current year instead of last 30 days
|
* Change recent session view to current year instead of last 30 days
|
||||||
|
|
|
@ -66,6 +66,12 @@ textarea {
|
||||||
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form input:disabled,
|
||||||
|
select:disabled,
|
||||||
|
textarea:disabled {
|
||||||
|
@apply dark:bg-slate-700 dark:text-slate-400;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
form input,
|
form input,
|
||||||
select,
|
select,
|
||||||
|
|
|
@ -77,10 +77,7 @@ class PurchaseForm(forms.ModelForm):
|
||||||
)
|
)
|
||||||
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
||||||
related_purchase = forms.ModelChoiceField(
|
related_purchase = forms.ModelChoiceField(
|
||||||
queryset=Purchase.objects.filter(type=Purchase.GAME).order_by(
|
queryset=Purchase.objects.order_by("edition__sort_name")
|
||||||
"edition__sort_name"
|
|
||||||
),
|
|
||||||
required=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
# Generated by Django 4.1.5 on 2023-11-14 08:41
|
# Generated by Django 4.1.5 on 2023-11-14 08:41
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("games", "0026_purchase_type"),
|
("games", "0026_purchase_type"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# Generated by Django 4.1.5 on 2023-11-14 11:05
|
# Generated by Django 4.1.5 on 2023-11-14 11:05
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
from games.models import Purchase
|
from games.models import Purchase
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -118,16 +118,12 @@ class Purchase(models.Model):
|
||||||
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
|
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
|
||||||
)
|
)
|
||||||
type = models.CharField(max_length=255, choices=TYPES, default=GAME)
|
type = models.CharField(max_length=255, choices=TYPES, default=GAME)
|
||||||
name = models.CharField(max_length=255, default="", null=True, blank=True)
|
name = models.CharField(
|
||||||
related_purchase = models.ForeignKey(
|
max_length=255, default="Unknown Name", null=True, blank=True
|
||||||
"Purchase",
|
)
|
||||||
on_delete=models.SET_NULL,
|
related_purchase = models.ForeignKey(
|
||||||
default=None,
|
"Purchase", on_delete=models.SET_NULL, default=None, null=True, blank=True
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name="related_purchases",
|
|
||||||
)
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
platform_info = self.platform
|
platform_info = self.platform
|
||||||
|
@ -135,6 +131,14 @@ class Purchase(models.Model):
|
||||||
platform_info = f"{self.edition.platform} version on {self.platform}"
|
platform_info = f"{self.edition.platform} version on {self.platform}"
|
||||||
return f"{self.edition} ({platform_info}, {self.edition.year_released}, {self.get_ownership_type_display()})"
|
return f"{self.edition} ({platform_info}, {self.edition.year_released}, {self.get_ownership_type_display()})"
|
||||||
|
|
||||||
|
def is_game(self):
|
||||||
|
return self.type == self.GAME
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.type == Purchase.GAME:
|
||||||
|
self.name = ""
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Platform(models.Model):
|
class Platform(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
|
|
@ -1148,6 +1148,15 @@ textarea) {
|
||||||
color: rgb(241 245 249 / var(--tw-text-opacity));
|
color: rgb(241 245 249 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:is(.dark form input:disabled),:is(.dark
|
||||||
|
select:disabled),:is(.dark
|
||||||
|
textarea:disabled) {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(51 65 85 / var(--tw-bg-opacity));
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
form input,
|
form input,
|
||||||
select,
|
select,
|
||||||
|
@ -1309,6 +1318,22 @@ th label {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sm\:pl-2 {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm\:pl-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm\:pl-6 {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm\:decoration-2 {
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
import {
|
import { syncSelectInputUntilChanged, getEl, conditionalElementHandler } from "./utils.js";
|
||||||
syncSelectInputUntilChanged,
|
|
||||||
getEl,
|
|
||||||
disableElementsWhenTrue,
|
|
||||||
disableElementsWhenFalse,
|
|
||||||
} from "./utils.js";
|
|
||||||
|
|
||||||
let syncData = [
|
let syncData = [
|
||||||
{
|
{
|
||||||
|
@ -16,28 +11,21 @@ let syncData = [
|
||||||
|
|
||||||
syncSelectInputUntilChanged(syncData, "form");
|
syncSelectInputUntilChanged(syncData, "form");
|
||||||
|
|
||||||
function setupElementHandlers() {
|
|
||||||
disableElementsWhenTrue("#id_type", "game", [
|
|
||||||
"#id_name",
|
|
||||||
"#id_related_purchase",
|
|
||||||
]);
|
|
||||||
disableElementsWhenFalse("#id_type", "game", ["#id_date_finished"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", setupElementHandlers);
|
let myConfig = [
|
||||||
document.addEventListener("htmx:afterSwap", setupElementHandlers);
|
() => {
|
||||||
|
return getEl("#id_type").value == "game";
|
||||||
|
},
|
||||||
|
["#id_name", "#id_related_purchase"],
|
||||||
|
(el) => {
|
||||||
|
el.disabled = "disabled";
|
||||||
|
},
|
||||||
|
(el) => {
|
||||||
|
el.disabled = "";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
document.DOMContentLoaded = conditionalElementHandler(...myConfig)
|
||||||
getEl("#id_type").onchange = () => {
|
getEl("#id_type").onchange = () => {
|
||||||
setupElementHandlers();
|
conditionalElementHandler(...myConfig)
|
||||||
};
|
}
|
||||||
|
|
||||||
document.body.addEventListener('htmx:beforeRequest', function(event) {
|
|
||||||
// Assuming 'Purchase1' is the element that triggers the HTMX request
|
|
||||||
if (event.target.id === 'id_edition') {
|
|
||||||
var idEditionValue = document.getElementById('id_edition').value;
|
|
||||||
|
|
||||||
// Condition to check - replace this with your actual logic
|
|
||||||
if (idEditionValue != '') {
|
|
||||||
event.preventDefault(); // This cancels the HTMX request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
@ -7,3 +7,122 @@ export function toISOUTCString(date) {
|
||||||
let month = (date.getMonth() + 1).toString().padStart(2, 0);
|
let month = (date.getMonth() + 1).toString().padStart(2, 0);
|
||||||
return `${date.getFullYear()}-${month}-${date.getDate()}T${date.getHours()}:${date.getMinutes()}`;
|
return `${date.getFullYear()}-${month}-${date.getDate()}T${date.getHours()}:${date.getMinutes()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Sync values between source and target elements based on syncData configuration.
|
||||||
|
* @param {Array} syncData - Array of objects to define source and target elements with their respective value types.
|
||||||
|
*/
|
||||||
|
function syncSelectInputUntilChanged(syncData, parentSelector = document) {
|
||||||
|
const parentElement =
|
||||||
|
parentSelector === document
|
||||||
|
? document
|
||||||
|
: document.querySelector(parentSelector);
|
||||||
|
|
||||||
|
if (!parentElement) {
|
||||||
|
console.error(`The parent selector "${parentSelector}" is not valid.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Set up a single change event listener on the document for handling all source changes
|
||||||
|
parentElement.addEventListener("change", function (event) {
|
||||||
|
// Loop through each sync configuration item
|
||||||
|
syncData.forEach((syncItem) => {
|
||||||
|
// Check if the change event target matches the source selector
|
||||||
|
if (event.target.matches(syncItem.source)) {
|
||||||
|
const sourceElement = event.target;
|
||||||
|
const valueToSync = getValueFromProperty(
|
||||||
|
sourceElement,
|
||||||
|
syncItem.source_value
|
||||||
|
);
|
||||||
|
const targetElement = document.querySelector(syncItem.target);
|
||||||
|
|
||||||
|
if (targetElement && valueToSync !== null) {
|
||||||
|
targetElement[syncItem.target_value] = valueToSync;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up a single focus event listener on the document for handling all target focuses
|
||||||
|
parentElement.addEventListener(
|
||||||
|
"focus",
|
||||||
|
function (event) {
|
||||||
|
// Loop through each sync configuration item
|
||||||
|
syncData.forEach((syncItem) => {
|
||||||
|
// Check if the focus event target matches the target selector
|
||||||
|
if (event.target.matches(syncItem.target)) {
|
||||||
|
// Remove the change event listener to stop syncing
|
||||||
|
// This assumes you want to stop syncing once any target receives focus
|
||||||
|
// You may need a more sophisticated way to remove listeners if you want to stop
|
||||||
|
// syncing selectively based on other conditions
|
||||||
|
document.removeEventListener("change", syncSelectInputUntilChanged);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true
|
||||||
|
); // Use capture phase to ensure the event is captured during focus, not bubble
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Retrieve the value from the source element based on the provided property.
|
||||||
|
* @param {Element} sourceElement - The source HTML element.
|
||||||
|
* @param {string} property - The property to retrieve the value from.
|
||||||
|
*/
|
||||||
|
function getValueFromProperty(sourceElement, property) {
|
||||||
|
let source = (sourceElement instanceof HTMLSelectElement) ? sourceElement.selectedOptions[0] : sourceElement
|
||||||
|
if (property.startsWith("dataset.")) {
|
||||||
|
let datasetKey = property.slice(8); // Remove 'dataset.' part
|
||||||
|
return source.dataset[datasetKey];
|
||||||
|
} else if (property in source) {
|
||||||
|
return source[property];
|
||||||
|
} else {
|
||||||
|
console.error(`Property ${property} is not valid for the option element.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns a single element by name.
|
||||||
|
* @param {string} selector The selector to look for.
|
||||||
|
*/
|
||||||
|
function getEl(selector) {
|
||||||
|
if (selector.startsWith("#")) {
|
||||||
|
return document.getElementById(selector.slice(1))
|
||||||
|
}
|
||||||
|
else if (selector.startsWith(".")) {
|
||||||
|
return document.getElementsByClassName(selector)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return document.getElementsByName(selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Does something to elements when something happens.
|
||||||
|
* @param {() => boolean} condition The condition that is being tested.
|
||||||
|
* @param {string[]} targetElements
|
||||||
|
* @param {(elementName: HTMLElement) => void} callbackfn1 Called when the condition matches.
|
||||||
|
* @param {(elementName: HTMLElement) => void} callbackfn2 Called when the condition doesn't match.
|
||||||
|
*/
|
||||||
|
function conditionalElementHandler(condition, targetElements, callbackfn1, callbackfn2) {
|
||||||
|
if (condition()) {
|
||||||
|
targetElements.forEach((elementName) => {
|
||||||
|
let el = getEl(elementName);
|
||||||
|
if (el === null) {
|
||||||
|
console.error("Element ${elementName} doesn't exist.");
|
||||||
|
} else {
|
||||||
|
callbackfn1(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
targetElements.forEach((elementName) => {
|
||||||
|
let el = getEl(elementName);
|
||||||
|
if (el === null) {
|
||||||
|
console.error("Element ${elementName} doesn't exist.");
|
||||||
|
} else {
|
||||||
|
callbackfn2(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler };
|
||||||
|
|
|
@ -196,10 +196,11 @@
|
||||||
{% for purchase in all_purchased_this_year %}
|
{% for purchase in all_purchased_this_year %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<a class="underline decoration-slate-500 sm:decoration-2"
|
<a class="underline decoration-slate-500 sm:decoration-2" href="{% url 'edit_purchase' purchase.id %}">
|
||||||
href="{% url 'edit_purchase' purchase.id %}">
|
|
||||||
{{ purchase.edition.name }}
|
{{ purchase.edition.name }}
|
||||||
{% if purchase.type != "game" %}({{ purchase.name }}, {{ purchase.get_type_display }}){% endif %}
|
{% if purchase.type != "game" %}
|
||||||
|
({{ purchase.name }}, {{ purchase.get_type_display }})
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
|
||||||
|
|
|
@ -27,32 +27,37 @@
|
||||||
{% if edition.wikidata %}
|
{% if edition.wikidata %}
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
<a href="https://www.wikidata.org/wiki/{{ edition.wikidata }}">
|
<a href="https://www.wikidata.org/wiki/{{ edition.wikidata }}">
|
||||||
<img class="inline mx-2 w-6" src="{% static 'icons/wikidata.png' %}" />
|
<img class="inline mx-2 w-6" src="{% static 'icons/wikidata.png' %}"/>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% url 'edit_edition' edition.id as edit_url %}
|
{% url 'edit_edition' edition.id as edit_url %}
|
||||||
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
||||||
</li>
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<h1 class="text-3xl mt-4 mb-1">Purchases <span class="dark:text-slate-500">({{ purchases.count }})</span></h1>
|
||||||
<ul>
|
<ul>
|
||||||
{% for purchase in edition.game_purchases %}
|
{% for purchase in purchases %}
|
||||||
<li class="sm:pl-6 flex items-center">
|
<li class="sm:pl-2 flex items-center">
|
||||||
{{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}
|
{{ purchase.platform }}
|
||||||
{% if purchase.price != 0 %}({{ purchase.price }} {{ purchase.price_currency }}){% endif %}
|
({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}})
|
||||||
{% url 'edit_purchase' purchase.id as edit_url %}
|
{% url 'edit_purchase' purchase.id as edit_url %}
|
||||||
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
||||||
</li>
|
{% if purchase.related_purchases %}
|
||||||
|
<li>
|
||||||
<ul>
|
<ul>
|
||||||
{% for related_purchase in purchase.nongame_related_purchases %}
|
{% for related_purchase in purchase.related_purchases %}
|
||||||
<li class="sm:pl-12 flex items-center">
|
<li class="sm:pl-6 flex items-center">
|
||||||
{{ related_purchase.name }} ({{ related_purchase.get_type_display }}, {{ purchase.platform }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency }})
|
{{ related_purchase.name}} ({{ related_purchase.get_type_display }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency}})
|
||||||
{% url 'edit_purchase' related_purchase.id as edit_url %}
|
{% url 'edit_purchase' related_purchase.id as edit_url %}
|
||||||
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endfor %}
|
</li>
|
||||||
</ul>
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center">
|
<h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center">
|
||||||
|
|
|
@ -133,10 +133,23 @@ def edit_game(request, game_id=None):
|
||||||
|
|
||||||
def view_game(request, game_id=None):
|
def view_game(request, game_id=None):
|
||||||
game = Game.objects.get(id=game_id)
|
game = Game.objects.get(id=game_id)
|
||||||
nongame_related_purchases_prefetch = Prefetch(
|
context["title"] = "View Game"
|
||||||
"related_purchases",
|
context["game"] = game
|
||||||
queryset=Purchase.objects.exclude(type=Purchase.GAME),
|
context["editions"] = Edition.objects.filter(game_id=game_id)
|
||||||
to_attr="nongame_related_purchases",
|
game_purchases = Purchase.objects.filter(edition__game_id=game_id).filter(
|
||||||
|
type=Purchase.GAME
|
||||||
|
)
|
||||||
|
for purchase in game_purchases:
|
||||||
|
purchase.related_purchases = Purchase.objects.exclude(
|
||||||
|
type=Purchase.GAME
|
||||||
|
).filter(related_purchase=purchase.id)
|
||||||
|
|
||||||
|
context["purchases"] = game_purchases
|
||||||
|
context["sessions"] = Session.objects.filter(
|
||||||
|
purchase__edition__game_id=game_id
|
||||||
|
).order_by("-timestamp_start")
|
||||||
|
context["total_hours"] = float(
|
||||||
|
format_duration(context["sessions"].total_duration_unformatted(), "%2.1H")
|
||||||
)
|
)
|
||||||
game_purchases_prefetch = Prefetch(
|
game_purchases_prefetch = Prefetch(
|
||||||
"purchase_set",
|
"purchase_set",
|
||||||
|
|
Loading…
Reference in New Issue