Compare commits
1 Commits
main
...
remove_edi
Author | SHA1 | Date |
---|---|---|
Lukáš Kucharczyk | 7111431048 |
|
@ -8,7 +8,6 @@
|
|||
* Add all-time stats
|
||||
* Manage purchases
|
||||
* Automatically convert purchase prices
|
||||
* Add emulated property to sessions
|
||||
|
||||
## Improved
|
||||
* mark refunded purchases red on game overview
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.urls import NoReverseMatch, reverse
|
|||
from django.utils.safestring import SafeText, mark_safe
|
||||
|
||||
from common.utils import truncate
|
||||
from games.models import Game, Purchase, Session
|
||||
from games.models import Purchase
|
||||
|
||||
HTMLAttribute = tuple[str, str | int | bool]
|
||||
HTMLTag = str
|
||||
|
@ -32,7 +32,7 @@ def Component(
|
|||
attributesList = [f'{name}="{value}"' for name, value in attributes]
|
||||
# make attribute list into a string
|
||||
# and insert space between tag and attribute list
|
||||
attributesBlob = f" {' '.join(attributesList)}"
|
||||
attributesBlob = f" {" ".join(attributesList)}"
|
||||
tag: str = ""
|
||||
if tag_name != "":
|
||||
tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>"
|
||||
|
@ -188,28 +188,49 @@ def Icon(
|
|||
return result
|
||||
|
||||
|
||||
def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText:
|
||||
link = reverse("view_game", args=[int(game_id)])
|
||||
a_content = Div(
|
||||
[("class", "inline-flex gap-2 items-center")],
|
||||
[
|
||||
Icon(
|
||||
platform.icon,
|
||||
[("title", platform.name)],
|
||||
),
|
||||
PopoverTruncated(name),
|
||||
],
|
||||
)
|
||||
|
||||
return mark_safe(
|
||||
A(
|
||||
url=link,
|
||||
children=[a_content],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def LinkedPurchase(purchase: Purchase) -> SafeText:
|
||||
link = reverse("view_purchase", args=[int(purchase.id)])
|
||||
link_content = ""
|
||||
popover_content = ""
|
||||
game_count = purchase.games.count()
|
||||
edition_count = purchase.editions.count()
|
||||
popover_if_not_truncated = False
|
||||
if game_count == 1:
|
||||
link_content += purchase.games.first().name
|
||||
if edition_count == 1:
|
||||
link_content += purchase.editions.first().name
|
||||
popover_content = link_content
|
||||
if game_count > 1:
|
||||
if edition_count > 1:
|
||||
if purchase.name:
|
||||
link_content += f"{purchase.name}"
|
||||
popover_content += f"<h1>{purchase.name}</h1><br>"
|
||||
else:
|
||||
link_content += f"{game_count} games"
|
||||
link_content += f"{edition_count} games"
|
||||
popover_if_not_truncated = True
|
||||
popover_content += f"""
|
||||
<ul class="list-disc list-inside">
|
||||
{"".join(f"<li>{game.name}</li>" for game in purchase.games.all())}
|
||||
{"".join(f"<li>{edition.name}</li>" for edition in purchase.editions.all())}
|
||||
</ul>
|
||||
"""
|
||||
icon = purchase.platform.icon if game_count == 1 else "unspecified"
|
||||
icon = purchase.platform.icon if edition_count == 1 else "unspecified"
|
||||
if link_content == "":
|
||||
raise ValueError("link_content is empty!!")
|
||||
a_content = Div(
|
||||
|
@ -229,54 +250,19 @@ def LinkedPurchase(purchase: Purchase) -> SafeText:
|
|||
return mark_safe(A(url=link, children=[a_content]))
|
||||
|
||||
|
||||
def NameWithIcon(
|
||||
name: str = "",
|
||||
platform: str = "",
|
||||
game_id: int = 0,
|
||||
session_id: int = 0,
|
||||
purchase_id: int = 0,
|
||||
linkify: bool = True,
|
||||
emulated: bool = False,
|
||||
) -> SafeText:
|
||||
create_link = False
|
||||
link = ""
|
||||
platform = None
|
||||
if (game_id != 0 or session_id != 0 or purchase_id != 0) and linkify:
|
||||
create_link = True
|
||||
if session_id:
|
||||
session = Session.objects.get(pk=session_id)
|
||||
emulated = session.emulated
|
||||
game_id = session.game.pk
|
||||
if purchase_id:
|
||||
purchase = Purchase.objects.get(pk=purchase_id)
|
||||
game_id = purchase.games.first().pk
|
||||
if game_id:
|
||||
game = Game.objects.get(pk=game_id)
|
||||
name = name or game.name
|
||||
platform = game.platform
|
||||
link = reverse("view_game", args=[int(game_id)])
|
||||
def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
|
||||
content = Div(
|
||||
[("class", "inline-flex gap-2 items-center")],
|
||||
[
|
||||
Icon(
|
||||
platform.icon,
|
||||
[("title", platform.name)],
|
||||
)
|
||||
if platform
|
||||
else "",
|
||||
Icon("emulated", [("title", "Emulated")]) if emulated else "",
|
||||
),
|
||||
PopoverTruncated(name),
|
||||
],
|
||||
)
|
||||
|
||||
return mark_safe(
|
||||
A(
|
||||
url=link,
|
||||
children=[content],
|
||||
)
|
||||
if create_link
|
||||
else content,
|
||||
)
|
||||
return mark_safe(content)
|
||||
|
||||
|
||||
def PurchasePrice(purchase) -> str:
|
||||
|
|
|
@ -44,9 +44,9 @@
|
|||
transition: all 0.2s ease-out;
|
||||
} */
|
||||
|
||||
/* form label {
|
||||
form label {
|
||||
@apply dark:text-slate-400;
|
||||
} */
|
||||
}
|
||||
|
||||
.responsive-table {
|
||||
@apply dark:text-white mx-auto table-fixed;
|
||||
|
@ -90,37 +90,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* form input,
|
||||
form input,
|
||||
select,
|
||||
textarea {
|
||||
@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-800 dark:text-slate-500 cursor-not-allowed;
|
||||
@apply dark:bg-slate-700 dark:text-slate-400;
|
||||
}
|
||||
|
||||
.errorlist {
|
||||
@apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-[300px];
|
||||
}
|
||||
|
||||
/* @media screen and (min-width: 768px) {
|
||||
@media screen and (min-width: 768px) {
|
||||
form input,
|
||||
select,
|
||||
textarea {
|
||||
width: 300px;
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
/* @media screen and (max-width: 768px) {
|
||||
@media screen and (max-width: 768px) {
|
||||
form input,
|
||||
select,
|
||||
textarea {
|
||||
width: 150px;
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
#button-container button {
|
||||
@apply mx-1;
|
||||
|
@ -169,27 +169,3 @@ textarea:disabled {
|
|||
|
||||
}
|
||||
} */
|
||||
|
||||
label {
|
||||
@apply dark:text-slate-500;
|
||||
}
|
||||
|
||||
[type="text"], [type="datetime-local"], [type="datetime"], [type="date"], [type="number"], select, textarea {
|
||||
@apply dark:bg-slate-600 dark:text-slate-300;
|
||||
}
|
||||
|
||||
[type="submit"] {
|
||||
@apply dark:text-white font-bold dark:bg-blue-600 px-4 py-2;
|
||||
}
|
||||
|
||||
form div label {
|
||||
@apply dark:text-white;
|
||||
}
|
||||
|
||||
form div {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
div [type="submit"] {
|
||||
@apply mt-3;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.contrib import admin
|
|||
|
||||
from games.models import (
|
||||
Device,
|
||||
Edition,
|
||||
ExchangeRate,
|
||||
Game,
|
||||
Platform,
|
||||
|
@ -14,5 +15,6 @@ admin.site.register(Game)
|
|||
admin.site.register(Purchase)
|
||||
admin.site.register(Platform)
|
||||
admin.site.register(Session)
|
||||
admin.site.register(Edition)
|
||||
admin.site.register(Device)
|
||||
admin.site.register(ExchangeRate)
|
||||
|
|
|
@ -11,8 +11,6 @@ class GamesConfig(AppConfig):
|
|||
name = "games"
|
||||
|
||||
def ready(self):
|
||||
import games.signals # noqa: F401
|
||||
|
||||
post_migrate.connect(schedule_tasks, sender=self)
|
||||
|
||||
|
||||
|
@ -26,16 +24,6 @@ def schedule_tasks(sender, **kwargs):
|
|||
name="Update converted prices",
|
||||
schedule_type=Schedule.MINUTES,
|
||||
next_run=now() + timedelta(seconds=30),
|
||||
catchup=False,
|
||||
)
|
||||
|
||||
if not Schedule.objects.filter(name="Update price per game").exists():
|
||||
schedule(
|
||||
"games.tasks.calculate_price_per_game",
|
||||
name="Update price per game",
|
||||
schedule_type=Schedule.MINUTES,
|
||||
next_run=now() + timedelta(seconds=30),
|
||||
catchup=False,
|
||||
)
|
||||
|
||||
from games.models import ExchangeRate
|
||||
|
|
|
@ -2,7 +2,7 @@ from django import forms
|
|||
from django.urls import reverse
|
||||
|
||||
from common.utils import safe_getattr
|
||||
from games.models import Device, Game, Platform, Purchase, Session
|
||||
from games.models import Device, Edition, Game, Platform, Purchase, Session
|
||||
|
||||
custom_date_widget = forms.DateInput(attrs={"type": "date"})
|
||||
custom_datetime_widget = forms.DateTimeInput(
|
||||
|
@ -11,25 +11,17 @@ custom_datetime_widget = forms.DateTimeInput(
|
|||
autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"})
|
||||
|
||||
|
||||
class GameChoiceField(forms.ModelChoiceField):
|
||||
def label_from_instance(self, obj) -> str:
|
||||
return f"{obj.sort_name} ({obj.platform}, {obj.year_released})"
|
||||
|
||||
|
||||
class SessionForm(forms.ModelForm):
|
||||
game = GameChoiceField(
|
||||
queryset=Game.objects.order_by("sort_name"),
|
||||
# purchase = forms.ModelChoiceField(
|
||||
# queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name")
|
||||
# )
|
||||
purchase = forms.ModelChoiceField(
|
||||
queryset=Purchase.objects.all(),
|
||||
widget=forms.Select(attrs={"autofocus": "autofocus"}),
|
||||
)
|
||||
|
||||
device = forms.ModelChoiceField(queryset=Device.objects.order_by("name"))
|
||||
|
||||
mark_as_played = forms.BooleanField(
|
||||
required=False,
|
||||
initial={"mark_as_played": True},
|
||||
label="Set game status to Played if Unplayed",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
widgets = {
|
||||
"timestamp_start": custom_datetime_widget,
|
||||
|
@ -37,27 +29,18 @@ class SessionForm(forms.ModelForm):
|
|||
}
|
||||
model = Session
|
||||
fields = [
|
||||
"game",
|
||||
"purchase",
|
||||
"timestamp_start",
|
||||
"timestamp_end",
|
||||
"duration_manual",
|
||||
"emulated",
|
||||
"device",
|
||||
"note",
|
||||
"mark_as_played",
|
||||
]
|
||||
|
||||
def save(self, commit=True):
|
||||
session = super().save(commit=False)
|
||||
if self.cleaned_data.get("mark_as_played"):
|
||||
game_instance = session.game
|
||||
if game_instance.status == "u":
|
||||
game_instance.status = "p"
|
||||
if commit:
|
||||
game_instance.save()
|
||||
if commit:
|
||||
session.save()
|
||||
return session
|
||||
|
||||
class EditionChoiceField(forms.ModelMultipleChoiceField):
|
||||
def label_from_instance(self, obj) -> str:
|
||||
return f"{obj.sort_name} ({obj.platform}, {obj.year_released})"
|
||||
|
||||
|
||||
class IncludePlatformSelect(forms.SelectMultiple):
|
||||
|
@ -73,19 +56,19 @@ class PurchaseForm(forms.ModelForm):
|
|||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Automatically update related_purchase <select/>
|
||||
# to only include purchases of the selected game.
|
||||
related_purchase_by_game_url = reverse("related_purchase_by_game")
|
||||
self.fields["games"].widget.attrs.update(
|
||||
# to only include purchases of the selected edition.
|
||||
related_purchase_by_edition_url = reverse("related_purchase_by_edition")
|
||||
self.fields["editions"].widget.attrs.update(
|
||||
{
|
||||
"hx-trigger": "load, click",
|
||||
"hx-get": related_purchase_by_game_url,
|
||||
"hx-get": related_purchase_by_edition_url,
|
||||
"hx-target": "#id_related_purchase",
|
||||
"hx-swap": "outerHTML",
|
||||
}
|
||||
)
|
||||
|
||||
games = GameChoiceField(
|
||||
queryset=Game.objects.order_by("sort_name"),
|
||||
editions = EditionChoiceField(
|
||||
queryset=Edition.objects.order_by("sort_name"),
|
||||
widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}),
|
||||
)
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
||||
|
@ -103,7 +86,7 @@ class PurchaseForm(forms.ModelForm):
|
|||
}
|
||||
model = Purchase
|
||||
fields = [
|
||||
"games",
|
||||
"editions",
|
||||
"platform",
|
||||
"date_purchased",
|
||||
"date_refunded",
|
||||
|
@ -155,21 +138,24 @@ class GameModelChoiceField(forms.ModelChoiceField):
|
|||
return obj.sort_name
|
||||
|
||||
|
||||
class GameForm(forms.ModelForm):
|
||||
class EditionForm(forms.ModelForm):
|
||||
game = GameModelChoiceField(
|
||||
queryset=Game.objects.order_by("sort_name"),
|
||||
widget=IncludeNameSelect(attrs={"autofocus": "autofocus"}),
|
||||
)
|
||||
platform = forms.ModelChoiceField(
|
||||
queryset=Platform.objects.order_by("name"), required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Edition
|
||||
fields = ["game", "name", "sort_name", "platform", "year_released", "wikidata"]
|
||||
|
||||
|
||||
class GameForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Game
|
||||
fields = [
|
||||
"name",
|
||||
"sort_name",
|
||||
"platform",
|
||||
"year_released",
|
||||
"status",
|
||||
"wikidata",
|
||||
]
|
||||
fields = ["name", "sort_name", "year_released", "wikidata"]
|
||||
widgets = {"name": autofocus_input_widget}
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from .device import Query as DeviceQuery
|
||||
from .edition import Query as EditionQuery
|
||||
from .game import Query as GameQuery
|
||||
from .platform import Query as PlatformQuery
|
||||
from .purchase import Query as PurchaseQuery
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import graphene
|
||||
|
||||
from games.graphql.types import Edition
|
||||
from games.models import Game as EditionModel
|
||||
|
||||
|
||||
class Query(graphene.ObjectType):
|
||||
editions = graphene.List(Edition)
|
||||
|
||||
def resolve_editions(self, info, **kwargs):
|
||||
return EditionModel.objects.all()
|
|
@ -1,6 +1,5 @@
|
|||
# Generated by Django 5.1.5 on 2025-01-29 21:26
|
||||
# Generated by Django 4.1.4 on 2023-01-02 18:27
|
||||
|
||||
import datetime
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
@ -9,96 +8,94 @@ class Migration(migrations.Migration):
|
|||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Device',
|
||||
name="Game",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('type', models.CharField(choices=[('PC', 'PC'), ('Console', 'Console'), ('Handheld', 'Handheld'), ('Mobile', 'Mobile'), ('Single-board computer', 'Single-board computer'), ('Unknown', 'Unknown')], default='Unknown', max_length=255)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("wikidata", models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Platform',
|
||||
name="Platform",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('group', models.CharField(blank=True, default=None, max_length=255, null=True)),
|
||||
('icon', models.SlugField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("group", models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ExchangeRate',
|
||||
name="Purchase",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('currency_from', models.CharField(max_length=255)),
|
||||
('currency_to', models.CharField(max_length=255)),
|
||||
('year', models.PositiveIntegerField()),
|
||||
('rate', models.FloatField()),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('currency_from', 'currency_to', 'year')},
|
||||
},
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("date_purchased", models.DateField()),
|
||||
("date_refunded", models.DateField(blank=True, null=True)),
|
||||
(
|
||||
"game",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="games.game"
|
||||
),
|
||||
),
|
||||
(
|
||||
"platform",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="games.platform",
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Game',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('sort_name', models.CharField(blank=True, default=None, max_length=255, null=True)),
|
||||
('year_released', models.IntegerField(blank=True, default=None, null=True)),
|
||||
('wikidata', models.CharField(blank=True, default=None, max_length=50, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('platform', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='games.platform')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('name', 'platform', 'year_released')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Purchase',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date_purchased', models.DateField()),
|
||||
('date_refunded', models.DateField(blank=True, null=True)),
|
||||
('date_finished', models.DateField(blank=True, null=True)),
|
||||
('date_dropped', models.DateField(blank=True, null=True)),
|
||||
('infinite', models.BooleanField(default=False)),
|
||||
('price', models.FloatField(default=0)),
|
||||
('price_currency', models.CharField(default='USD', max_length=3)),
|
||||
('converted_price', models.FloatField(null=True)),
|
||||
('converted_currency', models.CharField(max_length=3, null=True)),
|
||||
('ownership_type', models.CharField(choices=[('ph', 'Physical'), ('di', 'Digital'), ('du', 'Digital Upgrade'), ('re', 'Rented'), ('bo', 'Borrowed'), ('tr', 'Trial'), ('de', 'Demo'), ('pi', 'Pirated')], default='di', max_length=2)),
|
||||
('type', models.CharField(choices=[('game', 'Game'), ('dlc', 'DLC'), ('season_pass', 'Season Pass'), ('battle_pass', 'Battle Pass')], default='game', max_length=255)),
|
||||
('name', models.CharField(blank=True, default='', max_length=255, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('games', models.ManyToManyField(blank=True, related_name='purchases', to='games.game')),
|
||||
('platform', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='games.platform')),
|
||||
('related_purchase', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='related_purchases', to='games.purchase')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Session',
|
||||
name="Session",
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('timestamp_start', models.DateTimeField()),
|
||||
('timestamp_end', models.DateTimeField(blank=True, null=True)),
|
||||
('duration_manual', models.DurationField(blank=True, default=datetime.timedelta(0), null=True)),
|
||||
('duration_calculated', models.DurationField(blank=True, null=True)),
|
||||
('note', models.TextField(blank=True, null=True)),
|
||||
('emulated', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('modified_at', models.DateTimeField(auto_now=True)),
|
||||
('device', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='games.device')),
|
||||
('game', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to='games.game')),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("timestamp_start", models.DateTimeField()),
|
||||
("timestamp_end", models.DateTimeField()),
|
||||
("duration_manual", models.DurationField(blank=True, null=True)),
|
||||
("duration_calculated", models.DurationField(blank=True, null=True)),
|
||||
("note", models.TextField(blank=True, null=True)),
|
||||
(
|
||||
"purchase",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="games.purchase",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'get_latest_by': 'timestamp_start',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.1.4 on 2023-01-02 18:55
|
||||
|
||||
import datetime
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="duration_manual",
|
||||
field=models.DurationField(
|
||||
blank=True, default=datetime.timedelta(0), null=True
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 5.1.5 on 2025-01-30 11:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('games', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='purchase',
|
||||
name='price_per_game',
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.1.4 on 2023-01-02 23:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0002_alter_session_duration_manual"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="duration_manual",
|
||||
field=models.DurationField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="timestamp_end",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 5.1.5 on 2025-01-30 11:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('games', '0002_purchase_price_per_game'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='purchase',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.1.5 on 2023-01-09 14:49
|
||||
|
||||
import datetime
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0003_alter_session_duration_manual_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="duration_manual",
|
||||
field=models.DurationField(
|
||||
blank=True, default=datetime.timedelta(0), null=True
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,28 +0,0 @@
|
|||
# Generated by Django 5.1.5 on 2025-01-30 11:57
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Count
|
||||
|
||||
|
||||
def initialize_num_purchases(apps, schema_editor):
|
||||
Purchase = apps.get_model("games", "Purchase")
|
||||
purchases = Purchase.objects.annotate(num_games=Count("games"))
|
||||
|
||||
for purchase in purchases:
|
||||
purchase.num_purchases = purchase.num_games
|
||||
purchase.save(update_fields=["num_purchases"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0003_purchase_updated_at"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="num_purchases",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.RunPython(initialize_num_purchases),
|
||||
]
|
|
@ -0,0 +1,35 @@
|
|||
# Generated by Django 4.1.5 on 2023-01-09 17:43
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def set_duration_calculated_none_to_zero(apps, schema_editor):
|
||||
Session = apps.get_model("games", "Session")
|
||||
for session in Session.objects.all():
|
||||
if session.duration_calculated == None:
|
||||
session.duration_calculated = timedelta(0)
|
||||
session.save()
|
||||
|
||||
|
||||
def revert_set_duration_calculated_none_to_zero(apps, schema_editor):
|
||||
Session = apps.get_model("games", "Session")
|
||||
for session in Session.objects.all():
|
||||
if session.duration_calculated == timedelta(0):
|
||||
session.duration_calculated = None
|
||||
session.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0004_alter_session_duration_manual"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
set_duration_calculated_none_to_zero,
|
||||
revert_set_duration_calculated_none_to_zero,
|
||||
)
|
||||
]
|
|
@ -1,38 +0,0 @@
|
|||
# Generated by Django 5.1.5 on 2025-02-01 19:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_finished_status(apps, schema_editor):
|
||||
Game = apps.get_model("games", "Game")
|
||||
Game.objects.filter(purchases__date_finished__isnull=False).update(status="f")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0004_purchase_num_purchases"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="mastered",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("u", "Unplayed"),
|
||||
("p", "Played"),
|
||||
("f", "Finished"),
|
||||
("r", "Retired"),
|
||||
("a", "Abandoned"),
|
||||
],
|
||||
default="u",
|
||||
max_length=1,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(set_finished_status),
|
||||
]
|
|
@ -0,0 +1,35 @@
|
|||
# Generated by Django 4.1.5 on 2023-01-09 18:04
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def set_duration_manual_none_to_zero(apps, schema_editor):
|
||||
Session = apps.get_model("games", "Session")
|
||||
for session in Session.objects.all():
|
||||
if session.duration_manual == None:
|
||||
session.duration_manual = timedelta(0)
|
||||
session.save()
|
||||
|
||||
|
||||
def revert_set_duration_manual_none_to_zero(apps, schema_editor):
|
||||
Session = apps.get_model("games", "Session")
|
||||
for session in Session.objects.all():
|
||||
if session.duration_manual == timedelta(0):
|
||||
session.duration_manual = None
|
||||
session.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0005_auto_20230109_1843"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
set_duration_manual_none_to_zero,
|
||||
revert_set_duration_manual_none_to_zero,
|
||||
)
|
||||
]
|
|
@ -0,0 +1,35 @@
|
|||
# Generated by Django 4.1.5 on 2023-01-19 18:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0006_auto_20230109_1904"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="game",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="games.game"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="platform",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="games.platform"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="purchase",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="games.purchase"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 4.1.5 on 2023-02-18 16:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0007_alter_purchase_game_alter_purchase_platform_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Edition",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
(
|
||||
"game",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="games.game"
|
||||
),
|
||||
),
|
||||
(
|
||||
"platform",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="games.platform"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 4.1.5 on 2023-02-18 18:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def create_edition_of_game(apps, schema_editor):
|
||||
Game = apps.get_model("games", "Game")
|
||||
Edition = apps.get_model("games", "Edition")
|
||||
Platform = apps.get_model("games", "Platform")
|
||||
first_platform = Platform.objects.first()
|
||||
all_games = Game.objects.all()
|
||||
all_editions = Edition.objects.all()
|
||||
for game in all_games:
|
||||
existing_edition = None
|
||||
try:
|
||||
existing_edition = all_editions.objects.get(game=game.id)
|
||||
except:
|
||||
pass
|
||||
if existing_edition == None:
|
||||
edition = Edition()
|
||||
edition.id = game.id
|
||||
edition.game = game
|
||||
edition.name = game.name
|
||||
edition.platform = first_platform
|
||||
edition.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0008_edition"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(create_edition_of_game)]
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 4.1.5 on 2023-02-18 19:06
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0009_create_editions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="game",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="games.edition"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1.5 on 2023-02-18 19:18
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0010_alter_purchase_game"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="purchase",
|
||||
old_name="game",
|
||||
new_name="edition",
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.1.5 on 2023-02-18 19:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0011_rename_game_purchase_edition"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="price",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="price_currency",
|
||||
field=models.CharField(default="USD", max_length=3),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,31 @@
|
|||
# Generated by Django 4.1.5 on 2023-02-18 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0012_purchase_price_purchase_price_currency"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="ownership_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("ph", "Physical"),
|
||||
("di", "Digital"),
|
||||
("du", "Digital Upgrade"),
|
||||
("re", "Rented"),
|
||||
("bo", "Borrowed"),
|
||||
("tr", "Trial"),
|
||||
("de", "Demo"),
|
||||
("pi", "Pirated"),
|
||||
],
|
||||
default="di",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,52 @@
|
|||
# Generated by Django 4.1.5 on 2023-02-18 19:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0013_purchase_ownership_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Device",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("pc", "PC"),
|
||||
("co", "Console"),
|
||||
("ha", "Handheld"),
|
||||
("mo", "Mobile"),
|
||||
("sbc", "Single-board computer"),
|
||||
],
|
||||
default="pc",
|
||||
max_length=3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="session",
|
||||
name="device",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="games.device",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.1.5 on 2023-02-20 14:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0014_device_session_device"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="edition",
|
||||
name="wikidata",
|
||||
field=models.CharField(blank=True, default=None, max_length=50, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="edition",
|
||||
name="year_released",
|
||||
field=models.IntegerField(default=2023),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,51 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-06 11:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0015_edition_wikidata_edition_year_released"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="edition",
|
||||
name="platform",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="games.platform",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="edition",
|
||||
name="year_released",
|
||||
field=models.IntegerField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="game",
|
||||
name="wikidata",
|
||||
field=models.CharField(blank=True, default=None, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="platform",
|
||||
name="group",
|
||||
field=models.CharField(blank=True, default=None, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="device",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="games.device",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,141 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-06 18:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def rename_duplicates(apps, schema_editor):
|
||||
Edition = apps.get_model("games", "Edition")
|
||||
|
||||
duplicates = (
|
||||
Edition.objects.values("name", "platform")
|
||||
.annotate(name_count=models.Count("id"))
|
||||
.filter(name_count__gt=1)
|
||||
)
|
||||
|
||||
for duplicate in duplicates:
|
||||
counter = 1
|
||||
duplicate_editions = Edition.objects.filter(
|
||||
name=duplicate["name"], platform_id=duplicate["platform"]
|
||||
).order_by("id")
|
||||
|
||||
for edition in duplicate_editions[1:]: # Skip the first one
|
||||
edition.name = f"{edition.name} {counter}"
|
||||
edition.save()
|
||||
counter += 1
|
||||
|
||||
|
||||
def update_game_year(apps, schema_editor):
|
||||
Game = apps.get_model("games", "Game")
|
||||
Edition = apps.get_model("games", "Edition")
|
||||
|
||||
for game in Game.objects.filter(year__isnull=True):
|
||||
# Try to get the first related edition with a non-null year_released
|
||||
edition = Edition.objects.filter(game=game, year_released__isnull=False).first()
|
||||
if edition:
|
||||
# If an edition is found, update the game's year
|
||||
game.year = edition.year_released
|
||||
game.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
replaces = [
|
||||
("games", "0016_alter_edition_platform_alter_edition_year_released_and_more"),
|
||||
("games", "0017_alter_device_type_alter_purchase_platform"),
|
||||
("games", "0018_auto_20231106_1825"),
|
||||
("games", "0019_alter_edition_unique_together"),
|
||||
("games", "0020_game_year"),
|
||||
("games", "0021_auto_20231106_1909"),
|
||||
("games", "0022_rename_year_game_year_released"),
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
("games", "0015_edition_wikidata_edition_year_released"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="edition",
|
||||
name="platform",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="games.platform",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="edition",
|
||||
name="year_released",
|
||||
field=models.IntegerField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="game",
|
||||
name="wikidata",
|
||||
field=models.CharField(blank=True, default=None, max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="platform",
|
||||
name="group",
|
||||
field=models.CharField(blank=True, default=None, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="device",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="games.device",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="device",
|
||||
name="type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("pc", "PC"),
|
||||
("co", "Console"),
|
||||
("ha", "Handheld"),
|
||||
("mo", "Mobile"),
|
||||
("sbc", "Single-board computer"),
|
||||
("un", "Unknown"),
|
||||
],
|
||||
default="un",
|
||||
max_length=3,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="platform",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="games.platform",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rename_duplicates,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="edition",
|
||||
unique_together={("name", "platform")},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="year",
|
||||
field=models.IntegerField(blank=True, default=None, null=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=update_game_year,
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="game",
|
||||
old_name="year",
|
||||
new_name="year_released",
|
||||
),
|
||||
]
|
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-06 16:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0016_alter_edition_platform_alter_edition_year_released_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="device",
|
||||
name="type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("pc", "PC"),
|
||||
("co", "Console"),
|
||||
("ha", "Handheld"),
|
||||
("mo", "Mobile"),
|
||||
("sbc", "Single-board computer"),
|
||||
("un", "Unknown"),
|
||||
],
|
||||
default="un",
|
||||
max_length=3,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="platform",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="games.platform",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-06 17:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def rename_duplicates(apps, schema_editor):
|
||||
Edition = apps.get_model("games", "Edition")
|
||||
|
||||
duplicates = (
|
||||
Edition.objects.values("name", "platform")
|
||||
.annotate(name_count=models.Count("id"))
|
||||
.filter(name_count__gt=1)
|
||||
)
|
||||
|
||||
for duplicate in duplicates:
|
||||
counter = 1
|
||||
duplicate_editions = Edition.objects.filter(
|
||||
name=duplicate["name"], platform_id=duplicate["platform"]
|
||||
).order_by("id")
|
||||
|
||||
for edition in duplicate_editions[1:]: # Skip the first one
|
||||
edition.name = f"{edition.name} {counter}"
|
||||
edition.save()
|
||||
counter += 1
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0017_alter_device_type_alter_purchase_platform"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(rename_duplicates),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-06 17:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0018_auto_20231106_1825"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="edition",
|
||||
unique_together={("name", "platform")},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-06 18:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0019_alter_edition_unique_together"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="year",
|
||||
field=models.IntegerField(blank=True, default=None, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
def update_game_year(apps, schema_editor):
|
||||
Game = apps.get_model("games", "Game")
|
||||
Edition = apps.get_model("games", "Edition")
|
||||
|
||||
for game in Game.objects.filter(year__isnull=True):
|
||||
# Try to get the first related edition with a non-null year_released
|
||||
edition = Edition.objects.filter(game=game, year_released__isnull=False).first()
|
||||
if edition:
|
||||
# If an edition is found, update the game's year
|
||||
game.year = edition.year_released
|
||||
game.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0020_game_year"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_game_year),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-06 18:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0021_auto_20231106_1909"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="game",
|
||||
old_name="year",
|
||||
new_name="year_released",
|
||||
),
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-06 18:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"games",
|
||||
"0016_alter_edition_platform_alter_edition_year_released_and_more_squashed_0022_rename_year_game_year_released",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="date_finished",
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-09 09:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def create_sort_name(apps, schema_editor):
|
||||
Edition = apps.get_model(
|
||||
"games", "Edition"
|
||||
) # Replace 'your_app_name' with the actual name of your app
|
||||
|
||||
for edition in Edition.objects.all():
|
||||
name = edition.name
|
||||
# Check for articles at the beginning of the name and move them to the end
|
||||
if name.lower().startswith("the "):
|
||||
sort_name = f"{name[4:]}, The"
|
||||
elif name.lower().startswith("a "):
|
||||
sort_name = f"{name[2:]}, A"
|
||||
elif name.lower().startswith("an "):
|
||||
sort_name = f"{name[3:]}, An"
|
||||
else:
|
||||
sort_name = name
|
||||
# Save the sort_name back to the database
|
||||
edition.sort_name = sort_name
|
||||
edition.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0023_purchase_date_finished"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="edition",
|
||||
name="sort_name",
|
||||
field=models.CharField(blank=True, default=None, max_length=255, null=True),
|
||||
),
|
||||
migrations.RunPython(create_sort_name),
|
||||
]
|
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-09 09:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def create_sort_name(apps, schema_editor):
|
||||
Game = apps.get_model(
|
||||
"games", "Game"
|
||||
) # Replace 'your_app_name' with the actual name of your app
|
||||
|
||||
for game in Game.objects.all():
|
||||
name = game.name
|
||||
# Check for articles at the beginning of the name and move them to the end
|
||||
if name.lower().startswith("the "):
|
||||
sort_name = f"{name[4:]}, The"
|
||||
elif name.lower().startswith("a "):
|
||||
sort_name = f"{name[2:]}, A"
|
||||
elif name.lower().startswith("an "):
|
||||
sort_name = f"{name[3:]}, An"
|
||||
else:
|
||||
sort_name = name
|
||||
# Save the sort_name back to the database
|
||||
game.sort_name = sort_name
|
||||
game.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0024_edition_sort_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="sort_name",
|
||||
field=models.CharField(blank=True, default=None, max_length=255, null=True),
|
||||
),
|
||||
migrations.RunPython(create_sort_name),
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-14 08:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0025_game_sort_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("game", "Game"),
|
||||
("dlc", "DLC"),
|
||||
("season_pass", "Season Pass"),
|
||||
("battle_pass", "Battle Pass"),
|
||||
],
|
||||
default="game",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-14 08:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0026_purchase_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="related_purchase",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="games.purchase",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-14 11:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
from games.models import Purchase
|
||||
|
||||
|
||||
def null_game_name(apps, schema_editor):
|
||||
Purchase.objects.filter(type=Purchase.GAME).update(name=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0027_purchase_related_purchase"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="name",
|
||||
field=models.CharField(
|
||||
blank=True, default="Unknown Name", max_length=255, null=True
|
||||
),
|
||||
),
|
||||
migrations.RunPython(null_game_name),
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-14 21:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0028_purchase_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="related_purchase",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="related_purchases",
|
||||
to="games.purchase",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-15 12:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0029_alter_purchase_related_purchase"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="name",
|
||||
field=models.CharField(blank=True, default="", max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,44 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-15 13:51
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0030_alter_purchase_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="device",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="edition",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="platform",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="session",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,52 @@
|
|||
# Generated by Django 4.1.5 on 2023-11-15 18:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0031_device_created_at_edition_created_at_game_created_at_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="session",
|
||||
options={"get_latest_by": "timestamp_start"},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="session",
|
||||
name="modified_at",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="device",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="edition",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="game",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="platform",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="created_at",
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.2.7 on 2023-11-28 13:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0032_alter_session_options_session_modified_at_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="edition",
|
||||
unique_together={("name", "platform", "year_released")},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.2.7 on 2024-01-03 21:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0033_alter_edition_unique_together"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="date_dropped",
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="infinite",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 5.1 on 2024-08-11 15:50
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0034_purchase_date_dropped_purchase_infinite"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="device",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="games.device",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 5.1 on 2024-08-11 16:48
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('games', '0035_alter_session_device'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='edition',
|
||||
name='platform',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='games.platform'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 5.1.1 on 2024-09-14 07:05
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.utils.text import slugify
|
||||
|
||||
|
||||
def update_empty_icons(apps, schema_editor):
|
||||
Platform = apps.get_model("games", "Platform")
|
||||
for platform in Platform.objects.filter(icon=""):
|
||||
platform.icon = slugify(platform.name)
|
||||
platform.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0036_alter_edition_platform"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="platform",
|
||||
name="icon",
|
||||
field=models.SlugField(blank=True),
|
||||
),
|
||||
migrations.RunPython(update_empty_icons),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.1.1 on 2024-10-04 09:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('games', '0037_platform_icon'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='purchase',
|
||||
name='price',
|
||||
field=models.FloatField(default=0),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.1.2 on 2024-11-09 22:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('games', '0038_alter_purchase_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='device',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('PC', 'PC'), ('Console', 'Console'), ('Handheld', 'Handheld'), ('Mobile', 'Mobile'), ('Single-board computer', 'Single-board computer'), ('Unknown', 'Unknown')], default='Unknown', max_length=255),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 5.1.2 on 2024-11-09 22:39
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_device_types(apps, schema_editor):
|
||||
Device = apps.get_model("games", "Device")
|
||||
|
||||
# Mapping of short names to long names
|
||||
type_map = {
|
||||
"pc": "PC",
|
||||
"co": "Console",
|
||||
"ha": "Handheld",
|
||||
"mo": "Mobile",
|
||||
"sbc": "Single-board computer",
|
||||
"un": "Unknown",
|
||||
}
|
||||
|
||||
# Loop through all devices and update the type field
|
||||
for device in Device.objects.all():
|
||||
if device.type in type_map:
|
||||
device.type = type_map[device.type]
|
||||
device.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0039_alter_device_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_device_types),
|
||||
]
|
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 5.1.3 on 2024-11-10 15:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('games', '0040_migrate_device_types'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='purchase',
|
||||
name='converted_currency',
|
||||
field=models.CharField(max_length=3, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='purchase',
|
||||
name='converted_price',
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ExchangeRate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('currency_from', models.CharField(max_length=255)),
|
||||
('currency_to', models.CharField(max_length=255)),
|
||||
('year', models.PositiveIntegerField()),
|
||||
('rate', models.FloatField()),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('currency_from', 'currency_to', 'year')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.1.3 on 2025-01-07 20:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('games', '0041_purchase_converted_currency_purchase_converted_price_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='purchase',
|
||||
name='editions_temp',
|
||||
field=models.ManyToManyField(blank=True, related_name='temp_purchases', to='games.edition'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.1.3 on 2025-01-07 20:17
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_edition_to_editions_temp(apps, schema_editor):
|
||||
Purchase = apps.get_model("games", "Purchase")
|
||||
for purchase in Purchase.objects.all():
|
||||
if purchase.edition:
|
||||
purchase.editions_temp.add(purchase.edition)
|
||||
purchase.save()
|
||||
else:
|
||||
print(f"No edition found for Purchase {purchase.id}")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0042_purchase_editions_temp"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_edition_to_editions_temp),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.1.3 on 2025-01-07 20:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0043_auto_20250107_2117"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(model_name="purchase", name="edition"),
|
||||
migrations.RenameField(
|
||||
model_name="purchase",
|
||||
old_name="editions_temp",
|
||||
new_name="editions",
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.1.3 on 2025-01-07 20:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('games', '0044_auto_20250107_2132'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='purchase',
|
||||
name='editions',
|
||||
field=models.ManyToManyField(blank=True, related_name='purchases', to='games.edition'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 5.1.3 on 2025-01-08 20:06
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0045_alter_purchase_editions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="game",
|
||||
name="platform",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="games.platform",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="game",
|
||||
unique_together={("name", "platform", "year_released")},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,61 @@
|
|||
# Generated by Django 5.1.3 on 2025-01-19 20:29
|
||||
|
||||
from django.db import connection, migrations, models
|
||||
|
||||
|
||||
def recreate_games(apps, schema_editor):
|
||||
Edition = apps.get_model("games", "Edition")
|
||||
Game = apps.get_model("games", "Game")
|
||||
Purchase = apps.get_model("games", "Purchase")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
print("Create table games_gametemp")
|
||||
cursor.execute(
|
||||
"CREATE TABLE games_gametemp AS SELECT * FROM games_game WHERE 1=0;"
|
||||
)
|
||||
|
||||
for edition in Edition.objects.all():
|
||||
print(f"Re-create edition with ID {edition.id}")
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO games_gametemp (
|
||||
id, name, sort_name, year_released, platform_id, wikidata, created_at
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
[
|
||||
edition.id, # Reuse the Edition ID
|
||||
edition.name,
|
||||
edition.sort_name,
|
||||
edition.year_released,
|
||||
edition.platform_id,
|
||||
Game.objects.get(id=edition.game.id).wikidata,
|
||||
edition.created_at,
|
||||
],
|
||||
)
|
||||
print("Turn foreign keys off")
|
||||
cursor.execute("PRAGMA foreign_keys = OFF;")
|
||||
print("Drop table games_game")
|
||||
cursor.execute("DROP TABLE games_game;")
|
||||
print("Drop table games_edition")
|
||||
cursor.execute("DROP TABLE games_edition;")
|
||||
print("Rename table games_gametemp to games_game")
|
||||
# cursor.execute("ALTER TABLE games_gametemp RENAME TO games_game;")
|
||||
cursor.execute("CREATE TABLE games_game AS SELECT * FROM games_gametemp;")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("games", "0046_game_platform_alter_game_unique_together"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(recreate_games),
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="editions",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="purchases", to="games.game"
|
||||
),
|
||||
),
|
||||
]
|
168
games/models.py
168
games/models.py
|
@ -3,69 +3,12 @@ from datetime import timedelta
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Sum
|
||||
from django.template.defaultfilters import floatformat, pluralize, slugify
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils import timezone
|
||||
|
||||
from common.time import format_duration
|
||||
|
||||
|
||||
class Game(models.Model):
|
||||
class Meta:
|
||||
unique_together = [["name", "platform", "year_released"]]
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
|
||||
year_released = models.IntegerField(null=True, blank=True, default=None)
|
||||
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||
platform = models.ForeignKey(
|
||||
"Platform", on_delete=models.SET_DEFAULT, null=True, blank=True, default=None
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Status(models.TextChoices):
|
||||
UNPLAYED = (
|
||||
"u",
|
||||
"Unplayed",
|
||||
)
|
||||
PLAYED = (
|
||||
"p",
|
||||
"Played",
|
||||
)
|
||||
FINISHED = (
|
||||
"f",
|
||||
"Finished",
|
||||
)
|
||||
RETIRED = (
|
||||
"r",
|
||||
"Retired",
|
||||
)
|
||||
ABANDONED = (
|
||||
"a",
|
||||
"Abandoned",
|
||||
)
|
||||
|
||||
status = models.CharField(max_length=1, choices=Status, default=Status.UNPLAYED)
|
||||
mastered = models.BooleanField(default=False)
|
||||
|
||||
session_average: float | int | timedelta | None
|
||||
session_count: int | None
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.platform is None:
|
||||
self.platform = get_sentinel_platform()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
def get_sentinel_platform():
|
||||
return Platform.objects.get_or_create(
|
||||
name="Unspecified", icon="unspecified", group="Unspecified"
|
||||
)[0]
|
||||
|
||||
|
||||
class Platform(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
group = models.CharField(max_length=255, null=True, blank=True, default=None)
|
||||
|
@ -81,6 +24,55 @@ class Platform(models.Model):
|
|||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
def get_sentinel_platform():
|
||||
return Platform.objects.get_or_create(
|
||||
name="Unspecified", icon="unspecified", group="Unspecified"
|
||||
)[0]
|
||||
|
||||
|
||||
class Game(models.Model):
|
||||
class Meta:
|
||||
unique_together = [["name", "platform", "year_released"]]
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
|
||||
year_released = models.IntegerField(null=True, blank=True, default=None)
|
||||
platform = models.ForeignKey(
|
||||
Platform, on_delete=models.SET_DEFAULT, null=True, blank=True, default=None
|
||||
)
|
||||
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
session_average: float | int | timedelta | None
|
||||
session_count: int | None
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Edition(models.Model):
|
||||
class Meta:
|
||||
unique_together = [["name", "platform", "year_released"]]
|
||||
|
||||
game = models.ForeignKey(Game, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
|
||||
platform = models.ForeignKey(
|
||||
Platform, on_delete=models.SET_DEFAULT, null=True, blank=True, default=None
|
||||
)
|
||||
year_released = models.IntegerField(null=True, blank=True, default=None)
|
||||
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.sort_name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.platform is None:
|
||||
self.platform = get_sentinel_platform()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class PurchaseQueryset(models.QuerySet):
|
||||
def refunded(self):
|
||||
return self.filter(date_refunded__isnull=False)
|
||||
|
@ -127,8 +119,7 @@ class Purchase(models.Model):
|
|||
|
||||
objects = PurchaseQueryset().as_manager()
|
||||
|
||||
games = models.ManyToManyField(Game, related_name="purchases", blank=True)
|
||||
|
||||
editions = models.ManyToManyField(Game, related_name="purchases", blank=True)
|
||||
platform = models.ForeignKey(
|
||||
Platform, on_delete=models.CASCADE, default=None, null=True, blank=True
|
||||
)
|
||||
|
@ -141,8 +132,6 @@ class Purchase(models.Model):
|
|||
price_currency = models.CharField(max_length=3, default="USD")
|
||||
converted_price = models.FloatField(null=True)
|
||||
converted_currency = models.CharField(max_length=3, null=True)
|
||||
price_per_game = models.FloatField(null=True)
|
||||
num_purchases = models.IntegerField(default=0)
|
||||
ownership_type = models.CharField(
|
||||
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
|
||||
)
|
||||
|
@ -157,43 +146,23 @@ class Purchase(models.Model):
|
|||
related_name="related_purchases",
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@property
|
||||
def standardized_price(self):
|
||||
return (
|
||||
f"{floatformat(self.converted_price, 0)} {self.converted_currency}"
|
||||
if self.converted_price
|
||||
else None
|
||||
)
|
||||
|
||||
@property
|
||||
def has_one_item(self):
|
||||
return self.games.count() == 1
|
||||
|
||||
@property
|
||||
def standardized_name(self):
|
||||
return self.name or self.first_game.name
|
||||
|
||||
@property
|
||||
def first_game(self):
|
||||
return self.games.first()
|
||||
def first_edition(self):
|
||||
return self.editions.first()
|
||||
|
||||
def __str__(self):
|
||||
return self.standardized_name
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
additional_info = [
|
||||
str(item)
|
||||
for item in [
|
||||
f"{self.num_purchases} game{pluralize(self.num_purchases)}",
|
||||
self.date_purchased,
|
||||
self.standardized_price,
|
||||
self.get_type_display() if self.type != Purchase.GAME else "",
|
||||
(
|
||||
f"{self.first_edition.platform} version on {self.platform}"
|
||||
if self.platform != self.first_edition.platform
|
||||
else self.platform
|
||||
),
|
||||
self.first_edition.year_released,
|
||||
self.get_ownership_type_display(),
|
||||
]
|
||||
if item
|
||||
]
|
||||
return f"{self.standardized_name} ({', '.join(additional_info)})"
|
||||
return f"{self.first_edition} ({', '.join(filter(None, map(str, additional_info)))})"
|
||||
|
||||
def is_game(self):
|
||||
return self.type == self.GAME
|
||||
|
@ -244,14 +213,7 @@ class Session(models.Model):
|
|||
class Meta:
|
||||
get_latest_by = "timestamp_start"
|
||||
|
||||
game = models.ForeignKey(
|
||||
Game,
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
related_name="sessions",
|
||||
)
|
||||
purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE)
|
||||
timestamp_start = models.DateTimeField()
|
||||
timestamp_end = models.DateTimeField(blank=True, null=True)
|
||||
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0))
|
||||
|
@ -264,8 +226,6 @@ class Session(models.Model):
|
|||
default=None,
|
||||
)
|
||||
note = models.TextField(blank=True, null=True)
|
||||
emulated = models.BooleanField(default=False)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
modified_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
@ -273,7 +233,7 @@ class Session(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
mark = ", manual" if self.is_manual() else ""
|
||||
return f"{str(self.game)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
|
||||
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
|
||||
|
||||
def finish_now(self):
|
||||
self.timestamp_end = timezone.now()
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
from django.db.models.signals import m2m_changed
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
|
||||
from games.models import Purchase
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Purchase.games.through)
|
||||
def update_num_purchases(sender, instance, **kwargs):
|
||||
instance.num_purchases = instance.games.count()
|
||||
instance.updated_at = now()
|
||||
instance.save(update_fields=["num_purchases"])
|
|
@ -1299,10 +1299,6 @@ input:checked + .toggle-bg {
|
|||
bottom: 0px;
|
||||
}
|
||||
|
||||
.-left-3 {
|
||||
left: -0.75rem;
|
||||
}
|
||||
|
||||
.bottom-0 {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
@ -1339,10 +1335,6 @@ input:checked + .toggle-bg {
|
|||
top: 0px;
|
||||
}
|
||||
|
||||
.top-2 {
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.top-3 {
|
||||
top: 0.75rem;
|
||||
}
|
||||
|
@ -1423,10 +1415,6 @@ input:checked + .toggle-bg {
|
|||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
|
||||
.ml-3 {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
@ -1500,10 +1488,6 @@ input:checked + .toggle-bg {
|
|||
height: 3rem;
|
||||
}
|
||||
|
||||
.h-2 {
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
.h-2\.5 {
|
||||
height: 0.625rem;
|
||||
}
|
||||
|
@ -1540,10 +1524,6 @@ input:checked + .toggle-bg {
|
|||
width: 2.5rem;
|
||||
}
|
||||
|
||||
.w-2 {
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
.w-2\.5 {
|
||||
width: 0.625rem;
|
||||
}
|
||||
|
@ -1600,14 +1580,6 @@ input:checked + .toggle-bg {
|
|||
max-width: 20rem;
|
||||
}
|
||||
|
||||
.max-w-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.max-w-xl {
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
@ -1815,10 +1787,6 @@ input:checked + .toggle-bg {
|
|||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.rounded-e-lg {
|
||||
border-start-end-radius: 0.5rem;
|
||||
border-end-end-radius: 0.5rem;
|
||||
|
@ -1912,11 +1880,6 @@ input:checked + .toggle-bg {
|
|||
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-800 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
|
@ -1926,11 +1889,6 @@ input:checked + .toggle-bg {
|
|||
background-color: rgb(17 24 39 / 0.5);
|
||||
}
|
||||
|
||||
.bg-green-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(14 159 110 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-green-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(5 122 85 / var(--tw-bg-opacity));
|
||||
|
@ -1941,21 +1899,6 @@ input:checked + .toggle-bg {
|
|||
background-color: rgb(4 108 78 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-orange-400 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 138 76 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-purple-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(144 97 249 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(240 82 82 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-red-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(200 30 30 / var(--tw-bg-opacity));
|
||||
|
@ -2234,11 +2177,6 @@ input:checked + .toggle-bg {
|
|||
color: rgb(203 213 225 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-slate-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-slate-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(100 116 139 / var(--tw-text-opacity));
|
||||
|
@ -2411,9 +2349,10 @@ input:checked + .toggle-bg {
|
|||
transition: all 0.2s ease-out;
|
||||
} */
|
||||
|
||||
/* form label {
|
||||
@apply dark:text-slate-400;
|
||||
} */
|
||||
form label:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.responsive-table {
|
||||
margin-left: auto;
|
||||
|
@ -2452,25 +2391,25 @@ input:checked + .toggle-bg {
|
|||
border-left-color: rgb(100 116 139 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
/* form input,
|
||||
select,
|
||||
textarea {
|
||||
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
||||
} */
|
||||
|
||||
form input:disabled,
|
||||
select:disabled,
|
||||
textarea:disabled {
|
||||
cursor: not-allowed;
|
||||
form input:is(.dark *),
|
||||
select:is(.dark *),
|
||||
textarea:is(.dark *) {
|
||||
border-width: 1px;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(15 23 42 / var(--tw-border-opacity));
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(100 116 139 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(241 245 249 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
form input:disabled:is(.dark *),
|
||||
select:disabled:is(.dark *),
|
||||
textarea:disabled:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 41 59 / var(--tw-bg-opacity));
|
||||
background-color: rgb(51 65 85 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(100 116 139 / var(--tw-text-opacity));
|
||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.errorlist {
|
||||
|
@ -2486,21 +2425,21 @@ textarea:disabled:is(.dark *) {
|
|||
color: rgb(226 232 240 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
/* @media screen and (min-width: 768px) {
|
||||
@media screen and (min-width: 768px) {
|
||||
form input,
|
||||
select,
|
||||
textarea {
|
||||
width: 300px;
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
/* @media screen and (max-width: 768px) {
|
||||
@media screen and (max-width: 768px) {
|
||||
form input,
|
||||
select,
|
||||
textarea {
|
||||
width: 150px;
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
#button-container button {
|
||||
margin-left: 0.25rem;
|
||||
|
@ -2609,47 +2548,6 @@ textarea:disabled:is(.dark *) {
|
|||
}
|
||||
} */
|
||||
|
||||
label:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(100 116 139 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
[type="text"]:is(.dark *), [type="datetime-local"]:is(.dark *), [type="datetime"]:is(.dark *), [type="date"]:is(.dark *), [type="number"]:is(.dark *), select:is(.dark *), textarea:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(71 85 105 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(203 213 225 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
[type="submit"] {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
[type="submit"]:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(28 100 242 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
form div label:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
form div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
div [type="submit"] {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.odd\:bg-white:nth-child(odd) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
|
||||
let syncData = [
|
||||
{
|
||||
source: "#id_games",
|
||||
source: "#id_edition",
|
||||
source_value: "dataset.platform",
|
||||
target: "#id_platform",
|
||||
target_value: "value",
|
||||
|
@ -36,8 +36,8 @@ getEl("#id_type").onchange = () => {
|
|||
|
||||
document.body.addEventListener("htmx:beforeRequest", function (event) {
|
||||
// Assuming 'Purchase1' is the element that triggers the HTMX request
|
||||
if (event.target.id === "id_games") {
|
||||
var idEditionValue = document.getElementById("id_games").value;
|
||||
if (event.target.id === "id_edition") {
|
||||
var idEditionValue = document.getElementById("id_edition").value;
|
||||
|
||||
// Condition to check - replace this with your actual logic
|
||||
if (idEditionValue != "") {
|
||||
|
|
|
@ -36,7 +36,7 @@ function addToggleButton(targetNode) {
|
|||
targetNode.parentElement.appendChild(manualToggleButton);
|
||||
}
|
||||
|
||||
const toggleableFields = ["#id_games", "#id_platform"];
|
||||
const toggleableFields = ["#id_game", "#id_edition", "#id_platform"];
|
||||
|
||||
toggleableFields.map((selector) => {
|
||||
addToggleButton(document.querySelector(selector));
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import requests
|
||||
from django.db.models import ExpressionWrapper, F, FloatField, Q
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.utils.timezone import now
|
||||
from django_q.models import Task
|
||||
|
||||
from games.models import ExchangeRate, Purchase
|
||||
|
||||
|
@ -36,53 +32,26 @@ def convert_prices():
|
|||
).first()
|
||||
|
||||
if not exchange_rate:
|
||||
print(
|
||||
f"Getting exchange rate from {currency_from} to {currency_to} for {year}..."
|
||||
)
|
||||
try:
|
||||
# this API endpoint only accepts lowercase currency string
|
||||
response = requests.get(
|
||||
f"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@{year}-01-01/v1/currencies/{currency_from.lower()}.json"
|
||||
f"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@{year}-01-01/v1/currencies/{currency_from}.json"
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
currency_from_data = data.get(currency_from.lower())
|
||||
rate = currency_from_data.get(currency_to.lower())
|
||||
rate = data[currency_from].get(currency_to)
|
||||
|
||||
if rate:
|
||||
print(f"Got {rate}, saving...")
|
||||
exchange_rate = ExchangeRate.objects.create(
|
||||
currency_from=currency_from,
|
||||
currency_to=currency_to,
|
||||
year=year,
|
||||
rate=floatformat(rate, 2),
|
||||
rate=rate,
|
||||
)
|
||||
else:
|
||||
print("Could not get an exchange rate.")
|
||||
except requests.RequestException as e:
|
||||
print(
|
||||
f"Failed to fetch exchange rate for {currency_from}->{currency_to} in {year}: {e}"
|
||||
)
|
||||
if exchange_rate:
|
||||
save_converted_info(
|
||||
purchase,
|
||||
floatformat(purchase.price * exchange_rate.rate, 0),
|
||||
currency_to,
|
||||
)
|
||||
|
||||
|
||||
def calculate_price_per_game():
|
||||
try:
|
||||
last_task = Task.objects.filter(group="Update price per game").first()
|
||||
last_run = last_task.started
|
||||
except Task.DoesNotExist or AttributeError:
|
||||
last_run = now()
|
||||
purchases = Purchase.objects.filter(converted_price__isnull=False).filter(
|
||||
Q(updated_at__gte=last_run) | Q(price_per_game__isnull=True)
|
||||
)
|
||||
print(f"Updating {purchases.count()} purchases.")
|
||||
purchases.update(
|
||||
price_per_game=ExpressionWrapper(
|
||||
F("converted_price") / F("num_purchases"), output_field=FloatField()
|
||||
)
|
||||
purchase, purchase.price * exchange_rate.rate, currency_to
|
||||
)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<c-layouts.add>
|
||||
<c-slot name="additional_row">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input type="submit"
|
||||
name="submit_and_redirect"
|
||||
value="Submit & Create Purchase" />
|
||||
</td>
|
||||
</tr>
|
||||
</c-slot>
|
||||
</c-layouts.add>
|
|
@ -1,7 +1,12 @@
|
|||
<c-layouts.add>
|
||||
<c-slot name="additional_row">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input type="submit"
|
||||
name="submit_and_redirect"
|
||||
value="Submit & Create Purchase" />
|
||||
value="Submit & Create Edition" />
|
||||
</td>
|
||||
</tr>
|
||||
</c-slot>
|
||||
</c-layouts.add>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<div class="relative ml-3">
|
||||
<span class="rounded-xl w-2 h-2 absolute -left-3 top-2
|
||||
{% if status == "u" %}
|
||||
bg-gray-500
|
||||
{% elif status == "p" %}
|
||||
bg-orange-400
|
||||
{% elif status == "f" %}
|
||||
bg-green-500
|
||||
{% elif status == "a" %}
|
||||
bg-red-500
|
||||
{% elif status == "r" %}
|
||||
bg-purple-500
|
||||
{% endif %}
|
||||
"> </span>
|
||||
{{ slot }}
|
||||
</div>
|
|
@ -1,6 +0,0 @@
|
|||
<c-vars title="Emulated" />
|
||||
<c-svg :title=title viewbox="0 0 48 48">
|
||||
<c-slot name="path">
|
||||
M 8.5 5 C 6.0324991 5 4 7.0324991 4 9.5 L 4 30.5 C 4 32.967501 6.0324991 35 8.5 35 L 17 35 L 17 40 L 13.5 40 A 1.50015 1.50015 0 1 0 13.5 43 L 18.253906 43 A 1.50015 1.50015 0 0 0 18.740234 43 L 29.253906 43 A 1.50015 1.50015 0 0 0 29.740234 43 L 34.5 43 A 1.50015 1.50015 0 1 0 34.5 40 L 31 40 L 31 35 L 39.5 35 C 41.967501 35 44 32.967501 44 30.5 L 44 9.5 C 44 7.0324991 41.967501 5 39.5 5 L 8.5 5 z M 8.5 8 L 39.5 8 C 40.346499 8 41 8.6535009 41 9.5 L 41 30.5 C 41 31.346499 40.346499 32 39.5 32 L 29.746094 32 A 1.50015 1.50015 0 0 0 29.259766 32 L 18.746094 32 A 1.50015 1.50015 0 0 0 18.259766 32 L 8.5 32 C 7.6535009 32 7 31.346499 7 30.5 L 7 9.5 C 7 8.6535009 7.6535009 8 8.5 8 z M 17.5 12 C 16.136406 12 15 13.136406 15 14.5 L 15 25.5 C 15 26.863594 16.136406 28 17.5 28 L 30.5 28 C 31.863594 28 33 26.863594 33 25.5 L 33 14.5 C 33 13.136406 31.863594 12 30.5 12 L 17.5 12 z M 18 18 L 30 18 L 30 25 L 18 25 L 18 18 z M 20 35 L 28 35 L 28 40 L 20 40 L 20 35 z
|
||||
</c-slot>
|
||||
</c-svg>
|
|
@ -3,18 +3,19 @@
|
|||
{% if form_content %}
|
||||
{{ form_content }}
|
||||
{% else %}
|
||||
<div class="max-width-container">
|
||||
<div class="form-container max-w-xl mx-auto">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<table class="mx-auto">
|
||||
{% csrf_token %}
|
||||
{{ form.as_div }}
|
||||
<div><input type="submit" value="Submit" /></div>
|
||||
<div class="submit-button-container">
|
||||
{{ form.as_table }}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input type="submit" value="Submit" />
|
||||
</td>
|
||||
</tr>
|
||||
{{ additional_row }}
|
||||
</div>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<c-slot name="scripts">
|
||||
{% if script_name %}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<span title="Price is a result of conversion and rounding." class="decoration-dotted underline">{{ slot }}</span>
|
|
@ -36,8 +36,8 @@
|
|||
<td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name relative align-top w-24 h-12 group">
|
||||
<span class="inline-block relative">
|
||||
<a class="underline decoration-slate-500 sm:decoration-2 inline-block truncate max-w-20char group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4 group-hover:decoration-purple-900 group-hover:text-purple-100"
|
||||
href="{% url 'view_game' session.game.id %}">
|
||||
{{ session.game.name }}
|
||||
href="{% url 'view_game' session.purchase.edition.game.id %}">
|
||||
{{ session.purchase.edition.name }}
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
|
|
|
@ -57,6 +57,10 @@
|
|||
<a href="{% url 'add_game' %}"
|
||||
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Game</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'add_edition' %}"
|
||||
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Edition</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'add_platform' %}"
|
||||
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platform</a>
|
||||
|
@ -98,6 +102,10 @@
|
|||
<a href="{% url 'list_games' %}"
|
||||
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Games</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'list_editions' %}"
|
||||
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Editions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'list_platforms' %}"
|
||||
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platforms</a>
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
{% load static %}
|
||||
{% partialdef purchase-name %}
|
||||
{% if purchase.type != 'game' %}
|
||||
<c-gamelink :game_id=purchase.first_game.id>
|
||||
{{ purchase.name }} ({{ purchase.first_game.name }} {{ purchase.get_type_display }})
|
||||
<c-gamelink :game_id=purchase.first_edition.game.id>
|
||||
{{ purchase.name }} ({{ purchase.first_edition.edition.name }} {{ purchase.get_type_display }})
|
||||
</c-gamelink>
|
||||
{% else %}
|
||||
<c-gamelink :game_id=purchase.first_game.id :name=purchase.first_game.name />
|
||||
<c-gamelink :game_id=purchase.first_edition.game.id :name=purchase.first_edition.name />
|
||||
{% endif %}
|
||||
{% endpartialdef %}
|
||||
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
|
||||
|
@ -46,7 +46,7 @@
|
|||
{% endif %}
|
||||
<tr>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Games ({{ year }})</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_year_games }}</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_2023_games }}</td>
|
||||
</tr>
|
||||
{% if all_finished_this_year_count %}
|
||||
<tr>
|
||||
|
@ -148,7 +148,7 @@
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h1 class="text-5xl text-center my-6">Games by playtime</h1>
|
||||
<h1 class="text-5xl text-center my-6">Top games by playtime</h1>
|
||||
<table class="responsive-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
@ -52,18 +52,6 @@
|
|||
{{ playrange }}
|
||||
</c-popover>
|
||||
</div>
|
||||
<div class="mb-6 text-slate-400">
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="uppercase font-bold text-slate-300">Status</span>
|
||||
<c-gamestatus :status="game.status">
|
||||
{{ game.get_status_display }}
|
||||
</c-gamestatus>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="uppercase font-bold text-slate-300">Platform</span>
|
||||
<span>{{ game.platform }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
|
||||
<a href="{% url 'edit_game' game.id %}">
|
||||
<button type="button"
|
||||
|
@ -79,21 +67,17 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<c-h1 :badge="edition_count">Editions</c-h1>
|
||||
<div class="mb-6">
|
||||
<c-simple-table :rows=edition_data.rows :columns=edition_data.columns />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<c-h1 :badge="purchase_count">Purchases</c-h1>
|
||||
{% if purchase_count %}
|
||||
<c-simple-table :rows=purchase_data.rows :columns=purchase_data.columns />
|
||||
{% else %}
|
||||
No purchases yet.
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<c-h1 :badge="session_count">Sessions</c-h1>
|
||||
{% if session_count %}
|
||||
<c-simple-table :rows=session_data.rows :columns=session_data.columns :header_action=session_data.header_action :page_obj=session_page_obj :elided_page_range=session_elided_page_range />
|
||||
{% else %}
|
||||
No sessions yet.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
|
|
@ -2,17 +2,8 @@
|
|||
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
|
||||
|
||||
<div class="flex flex-col gap-5 mb-3">
|
||||
<div class="font-bold font-serif text-slate-500 text-2xl">
|
||||
{% if not purchase.name %}
|
||||
Unnamed purchase
|
||||
{% else %}
|
||||
{{ purchase.name }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="text-balance max-w-[30rem] text-4xl">
|
||||
<span class="font-bold font-serif">
|
||||
{{ purchase.date_purchased }} ({{ purchase.num_purchases }} game{{ purchase.num_purchases|pluralize}})
|
||||
</span>
|
||||
<span class="font-bold font-serif">{% if purchase.name %}{{ purchase.name }}{% else %}Unnamed purchase{% endif %}</span> <span class="text-slate-500 text-2xl">({{ purchase.editions.count }} games)</span>
|
||||
</span>
|
||||
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
|
||||
<a href="{% url 'edit_purchase' purchase.id %}">
|
||||
|
@ -28,19 +19,12 @@
|
|||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
Price:
|
||||
<c-price-converted>{{ purchase.standardized_price }}</c-price-converted>
|
||||
({{ purchase.price | floatformat:2 }} {{ purchase.price_currency }})
|
||||
</p>
|
||||
<p>Price per game: <c-price-converted>{{ purchase.price_per_game | floatformat:0 }} {{ purchase.converted_currency }}</c-price-converted> </p>
|
||||
</div>
|
||||
<div>Price: {{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }} ({{ purchase.price | floatformat }} {{ purchase.price_currency }})</div>
|
||||
<div>
|
||||
<h2 class="text-base">Items:</h2>
|
||||
<ul class="list-disc list-inside">
|
||||
{% for game in purchase.games.all %}
|
||||
<li><c-gamelink :game_id=game.id :name=game.name /></li>
|
||||
{% for edition in purchase.editions.all %}
|
||||
<li><c-gamelink :game_id=edition.game.id :name=edition.name /></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.urls import path
|
||||
|
||||
from games.views import device, game, general, platform, purchase, session
|
||||
from games.views import device, edition, game, general, platform, purchase, session
|
||||
|
||||
urlpatterns = [
|
||||
path("", general.index, name="index"),
|
||||
|
@ -8,6 +8,19 @@ urlpatterns = [
|
|||
path("device/delete/<int:device_id>", device.delete_device, name="delete_device"),
|
||||
path("device/edit/<int:device_id>", device.edit_device, name="edit_device"),
|
||||
path("device/list", device.list_devices, name="list_devices"),
|
||||
path("edition/add", edition.add_edition, name="add_edition"),
|
||||
path(
|
||||
"edition/add/for-game/<int:game_id>",
|
||||
edition.add_edition,
|
||||
name="add_edition_for_game",
|
||||
),
|
||||
path("edition/<int:edition_id>/edit", edition.edit_edition, name="edit_edition"),
|
||||
path("edition/list", edition.list_editions, name="list_editions"),
|
||||
path(
|
||||
"edition/<int:edition_id>/delete",
|
||||
edition.delete_edition,
|
||||
name="delete_edition",
|
||||
),
|
||||
path("game/add", game.add_game, name="add_game"),
|
||||
path("game/<int:game_id>/edit", game.edit_game, name="edit_game"),
|
||||
path("game/<int:game_id>/view", game.view_game, name="view_game"),
|
||||
|
@ -26,11 +39,6 @@ urlpatterns = [
|
|||
),
|
||||
path("platform/list", platform.list_platforms, name="list_platforms"),
|
||||
path("purchase/add", purchase.add_purchase, name="add_purchase"),
|
||||
path(
|
||||
"purchase/add/for-game/<int:game_id>",
|
||||
purchase.add_purchase,
|
||||
name="add_purchase_for_game",
|
||||
),
|
||||
path(
|
||||
"purchase/<int:purchase_id>/edit",
|
||||
purchase.edit_purchase,
|
||||
|
@ -67,15 +75,20 @@ urlpatterns = [
|
|||
name="refund_purchase",
|
||||
),
|
||||
path(
|
||||
"purchase/related-purchase-by-game",
|
||||
purchase.related_purchase_by_game,
|
||||
name="related_purchase_by_game",
|
||||
"purchase/related-purchase-by-edition",
|
||||
purchase.related_purchase_by_edition,
|
||||
name="related_purchase_by_edition",
|
||||
),
|
||||
path(
|
||||
"purchase/add/for-edition/<int:edition_id>",
|
||||
purchase.add_purchase,
|
||||
name="add_purchase_for_edition",
|
||||
),
|
||||
path("session/add", session.add_session, name="add_session"),
|
||||
path(
|
||||
"session/add/for-game/<int:game_id>",
|
||||
"session/add/for-purchase/<int:purchase_id>",
|
||||
session.add_session,
|
||||
name="add_session_for_game",
|
||||
name="add_session_for_purchase",
|
||||
),
|
||||
path(
|
||||
"session/add/from-game/<int:session_id>",
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
from typing import Any
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from common.components import (
|
||||
A,
|
||||
Button,
|
||||
Icon,
|
||||
LinkedNameWithPlatformIcon,
|
||||
PopoverTruncated,
|
||||
)
|
||||
from common.time import dateformat, local_strftime
|
||||
from games.forms import EditionForm
|
||||
from games.models import Edition, Game
|
||||
|
||||
|
||||
@login_required
|
||||
def list_editions(request: HttpRequest) -> HttpResponse:
|
||||
context: dict[Any, Any] = {}
|
||||
page_number = request.GET.get("page", 1)
|
||||
limit = request.GET.get("limit", 10)
|
||||
editions = Edition.objects.order_by("-created_at")
|
||||
page_obj = None
|
||||
if int(limit) != 0:
|
||||
paginator = Paginator(editions, limit)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
editions = page_obj.object_list
|
||||
|
||||
context = {
|
||||
"title": "Manage editions",
|
||||
"page_obj": page_obj or None,
|
||||
"elided_page_range": (
|
||||
page_obj.paginator.get_elided_page_range(
|
||||
page_number, on_each_side=1, on_ends=1
|
||||
)
|
||||
if page_obj
|
||||
else None
|
||||
),
|
||||
"data": {
|
||||
"header_action": A([], Button([], "Add edition"), url="add_edition"),
|
||||
"columns": [
|
||||
"Game",
|
||||
"Name",
|
||||
"Sort Name",
|
||||
"Year",
|
||||
"Wikidata",
|
||||
"Created",
|
||||
"Actions",
|
||||
],
|
||||
"rows": [
|
||||
[
|
||||
LinkedNameWithPlatformIcon(
|
||||
name=edition.name,
|
||||
game_id=edition.game.id,
|
||||
platform=edition.platform,
|
||||
),
|
||||
PopoverTruncated(
|
||||
edition.name
|
||||
if edition.game.name != edition.name
|
||||
else "(identical)"
|
||||
),
|
||||
PopoverTruncated(
|
||||
edition.sort_name
|
||||
if edition.sort_name is not None
|
||||
and edition.game.name != edition.sort_name
|
||||
else "(identical)"
|
||||
),
|
||||
edition.year_released,
|
||||
edition.wikidata,
|
||||
local_strftime(edition.created_at, dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse("edit_edition", args=[edition.pk]),
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse(
|
||||
"delete_edition", args=[edition.pk]
|
||||
),
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
]
|
||||
for edition in editions
|
||||
],
|
||||
},
|
||||
}
|
||||
return render(request, "list_purchases.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_edition(request: HttpRequest, edition_id: int = 0) -> HttpResponse:
|
||||
edition = get_object_or_404(Edition, id=edition_id)
|
||||
form = EditionForm(request.POST or None, instance=edition)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("list_editions")
|
||||
|
||||
context: dict[str, Any] = {"form": form, "title": "Edit edition"}
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_edition(request: HttpRequest, edition_id: int) -> HttpResponse:
|
||||
edition = get_object_or_404(Edition, id=edition_id)
|
||||
edition.delete()
|
||||
return redirect("list_editions")
|
||||
|
||||
|
||||
@login_required
|
||||
def add_edition(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
||||
context: dict[str, Any] = {}
|
||||
if request.method == "POST":
|
||||
form = EditionForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
edition = form.save()
|
||||
if "submit_and_redirect" in request.POST:
|
||||
return HttpResponseRedirect(
|
||||
reverse(
|
||||
"add_purchase_for_edition", kwargs={"edition_id": edition.id}
|
||||
)
|
||||
)
|
||||
else:
|
||||
return redirect("index")
|
||||
else:
|
||||
if game_id:
|
||||
game = get_object_or_404(Game, id=game_id)
|
||||
form = EditionForm(
|
||||
initial={
|
||||
"game": game,
|
||||
"name": game.name,
|
||||
"sort_name": game.sort_name,
|
||||
"year_released": game.year_released,
|
||||
}
|
||||
)
|
||||
else:
|
||||
form = EditionForm()
|
||||
|
||||
context["form"] = form
|
||||
context["title"] = "Add New Edition"
|
||||
context["script_name"] = "add_edition.js"
|
||||
return render(request, "add_edition.html", context)
|
|
@ -14,7 +14,7 @@ from common.components import (
|
|||
Div,
|
||||
Icon,
|
||||
LinkedPurchase,
|
||||
NameWithIcon,
|
||||
NameWithPlatformIcon,
|
||||
Popover,
|
||||
PopoverTruncated,
|
||||
PurchasePrice,
|
||||
|
@ -29,7 +29,7 @@ from common.time import (
|
|||
)
|
||||
from common.utils import safe_division, truncate
|
||||
from games.forms import GameForm
|
||||
from games.models import Game, Purchase
|
||||
from games.models import Edition, Game, Purchase, Session
|
||||
from games.views.general import use_custom_redirect
|
||||
|
||||
|
||||
|
@ -61,24 +61,30 @@ def list_games(request: HttpRequest) -> HttpResponse:
|
|||
"Name",
|
||||
"Sort Name",
|
||||
"Year",
|
||||
"Status",
|
||||
"Wikidata",
|
||||
"Created",
|
||||
"Actions",
|
||||
],
|
||||
"rows": [
|
||||
[
|
||||
NameWithIcon(game_id=game.pk),
|
||||
A(
|
||||
[
|
||||
(
|
||||
"href",
|
||||
reverse(
|
||||
"view_game",
|
||||
args=[game.pk],
|
||||
),
|
||||
)
|
||||
],
|
||||
PopoverTruncated(game.name),
|
||||
),
|
||||
PopoverTruncated(
|
||||
game.sort_name
|
||||
if game.sort_name is not None and game.name != game.sort_name
|
||||
else "(identical)"
|
||||
),
|
||||
game.year_released,
|
||||
render_to_string(
|
||||
"cotton/gamestatus.html",
|
||||
{"status": game.status, "slot": game.get_status_display()},
|
||||
),
|
||||
game.wikidata,
|
||||
local_strftime(game.created_at, dateformat),
|
||||
render_to_string(
|
||||
|
@ -114,7 +120,7 @@ def add_game(request: HttpRequest) -> HttpResponse:
|
|||
game = form.save()
|
||||
if "submit_and_redirect" in request.POST:
|
||||
return HttpResponseRedirect(
|
||||
reverse("add_purchase_for_game", kwargs={"game_id": game.id})
|
||||
reverse("add_edition_for_game", kwargs={"game_id": game.id})
|
||||
)
|
||||
else:
|
||||
return redirect("list_games")
|
||||
|
@ -163,14 +169,23 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
),
|
||||
to_attr="game_purchases",
|
||||
)
|
||||
editions = (
|
||||
Edition.objects.filter(game=game)
|
||||
.prefetch_related(game_purchases_prefetch)
|
||||
.order_by("year_released")
|
||||
)
|
||||
|
||||
purchases = game.purchases.order_by("date_purchased")
|
||||
purchases = Purchase.objects.filter(editions__game=game).order_by("date_purchased")
|
||||
|
||||
sessions = game.sessions
|
||||
sessions = Session.objects.prefetch_related("device").filter(
|
||||
purchase__editions__game=game
|
||||
)
|
||||
session_count = sessions.count()
|
||||
session_count_without_manual = game.sessions.without_manual().count()
|
||||
session_count_without_manual = (
|
||||
Session.objects.without_manual().filter(purchase__editions__game=game).count()
|
||||
)
|
||||
|
||||
if sessions.exists():
|
||||
if sessions:
|
||||
playrange_start = local_strftime(sessions.earliest().timestamp_start, "%b %Y")
|
||||
latest_session = sessions.latest()
|
||||
playrange_end = local_strftime(latest_session.timestamp_start, "%b %Y")
|
||||
|
@ -189,6 +204,41 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
format_duration(sessions.calculated_duration_unformatted(), "%2.1H")
|
||||
)
|
||||
|
||||
edition_data: dict[str, Any] = {
|
||||
"columns": [
|
||||
"Name",
|
||||
"Year Released",
|
||||
"Actions",
|
||||
],
|
||||
"rows": [
|
||||
[
|
||||
NameWithPlatformIcon(
|
||||
name=edition.name,
|
||||
platform=edition.platform,
|
||||
),
|
||||
edition.year_released,
|
||||
render_to_string(
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse("edit_edition", args=[edition.pk]),
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse("delete_edition", args=[edition.pk]),
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
]
|
||||
for edition in editions
|
||||
],
|
||||
}
|
||||
|
||||
purchase_data: dict[str, Any] = {
|
||||
"columns": ["Name", "Type", "Date", "Price", "Actions"],
|
||||
"rows": [
|
||||
|
@ -219,8 +269,9 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
],
|
||||
}
|
||||
|
||||
sessions_all = game.sessions.order_by("-timestamp_start")
|
||||
|
||||
sessions_all = Session.objects.filter(purchase__editions__game=game).order_by(
|
||||
"-timestamp_start"
|
||||
)
|
||||
last_session = None
|
||||
if sessions_all.exists():
|
||||
last_session = sessions_all.latest()
|
||||
|
@ -247,7 +298,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
args=[last_session.pk],
|
||||
),
|
||||
children=Popover(
|
||||
popover_content=last_session.game.name,
|
||||
popover_content=last_session.purchase.first_edition.name,
|
||||
children=[
|
||||
Button(
|
||||
icon=True,
|
||||
|
@ -255,7 +306,9 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
size="xs",
|
||||
children=[
|
||||
Icon("play"),
|
||||
truncate(f"{last_session.game.name}"),
|
||||
truncate(
|
||||
f"{last_session.purchase.first_edition.name}"
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
|
@ -265,13 +318,16 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
else "",
|
||||
],
|
||||
),
|
||||
"columns": ["Game", "Date", "Duration", "Actions"],
|
||||
"columns": ["Edition", "Date", "Duration", "Actions"],
|
||||
"rows": [
|
||||
[
|
||||
NameWithIcon(
|
||||
session_id=session.pk,
|
||||
NameWithPlatformIcon(
|
||||
name=session.purchase.name
|
||||
if session.purchase.name
|
||||
else session.purchase.first_edition.name,
|
||||
platform=session.purchase.platform,
|
||||
),
|
||||
f"{local_strftime(session.timestamp_start)}{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}",
|
||||
f"{local_strftime(session.timestamp_start)}{f" — {local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}",
|
||||
(
|
||||
format_duration(session.duration_calculated, durationformat)
|
||||
if session.duration_calculated
|
||||
|
@ -315,9 +371,11 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
}
|
||||
|
||||
context: dict[str, Any] = {
|
||||
"edition_count": editions.count(),
|
||||
"editions": editions,
|
||||
"game": game,
|
||||
"playrange": playrange,
|
||||
"purchase_count": game.purchases.count(),
|
||||
"purchase_count": Purchase.objects.filter(editions__game=game).count(),
|
||||
"session_average_without_manual": round(
|
||||
safe_division(
|
||||
total_hours_without_manual, int(session_count_without_manual)
|
||||
|
@ -328,6 +386,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
"sessions": sessions,
|
||||
"title": f"Game Overview - {game.name}",
|
||||
"hours_sum": total_hours,
|
||||
"edition_data": edition_data,
|
||||
"purchase_data": purchase_data,
|
||||
"session_data": session_data,
|
||||
"session_page_obj": session_page_obj,
|
||||
|
|
|
@ -11,12 +11,13 @@ from django.urls import reverse
|
|||
|
||||
from common.time import available_stats_year_range, dateformat, format_duration
|
||||
from common.utils import safe_division
|
||||
from games.models import Game, Platform, Purchase, Session
|
||||
from games.models import Edition, Game, Platform, Purchase, Session
|
||||
|
||||
|
||||
def model_counts(request: HttpRequest) -> dict[str, bool]:
|
||||
return {
|
||||
"game_available": Game.objects.exists(),
|
||||
"edition_available": Edition.objects.exists(),
|
||||
"platform_available": Platform.objects.exists(),
|
||||
"purchase_available": Purchase.objects.exists(),
|
||||
"session_count": Session.objects.exists(),
|
||||
|
@ -48,7 +49,9 @@ def use_custom_redirect(
|
|||
@login_required
|
||||
def stats_alltime(request: HttpRequest) -> HttpResponse:
|
||||
year = "Alltime"
|
||||
this_year_sessions = Session.objects.all().prefetch_related(Prefetch("game"))
|
||||
this_year_sessions = Session.objects.all().prefetch_related(
|
||||
Prefetch("purchase__editions")
|
||||
)
|
||||
this_year_sessions_with_durations = this_year_sessions.annotate(
|
||||
duration=ExpressionWrapper(
|
||||
F("timestamp_end") - F("timestamp_start"),
|
||||
|
@ -56,9 +59,11 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||
)
|
||||
)
|
||||
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
|
||||
this_year_games = Game.objects.filter(sessions__in=this_year_sessions).distinct()
|
||||
this_year_games = Game.objects.filter(
|
||||
editions__purchase__session__in=this_year_sessions
|
||||
).distinct()
|
||||
this_year_games_with_session_counts = this_year_games.annotate(
|
||||
session_count=Count("sessions"),
|
||||
session_count=Count("editions__purchase__session"),
|
||||
)
|
||||
game_highest_session_count = this_year_games_with_session_counts.order_by(
|
||||
"-session_count"
|
||||
|
@ -71,11 +76,11 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||
.aggregate(dates=Count("date"))
|
||||
)
|
||||
this_year_played_purchases = Purchase.objects.filter(
|
||||
games__sessions__in=this_year_sessions
|
||||
session__in=this_year_sessions
|
||||
).distinct()
|
||||
|
||||
this_year_purchases = Purchase.objects.all()
|
||||
this_year_purchases_with_currency = this_year_purchases.select_related("games")
|
||||
this_year_purchases_with_currency = this_year_purchases.select_related("editions")
|
||||
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
|
||||
date_refunded=None
|
||||
)
|
||||
|
@ -124,10 +129,11 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||
total_spent = this_year_spendings["total_spent"] or 0
|
||||
|
||||
games_with_playtime = (
|
||||
Game.objects.filter(sessions__in=this_year_sessions)
|
||||
Game.objects.filter(editions__purchase__session__in=this_year_sessions)
|
||||
.annotate(
|
||||
total_playtime=Sum(
|
||||
F("sessions__duration_calculated") + F("sessions__duration_manual")
|
||||
F("editions__purchase__session__duration_calculated")
|
||||
+ F("editions__purchase__session__duration_manual")
|
||||
)
|
||||
)
|
||||
.values("id", "name", "total_playtime")
|
||||
|
@ -142,8 +148,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||
month["playtime"] = format_duration(month["playtime"], "%2.0H")
|
||||
|
||||
highest_session_average_game = (
|
||||
Game.objects.filter(sessions__in=this_year_sessions)
|
||||
.annotate(session_average=Avg("sessions__duration_calculated"))
|
||||
Game.objects.filter(editions__purchase__session__in=this_year_sessions)
|
||||
.annotate(
|
||||
session_average=Avg("editions__purchase__session__duration_calculated")
|
||||
)
|
||||
.order_by("-session_average")
|
||||
.first()
|
||||
)
|
||||
|
@ -152,9 +160,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
|
||||
|
||||
total_playtime_per_platform = (
|
||||
this_year_sessions.values("game__platform__name")
|
||||
this_year_sessions.values("purchase__platform__name")
|
||||
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
|
||||
.annotate(platform_name=F("game__platform__name"))
|
||||
.annotate(platform_name=F("purchase__platform__name"))
|
||||
.values("platform_name", "total_playtime")
|
||||
.order_by("-total_playtime")
|
||||
)
|
||||
|
@ -169,10 +177,10 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||
last_play_date = "N/A"
|
||||
if this_year_sessions:
|
||||
first_session = this_year_sessions.earliest()
|
||||
first_play_game = first_session.game
|
||||
first_play_game = first_session.purchase.first_edition.game
|
||||
first_play_date = first_session.timestamp_start.strftime(dateformat)
|
||||
last_session = this_year_sessions.latest()
|
||||
last_play_game = last_session.game
|
||||
last_play_game = last_session.purchase.first_edition.game
|
||||
last_play_date = last_session.timestamp_start.strftime(dateformat)
|
||||
|
||||
all_purchased_this_year_count = this_year_purchases_with_currency.count()
|
||||
|
@ -187,7 +195,7 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||
"total_hours": format_duration(
|
||||
this_year_sessions.total_duration_unformatted(), "%2.0H"
|
||||
),
|
||||
"total_year_games": this_year_played_purchases.all().count(),
|
||||
"total_2023_games": this_year_played_purchases.all().count(),
|
||||
"top_10_games_by_playtime": top_10_games_by_playtime,
|
||||
"year": year,
|
||||
"total_playtime_per_platform": total_playtime_per_platform,
|
||||
|
@ -220,7 +228,9 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
|
|||
if longest_session
|
||||
else 0
|
||||
),
|
||||
"longest_session_game": (longest_session.game if longest_session else None),
|
||||
"longest_session_game": (
|
||||
longest_session.purchase.first_edition.game if longest_session else None
|
||||
),
|
||||
"highest_session_count": (
|
||||
game_highest_session_count.session_count
|
||||
if game_highest_session_count
|
||||
|
@ -258,7 +268,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||
return HttpResponseRedirect(reverse("stats_alltime"))
|
||||
this_year_sessions = Session.objects.filter(
|
||||
timestamp_start__year=year
|
||||
).prefetch_related("game")
|
||||
).prefetch_related("purchase__editions")
|
||||
this_year_sessions_with_durations = this_year_sessions.annotate(
|
||||
duration=ExpressionWrapper(
|
||||
F("timestamp_end") - F("timestamp_start"),
|
||||
|
@ -266,11 +276,13 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||
)
|
||||
)
|
||||
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
|
||||
this_year_games = Game.objects.filter(sessions__in=this_year_sessions).distinct()
|
||||
this_year_games = Game.objects.filter(
|
||||
edition__purchases__session__in=this_year_sessions
|
||||
).distinct()
|
||||
this_year_games_with_session_counts = this_year_games.annotate(
|
||||
session_count=Count(
|
||||
"sessions",
|
||||
filter=Q(sessions__timestamp_start__year=year),
|
||||
"edition__purchases__session",
|
||||
filter=Q(edition__purchases__session__timestamp_start__year=year),
|
||||
)
|
||||
)
|
||||
game_highest_session_count = this_year_games_with_session_counts.order_by(
|
||||
|
@ -284,15 +296,11 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||
.aggregate(dates=Count("date"))
|
||||
)
|
||||
this_year_played_purchases = Purchase.objects.filter(
|
||||
games__sessions__in=this_year_sessions
|
||||
).distinct()
|
||||
|
||||
this_year_played_games = Game.objects.filter(
|
||||
sessions__in=this_year_sessions
|
||||
session__in=this_year_sessions
|
||||
).distinct()
|
||||
|
||||
this_year_purchases = Purchase.objects.filter(date_purchased__year=year)
|
||||
this_year_purchases_with_currency = this_year_purchases.prefetch_related("games")
|
||||
this_year_purchases_with_currency = this_year_purchases.prefetch_related("editions")
|
||||
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
|
||||
date_refunded=None
|
||||
).exclude(ownership_type=Purchase.DEMO)
|
||||
|
@ -329,7 +337,7 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||
|
||||
purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year)
|
||||
purchases_finished_this_year_released_this_year = (
|
||||
purchases_finished_this_year.filter(games__year_released=year).order_by(
|
||||
purchases_finished_this_year.filter(editions__year_released=year).order_by(
|
||||
"date_finished"
|
||||
)
|
||||
)
|
||||
|
@ -343,10 +351,11 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||
total_spent = this_year_spendings["total_spent"] or 0
|
||||
|
||||
games_with_playtime = (
|
||||
Game.objects.filter(sessions__in=this_year_sessions)
|
||||
Game.objects.filter(edition__purchases__session__in=this_year_sessions)
|
||||
.annotate(
|
||||
total_playtime=Sum(
|
||||
F("sessions__duration_calculated") + F("sessions__duration_manual")
|
||||
F("edition__purchases__session__duration_calculated")
|
||||
+ F("edition__purchases__session__duration_manual")
|
||||
)
|
||||
)
|
||||
.values("id", "name", "total_playtime")
|
||||
|
@ -361,19 +370,21 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||
month["playtime"] = format_duration(month["playtime"], "%2.0H")
|
||||
|
||||
highest_session_average_game = (
|
||||
Game.objects.filter(sessions__in=this_year_sessions)
|
||||
.annotate(session_average=Avg("sessions__duration_calculated"))
|
||||
Game.objects.filter(edition__purchases__session__in=this_year_sessions)
|
||||
.annotate(
|
||||
session_average=Avg("edition__purchases__session__duration_calculated")
|
||||
)
|
||||
.order_by("-session_average")
|
||||
.first()
|
||||
)
|
||||
top_10_games_by_playtime = games_with_playtime.order_by("-total_playtime")
|
||||
top_10_games_by_playtime = games_with_playtime.order_by("-total_playtime")[:10]
|
||||
for game in top_10_games_by_playtime:
|
||||
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
|
||||
|
||||
total_playtime_per_platform = (
|
||||
this_year_sessions.values("game__platform__name")
|
||||
this_year_sessions.values("purchase__platform__name")
|
||||
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
|
||||
.annotate(platform_name=F("game__platform__name"))
|
||||
.annotate(platform_name=F("purchase__platform__name"))
|
||||
.values("platform_name", "total_playtime")
|
||||
.order_by("-total_playtime")
|
||||
)
|
||||
|
@ -392,10 +403,10 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||
last_play_game = None
|
||||
if this_year_sessions:
|
||||
first_session = this_year_sessions.earliest()
|
||||
first_play_game = first_session.game
|
||||
first_play_game = first_session.purchase.first_edition.game
|
||||
first_play_date = first_session.timestamp_start.strftime(dateformat)
|
||||
last_session = this_year_sessions.latest()
|
||||
last_play_game = last_session.game
|
||||
last_play_game = last_session.purchase.first_edition.game
|
||||
last_play_date = last_session.timestamp_start.strftime(dateformat)
|
||||
|
||||
all_purchased_this_year_count = this_year_purchases_with_currency.count()
|
||||
|
@ -410,9 +421,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||
"total_hours": format_duration(
|
||||
this_year_sessions.total_duration_unformatted(), "%2.0H"
|
||||
),
|
||||
"total_games": this_year_played_games.count(),
|
||||
"total_year_games": this_year_played_purchases.filter(
|
||||
games__year_released=year
|
||||
"total_games": this_year_played_purchases.count(),
|
||||
"total_2023_games": this_year_played_purchases.filter(
|
||||
editions__year_released=year
|
||||
).count(),
|
||||
"top_10_games_by_playtime": top_10_games_by_playtime,
|
||||
"year": year,
|
||||
|
@ -424,15 +435,15 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||
safe_division(total_spent, this_year_purchases_without_refunded_count)
|
||||
),
|
||||
"all_finished_this_year": purchases_finished_this_year.prefetch_related(
|
||||
"games"
|
||||
"editions"
|
||||
).order_by("date_finished"),
|
||||
"all_finished_this_year_count": purchases_finished_this_year.count(),
|
||||
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.prefetch_related(
|
||||
"games"
|
||||
"editions"
|
||||
).order_by("date_finished"),
|
||||
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(),
|
||||
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.prefetch_related(
|
||||
"games"
|
||||
"editions"
|
||||
).order_by("date_finished"),
|
||||
"total_sessions": this_year_sessions.count(),
|
||||
"unique_days": unique_days["dates"],
|
||||
|
@ -461,7 +472,9 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
|
|||
if longest_session
|
||||
else 0
|
||||
),
|
||||
"longest_session_game": (longest_session.game if longest_session else None),
|
||||
"longest_session_game": (
|
||||
longest_session.purchase.first_edition.game if longest_session else None
|
||||
),
|
||||
"highest_session_count": (
|
||||
game_highest_session_count.session_count
|
||||
if game_highest_session_count
|
||||
|
|
|
@ -16,7 +16,7 @@ from django.utils import timezone
|
|||
from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice
|
||||
from common.time import dateformat
|
||||
from games.forms import PurchaseForm
|
||||
from games.models import Game, Purchase
|
||||
from games.models import Edition, Purchase
|
||||
from games.views.general import use_custom_redirect
|
||||
|
||||
|
||||
|
@ -138,7 +138,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
|||
|
||||
|
||||
@login_required
|
||||
def add_purchase(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
||||
def add_purchase(request: HttpRequest, edition_id: int = 0) -> HttpResponse:
|
||||
context: dict[str, Any] = {}
|
||||
initial = {"date_purchased": timezone.now()}
|
||||
|
||||
|
@ -149,20 +149,19 @@ def add_purchase(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
|||
if "submit_and_redirect" in request.POST:
|
||||
return HttpResponseRedirect(
|
||||
reverse(
|
||||
"add_session_for_game",
|
||||
kwargs={"game_id": purchase.first_game.id},
|
||||
"add_session_for_purchase", kwargs={"purchase_id": purchase.id}
|
||||
)
|
||||
)
|
||||
else:
|
||||
return redirect("list_purchases")
|
||||
else:
|
||||
if game_id:
|
||||
game = Game.objects.get(id=game_id)
|
||||
if edition_id:
|
||||
edition = Edition.objects.get(id=edition_id)
|
||||
form = PurchaseForm(
|
||||
initial={
|
||||
**initial,
|
||||
"games": [game],
|
||||
"platform": game.platform,
|
||||
"edition": edition,
|
||||
"platform": edition.platform,
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
@ -200,11 +199,7 @@ def delete_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
|
|||
@login_required
|
||||
def view_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
|
||||
purchase = get_object_or_404(Purchase, id=purchase_id)
|
||||
return render(
|
||||
request,
|
||||
"view_purchase.html",
|
||||
{"purchase": purchase, "title": f"Purchase: {purchase.full_name}"},
|
||||
)
|
||||
return render(request, "view_purchase.html", {"purchase": purchase})
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -231,14 +226,12 @@ def finish_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
|
|||
return redirect("list_purchases")
|
||||
|
||||
|
||||
def related_purchase_by_game(request: HttpRequest) -> HttpResponse:
|
||||
games = request.GET.getlist("games")
|
||||
if not games:
|
||||
return HttpResponseBadRequest("Invalid game_id")
|
||||
if isinstance(games, int) or isinstance(games, str):
|
||||
games = [games]
|
||||
def related_purchase_by_edition(request: HttpRequest) -> HttpResponse:
|
||||
edition_id = request.GET.get("edition")
|
||||
if not edition_id:
|
||||
return HttpResponseBadRequest("Invalid edition_id")
|
||||
form = PurchaseForm()
|
||||
form.fields["related_purchase"].queryset = Purchase.objects.filter(
|
||||
games__in=games, type=Purchase.GAME
|
||||
).order_by("games__sort_name")
|
||||
edition_id=edition_id, type=Purchase.GAME
|
||||
).order_by("edition__sort_name")
|
||||
return render(request, "partials/related_purchase_field.html", {"form": form})
|
||||
|
|
|
@ -15,7 +15,7 @@ from common.components import (
|
|||
Div,
|
||||
Form,
|
||||
Icon,
|
||||
NameWithIcon,
|
||||
LinkedNameWithPlatformIcon,
|
||||
Popover,
|
||||
)
|
||||
from common.time import (
|
||||
|
@ -28,7 +28,7 @@ from common.time import (
|
|||
)
|
||||
from common.utils import truncate
|
||||
from games.forms import SessionForm
|
||||
from games.models import Game, Session
|
||||
from games.models import Purchase, Session
|
||||
from games.views.general import use_custom_redirect
|
||||
|
||||
|
||||
|
@ -37,13 +37,13 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
|||
context: dict[Any, Any] = {}
|
||||
page_number = request.GET.get("page", 1)
|
||||
limit = request.GET.get("limit", 10)
|
||||
sessions = Session.objects.order_by("-timestamp_start", "created_at")
|
||||
sessions = Session.objects.order_by("-timestamp_start")
|
||||
search_string = request.GET.get("search_string", search_string)
|
||||
if search_string != "":
|
||||
sessions = sessions.filter(
|
||||
Q(game__name__icontains=search_string)
|
||||
| Q(game__name__icontains=search_string)
|
||||
| Q(game__platform__name__icontains=search_string)
|
||||
Q(purchase__edition__name__icontains=search_string)
|
||||
| Q(purchase__edition__game__name__icontains=search_string)
|
||||
| Q(purchase__platform__name__icontains=search_string)
|
||||
| Q(device__name__icontains=search_string)
|
||||
| Q(device__type__icontains=search_string)
|
||||
)
|
||||
|
@ -97,7 +97,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
|||
args=[last_session.pk],
|
||||
),
|
||||
children=Popover(
|
||||
popover_content=last_session.game.name,
|
||||
popover_content=last_session.purchase.first_edition.name,
|
||||
children=[
|
||||
Button(
|
||||
icon=True,
|
||||
|
@ -105,7 +105,9 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
|||
size="xs",
|
||||
children=[
|
||||
Icon("play"),
|
||||
truncate(f"{last_session.game.name}"),
|
||||
truncate(
|
||||
f"{last_session.purchase.first_edition.name}"
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
|
@ -128,8 +130,12 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
|
|||
],
|
||||
"rows": [
|
||||
[
|
||||
NameWithIcon(session_id=session.pk),
|
||||
f"{local_strftime(session.timestamp_start)}{f' — {local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}",
|
||||
LinkedNameWithPlatformIcon(
|
||||
name=session.purchase.first_edition.name,
|
||||
game_id=session.purchase.first_edition.game.pk,
|
||||
platform=session.purchase.platform,
|
||||
),
|
||||
f"{local_strftime(session.timestamp_start)}{f" — {local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}",
|
||||
(
|
||||
format_duration(session.duration_calculated, durationformat)
|
||||
if session.duration_calculated
|
||||
|
@ -189,13 +195,13 @@ def search_sessions(request: HttpRequest) -> HttpResponse:
|
|||
|
||||
|
||||
@login_required
|
||||
def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
||||
def add_session(request: HttpRequest, purchase_id: int = 0) -> HttpResponse:
|
||||
context = {}
|
||||
initial: dict[str, Any] = {"timestamp_start": timezone.now()}
|
||||
|
||||
last = Session.objects.last()
|
||||
if last != None:
|
||||
initial["game"] = last.game
|
||||
initial["purchase"] = last.purchase
|
||||
|
||||
if request.method == "POST":
|
||||
form = SessionForm(request.POST or None, initial=initial)
|
||||
|
@ -203,12 +209,12 @@ def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
|||
form.save()
|
||||
return redirect("list_sessions")
|
||||
else:
|
||||
if game_id:
|
||||
game = Game.objects.get(id=game_id)
|
||||
if purchase_id:
|
||||
purchase = Purchase.objects.get(id=purchase_id)
|
||||
form = SessionForm(
|
||||
initial={
|
||||
**initial,
|
||||
"game": game,
|
||||
"purchase": purchase,
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
@ -217,7 +223,7 @@ def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse:
|
|||
context["title"] = "Add New Session"
|
||||
context["script_name"] = "add_session.js"
|
||||
context["form"] = form
|
||||
return render(request, "add.html", context)
|
||||
return render(request, "add_session.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
|
@ -6,7 +6,6 @@ version = "3.8.1"
|
|||
description = "ASGI specs, helper code, and adapters"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"},
|
||||
{file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"},
|
||||
|
@ -21,7 +20,6 @@ version = "2024.8.30"
|
|||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
|
||||
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
|
||||
|
@ -33,7 +31,6 @@ version = "3.4.0"
|
|||
description = "Validate configuration and produce human readable error messages."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
|
||||
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
|
||||
|
@ -45,7 +42,6 @@ version = "3.4.0"
|
|||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"},
|
||||
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"},
|
||||
|
@ -160,7 +156,6 @@ version = "8.1.7"
|
|||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
|
@ -175,12 +170,10 @@ version = "0.4.6"
|
|||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
markers = {main = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "croniter"
|
||||
|
@ -188,7 +181,6 @@ version = "5.0.1"
|
|||
description = "croniter provides iteration for datetime object with cron like format"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "croniter-5.0.1-py2.py3-none-any.whl", hash = "sha256:eb28439742291f6c10b181df1a5ecf421208b1fc62ef44501daec1780a0b09e9"},
|
||||
{file = "croniter-5.0.1.tar.gz", hash = "sha256:7d9b1ef25b10eece48fdf29d8ac52f9b6252abff983ac614ade4f3276294019e"},
|
||||
|
@ -204,7 +196,6 @@ version = "1.15.1"
|
|||
description = "CSS unobfuscator and beautifier."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
|
||||
]
|
||||
|
@ -220,7 +211,6 @@ version = "0.3.9"
|
|||
description = "Distribution utilities"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
|
||||
{file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
|
||||
|
@ -228,14 +218,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.1.5"
|
||||
version = "5.1.3"
|
||||
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "Django-5.1.5-py3-none-any.whl", hash = "sha256:c46eb936111fffe6ec4bc9930035524a8be98ec2f74d8a0ff351226a3e52f459"},
|
||||
{file = "Django-5.1.5.tar.gz", hash = "sha256:19bbca786df50b9eca23cee79d495facf55c8f5c54c529d9bf1fe7b5ea086af3"},
|
||||
{file = "Django-5.1.3-py3-none-any.whl", hash = "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818"},
|
||||
{file = "Django-5.1.3.tar.gz", hash = "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -253,7 +242,6 @@ version = "1.3.0"
|
|||
description = "Bringing component based design to Django templates."
|
||||
optional = false
|
||||
python-versions = "<4,>=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "django_cotton-1.3.0-py3-none-any.whl", hash = "sha256:a23f29b759c43423e2f901352c0810388839cc412f6985614153c6ccfcfc2595"},
|
||||
{file = "django_cotton-1.3.0.tar.gz", hash = "sha256:8f4a15dd55c8ee9182cf7234c228ea45d9fcdec1de125221bce8d05af035730a"},
|
||||
|
@ -268,7 +256,6 @@ version = "4.4.6"
|
|||
description = "A configurable set of panels that display various debug information about the current request/response."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45"},
|
||||
{file = "django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044"},
|
||||
|
@ -284,7 +271,6 @@ version = "3.2.3"
|
|||
description = "Extensions for Django"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"},
|
||||
{file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"},
|
||||
|
@ -299,7 +285,6 @@ version = "1.21.0"
|
|||
description = "Extensions for using Django with htmx."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "django_htmx-1.21.0-py3-none-any.whl", hash = "sha256:64bc31463017a80552b767bc216ee5700248fa72e7ccd2963495e69afbdb6abe"},
|
||||
{file = "django_htmx-1.21.0.tar.gz", hash = "sha256:6ed3b42effd5980f22e68f36cd14ee4311bff3b6cb8435a89e27f45995691572"},
|
||||
|
@ -315,7 +300,6 @@ version = "3.2"
|
|||
description = "Pickled object field for Django"
|
||||
optional = false
|
||||
python-versions = ">=3"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "django-picklefield-3.2.tar.gz", hash = "sha256:aa463f5d79d497dbe789f14b45180f00a51d0d670067d0729f352a3941cdfa4d"},
|
||||
{file = "django_picklefield-3.2-py3-none-any.whl", hash = "sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c"},
|
||||
|
@ -333,7 +317,6 @@ version = "1.7.4"
|
|||
description = "A multiprocessing distributed task queue for Django"
|
||||
optional = false
|
||||
python-versions = "<4,>=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "django_q2-1.7.4-py3-none-any.whl", hash = "sha256:6eda6d56505822ee5ebc6c4eac1dde726f5dbf20ee9ea7080575535852e2671f"},
|
||||
{file = "django_q2-1.7.4.tar.gz", hash = "sha256:56a3781cc480474fa9c04bbde62445b0a9b4195adc409bd963b8f593b0598c43"},
|
||||
|
@ -354,7 +337,6 @@ version = "24.4"
|
|||
description = "django-template-partials"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "django_template_partials-24.4-py2.py3-none-any.whl", hash = "sha256:ee59d3839385d7f648907c3fa8d5923fcd66cd8090f141fe2a1c338b917984e2"},
|
||||
{file = "django_template_partials-24.4.tar.gz", hash = "sha256:25b67301470fc274ecc419e5e5fd4686a5020b1c038fd241a70eb087809034b6"},
|
||||
|
@ -373,7 +355,6 @@ version = "3.0.7"
|
|||
description = "Django/Jinja template indenter"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "djhtml-3.0.7.tar.gz", hash = "sha256:558c905b092a0c8afcbed27dea2f50aa6eb853a658b309e4e0f2bb378bdf6178"},
|
||||
]
|
||||
|
@ -387,7 +368,6 @@ version = "1.36.1"
|
|||
description = "HTML Template Linter and Formatter"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "djlint-1.36.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef40527fd6cd82cdd18f65a6bf5b486b767d2386f6c21f2ebd60e5d88f487fe8"},
|
||||
{file = "djlint-1.36.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4712de3dea172000a098da6a0cd709d158909b4964ba0f68bee584cef18b4878"},
|
||||
|
@ -430,7 +410,6 @@ version = "0.12.4"
|
|||
description = "EditorConfig File Locator and Interpreter for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
|
||||
]
|
||||
|
@ -441,7 +420,6 @@ version = "3.16.1"
|
|||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"},
|
||||
{file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"},
|
||||
|
@ -458,7 +436,6 @@ version = "3.4.3"
|
|||
description = "GraphQL Framework for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71"},
|
||||
{file = "graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa"},
|
||||
|
@ -480,7 +457,6 @@ version = "3.2.2"
|
|||
description = "Graphene Django integration"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "graphene-django-3.2.2.tar.gz", hash = "sha256:059ccf25d9a5159f28d7ebf1a648c993ab34deb064e80b70ca096aa22a609556"},
|
||||
{file = "graphene_django-3.2.2-py2.py3-none-any.whl", hash = "sha256:0fd95c8c1cbe77ae2a5940045ce276803c3acbf200a156731e0c730f2776ae2c"},
|
||||
|
@ -505,7 +481,6 @@ version = "3.2.5"
|
|||
description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL."
|
||||
optional = false
|
||||
python-versions = "<4,>=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a"},
|
||||
{file = "graphql_core-3.2.5.tar.gz", hash = "sha256:e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5"},
|
||||
|
@ -517,7 +492,6 @@ version = "3.2.0"
|
|||
description = "Relay library for graphql-core"
|
||||
optional = false
|
||||
python-versions = ">=3.6,<4"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c"},
|
||||
{file = "graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5"},
|
||||
|
@ -532,7 +506,6 @@ version = "22.0.0"
|
|||
description = "WSGI HTTP Server for UNIX"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"},
|
||||
{file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"},
|
||||
|
@ -554,7 +527,6 @@ version = "0.14.0"
|
|||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
|
@ -566,7 +538,6 @@ version = "2.6.2"
|
|||
description = "File identification library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"},
|
||||
{file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"},
|
||||
|
@ -581,7 +552,6 @@ version = "3.10"
|
|||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
|
@ -596,7 +566,6 @@ version = "2.0.0"
|
|||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
|
@ -608,7 +577,6 @@ version = "5.13.2"
|
|||
description = "A Python utility / library to sort Python imports."
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
|
||||
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
|
||||
|
@ -623,7 +591,6 @@ version = "1.15.1"
|
|||
description = "JavaScript unobfuscator and beautifier."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
|
||||
]
|
||||
|
@ -638,7 +605,6 @@ version = "0.9.25"
|
|||
description = "A Python implementation of the JSON5 data format."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"},
|
||||
{file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"},
|
||||
|
@ -650,7 +616,6 @@ version = "3.7"
|
|||
description = "Python implementation of John Gruber's Markdown."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"},
|
||||
{file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"},
|
||||
|
@ -666,7 +631,6 @@ version = "1.13.0"
|
|||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
|
||||
{file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
|
||||
|
@ -719,7 +683,6 @@ version = "1.0.0"
|
|||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
|
@ -731,7 +694,6 @@ version = "1.9.1"
|
|||
description = "Node.js virtual environment builder"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
|
||||
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
|
||||
|
@ -743,7 +705,6 @@ version = "24.2"
|
|||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
|
@ -755,7 +716,6 @@ version = "0.12.1"
|
|||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||
|
@ -767,7 +727,6 @@ version = "4.3.6"
|
|||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
|
||||
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
|
||||
|
@ -784,7 +743,6 @@ version = "1.5.0"
|
|||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
|
@ -800,7 +758,6 @@ version = "3.8.0"
|
|||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
|
||||
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
|
||||
|
@ -819,7 +776,6 @@ version = "2.3"
|
|||
description = "Promises/A+ implementation for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
|
||||
]
|
||||
|
@ -836,7 +792,6 @@ version = "8.3.3"
|
|||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
|
||||
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
|
||||
|
@ -857,7 +812,6 @@ version = "2.9.0.post0"
|
|||
description = "Extensions to the standard Python datetime module"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||
|
@ -872,7 +826,6 @@ version = "2024.2"
|
|||
description = "World timezone definitions, modern and historical"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"},
|
||||
{file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"},
|
||||
|
@ -884,7 +837,6 @@ version = "6.0.2"
|
|||
description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
|
||||
|
@ -947,7 +899,6 @@ version = "2024.11.6"
|
|||
description = "Alternative regular expression module, to replace re."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"},
|
||||
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"},
|
||||
|
@ -1051,7 +1002,6 @@ version = "2.32.3"
|
|||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
|
@ -1073,7 +1023,6 @@ version = "1.16.0"
|
|||
description = "Python 2 and 3 compatibility utilities"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
|
@ -1085,7 +1034,6 @@ version = "0.5.1"
|
|||
description = "A non-validating SQL parser."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"},
|
||||
{file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"},
|
||||
|
@ -1101,7 +1049,6 @@ version = "1.3"
|
|||
description = "The most basic Text::Unidecode port"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
|
||||
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
|
||||
|
@ -1113,7 +1060,6 @@ version = "4.67.0"
|
|||
description = "Fast, Extensible Progress Meter"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be"},
|
||||
{file = "tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a"},
|
||||
|
@ -1135,7 +1081,6 @@ version = "4.12.2"
|
|||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
|
@ -1147,8 +1092,6 @@ version = "2024.2"
|
|||
description = "Provider of IANA time zone data"
|
||||
optional = false
|
||||
python-versions = ">=2"
|
||||
groups = ["main", "dev"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
files = [
|
||||
{file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"},
|
||||
{file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"},
|
||||
|
@ -1160,7 +1103,6 @@ version = "2.2.3"
|
|||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
|
||||
{file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
|
||||
|
@ -1178,7 +1120,6 @@ version = "0.30.6"
|
|||
description = "The lightning-fast ASGI server."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"},
|
||||
{file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"},
|
||||
|
@ -1193,14 +1134,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
|
|||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.29.1"
|
||||
version = "20.27.1"
|
||||
description = "Virtual Python Environment builder"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"},
|
||||
{file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"},
|
||||
{file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"},
|
||||
{file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1213,6 +1153,6 @@ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "s
|
|||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "b5bb46a6591964aec145637cd9a412a681f2cc5e7e4fdd6fd9ecb0fe8724b8e3"
|
||||
|
|
|
@ -10,7 +10,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
|||
django.setup()
|
||||
from django.conf import settings
|
||||
|
||||
from games.models import Game, Platform, Purchase, Session
|
||||
from games.models import Edition, Game, Platform, Purchase, Session
|
||||
|
||||
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
|
||||
|
||||
|
@ -21,8 +21,10 @@ class PathWorksTest(TestCase):
|
|||
pl.save()
|
||||
g = Game(name="The Test Game")
|
||||
g.save()
|
||||
e = Edition(game=g, name="The Test Game Edition", platform=pl)
|
||||
e.save()
|
||||
p = Purchase(
|
||||
games=[e],
|
||||
edition=e,
|
||||
platform=pl,
|
||||
date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO),
|
||||
)
|
||||
|
@ -51,6 +53,11 @@ class PathWorksTest(TestCase):
|
|||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_add_edition_returns_200(self):
|
||||
url = reverse("add_edition")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_add_purchase_returns_200(self):
|
||||
url = reverse("add_purchase")
|
||||
response = self.client.get(url)
|
||||
|
|
|
@ -3,13 +3,14 @@ from datetime import datetime
|
|||
from zoneinfo import ZoneInfo
|
||||
|
||||
import django
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
||||
django.setup()
|
||||
from django.conf import settings
|
||||
|
||||
from games.models import Game, Purchase, Session
|
||||
from games.models import Edition, Game, Purchase, Session
|
||||
|
||||
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
|
||||
|
||||
|
@ -21,8 +22,10 @@ class FormatDurationTest(TestCase):
|
|||
def test_duration_format(self):
|
||||
g = Game(name="The Test Game")
|
||||
g.save()
|
||||
e = Edition(game=g, name="The Test Game Edition")
|
||||
e.save()
|
||||
p = Purchase(
|
||||
game=g, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO)
|
||||
edition=e, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO)
|
||||
)
|
||||
p.save()
|
||||
s = Session(
|
||||
|
|
|
@ -47,7 +47,7 @@ INSTALLED_APPS = [
|
|||
|
||||
Q_CLUSTER = {
|
||||
"name": "DjangoQ",
|
||||
"workers": 1,
|
||||
"workers": 4,
|
||||
"recycle": 500,
|
||||
"timeout": 60,
|
||||
"retry": 120,
|
||||
|
@ -113,10 +113,6 @@ DATABASES = {
|
|||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
"OPTIONS": {
|
||||
"timeout": 20,
|
||||
"init_command": "PRAGMA synchronous=NORMAL; PRAGMA journal_mode=WAL;",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue