diff --git a/guild/forms.py b/guild/forms.py index feb8b33..04eec4f 100644 --- a/guild/forms.py +++ b/guild/forms.py @@ -1,5 +1,6 @@ from django import forms -from guild.models import PlaySession, Character +from guild.models import PlaySession, Character, Reward +from django.utils.translation import gettext as _ class PlaySessionCharacterForm(forms.ModelMultipleChoiceField): @@ -23,3 +24,15 @@ class PlaySessionForm(forms.ModelForm): "date": forms.DateInput(attrs={"type": "date"}), "summary": forms.Textarea(attrs={"rows": 32}), } + + +class RewardForm(forms.ModelForm): + character = forms.ModelChoiceField( + queryset=Character.objects.all(), + empty_label="All Guild Members", + required=False, + ) + + class Meta: + model = Reward + fields = ["amount", "resource", "character"] diff --git a/guild/migrations/0005_adventure_created_at_adventure_updated_at_and_more.py b/guild/migrations/0005_adventure_created_at_adventure_updated_at_and_more.py new file mode 100644 index 0000000..fd0b911 --- /dev/null +++ b/guild/migrations/0005_adventure_created_at_adventure_updated_at_and_more.py @@ -0,0 +1,115 @@ +# Generated by Django 4.2.1 on 2023-06-20 18:10 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + dependencies = [ + ("guild", "0004_alter_playsession_options_player_address"), + ] + + operations = [ + migrations.AddField( + model_name="adventure", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.now, + verbose_name="created at", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="adventure", + name="updated_at", + field=models.DateTimeField(auto_now=True, verbose_name="updated at"), + ), + migrations.AddField( + model_name="character", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.now, + verbose_name="created at", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="character", + name="updated_at", + field=models.DateTimeField(auto_now=True, verbose_name="updated at"), + ), + migrations.AddField( + model_name="player", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.now, + verbose_name="created at", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="player", + name="updated_at", + field=models.DateTimeField(auto_now=True, verbose_name="updated at"), + ), + migrations.AddField( + model_name="playsession", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.now, + verbose_name="created at", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="playsession", + name="updated_at", + field=models.DateTimeField(auto_now=True, verbose_name="updated at"), + ), + migrations.AddField( + model_name="resource", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.now, + verbose_name="created at", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="resource", + name="updated_at", + field=models.DateTimeField(auto_now=True, verbose_name="updated at"), + ), + migrations.AddField( + model_name="resourceearned", + name="created_at", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.now, + verbose_name="created at", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="resourceearned", + name="updated_at", + field=models.DateTimeField(auto_now=True, verbose_name="updated at"), + ), + migrations.AlterField( + model_name="resourceearned", + name="character", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="guild.character", + verbose_name="character", + ), + ), + ] diff --git a/guild/migrations/0006_alter_resource_name.py b/guild/migrations/0006_alter_resource_name.py new file mode 100644 index 0000000..8f7d1fd --- /dev/null +++ b/guild/migrations/0006_alter_resource_name.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-06-20 18:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("guild", "0005_adventure_created_at_adventure_updated_at_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="resource", + name="name", + field=models.CharField(max_length=255, unique=True), + ), + ] diff --git a/guild/migrations/0007_resourceearned_playsession.py b/guild/migrations/0007_resourceearned_playsession.py new file mode 100644 index 0000000..3b12043 --- /dev/null +++ b/guild/migrations/0007_resourceearned_playsession.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.1 on 2023-06-20 18:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("guild", "0006_alter_resource_name"), + ] + + operations = [ + migrations.AddField( + model_name="resourceearned", + name="playsession", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="guild.playsession", + verbose_name="playsession", + ), + ), + ] diff --git a/guild/migrations/0008_alter_resourceearned_playsession.py b/guild/migrations/0008_alter_resourceearned_playsession.py new file mode 100644 index 0000000..20a6fdd --- /dev/null +++ b/guild/migrations/0008_alter_resourceearned_playsession.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.1 on 2023-06-20 18:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("guild", "0007_resourceearned_playsession"), + ] + + operations = [ + migrations.AlterField( + model_name="resourceearned", + name="playsession", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="guild.playsession", + verbose_name="playsession", + ), + ), + ] diff --git a/guild/migrations/0009_rename_resourceearned_reward_alter_reward_options.py b/guild/migrations/0009_rename_resourceearned_reward_alter_reward_options.py new file mode 100644 index 0000000..22502db --- /dev/null +++ b/guild/migrations/0009_rename_resourceearned_reward_alter_reward_options.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.1 on 2023-06-20 18:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("guild", "0008_alter_resourceearned_playsession"), + ] + + operations = [ + migrations.RenameModel( + old_name="ResourceEarned", + new_name="Reward", + ), + migrations.AlterModelOptions( + name="reward", + options={"verbose_name": "reward", "verbose_name_plural": "rewards"}, + ), + ] diff --git a/guild/models.py b/guild/models.py index 411a201..24e9db3 100644 --- a/guild/models.py +++ b/guild/models.py @@ -8,6 +8,9 @@ class Player(models.Model): address = models.TextField(_("address"), blank=True) + created_at = models.DateTimeField(_("created at"), auto_now_add=True) + updated_at = models.DateTimeField(_("updated at"), auto_now=True) + class Meta: verbose_name = _("player") verbose_name_plural = _("players") @@ -20,9 +23,12 @@ class Player(models.Model): class Resource(models.Model): - name = models.CharField(max_length=255) + name = models.CharField(max_length=255, unique=True) description = models.TextField() + created_at = models.DateTimeField(_("created at"), auto_now_add=True) + updated_at = models.DateTimeField(_("updated at"), auto_now=True) + class Meta: verbose_name = _("resource") verbose_name_plural = _("resources") @@ -46,6 +52,9 @@ class Adventure(models.Model): blank=True, ) + created_at = models.DateTimeField(_("created at"), auto_now_add=True) + updated_at = models.DateTimeField(_("updated at"), auto_now=True) + @property def last_session(self): return self.playsession_set.order_by("-date").first() @@ -82,6 +91,9 @@ class Character(models.Model): blank=True, ) + created_at = models.DateTimeField(_("created at"), auto_now_add=True) + updated_at = models.DateTimeField(_("updated at"), auto_now=True) + class Meta: verbose_name = _("character") verbose_name_plural = _("characters") @@ -102,6 +114,9 @@ class PlaySession(models.Model): summary = models.TextField() + created_at = models.DateTimeField(_("created at"), auto_now_add=True) + updated_at = models.DateTimeField(_("updated at"), auto_now=True) + class Meta: verbose_name = _("playsession") verbose_name_plural = _("playsessions") @@ -118,25 +133,34 @@ class PlaySession(models.Model): ) -class ResourceEarned(models.Model): +class Reward(models.Model): resource = models.ForeignKey( "Resource", verbose_name=_("resource"), on_delete=models.CASCADE ) character = models.ForeignKey( "Character", verbose_name=_("character"), - on_delete=models.CASCADE, + on_delete=models.SET_NULL, null=True, blank=True, ) + playsession = models.ForeignKey( + "PlaySession", + verbose_name=_("playsession"), + on_delete=models.CASCADE, + null=False, + ) amount = models.IntegerField() + created_at = models.DateTimeField(_("created at"), auto_now_add=True) + updated_at = models.DateTimeField(_("updated at"), auto_now=True) + class Meta: - verbose_name = _("resourceearned") - verbose_name_plural = _("resourceearneds") + verbose_name = _("reward") + verbose_name_plural = _("rewards") def __str__(self): - return self.name + return f"{self.resource.name} ({self.amount}) - {self.character}" def get_absolute_url(self): - return reverse("guild:resourceearned_detail", kwargs={"pk": self.pk}) + return reverse("guild:reward_detail", kwargs={"pk": self.pk}) diff --git a/guild/templates/guild/adventure_detail.html b/guild/templates/guild/adventure_detail.html index 6027039..a336661 100644 --- a/guild/templates/guild/adventure_detail.html +++ b/guild/templates/guild/adventure_detail.html @@ -42,7 +42,9 @@ {% else %} {% for playsession in adventure.playsession_set.all %}
-

{{ playsession.date }}

+ +

{{ playsession.date }}

+
Edit Delete
@@ -51,7 +53,7 @@ diff --git a/guild/templates/guild/character_detail.html b/guild/templates/guild/character_detail.html index 2385107..40471fb 100644 --- a/guild/templates/guild/character_detail.html +++ b/guild/templates/guild/character_detail.html @@ -10,10 +10,12 @@

Character: {{ character.name }}

{{ character.get_status_display }} - , played by: - - {{ character.player.name }} - + {% if character.player %} + , played by: + + {{ character.player.name }} + + {% endif %}

@@ -42,4 +44,35 @@ {% endif %} +

Rewards

+ + + +

Reward History

+{% if not character.reward_set.count %} +

No rewards.

+{% else %} + +{% endif %} + {% endblock content %} \ No newline at end of file diff --git a/guild/templates/guild/home.html b/guild/templates/guild/home.html index 9f1c713..ba360f0 100644 --- a/guild/templates/guild/home.html +++ b/guild/templates/guild/home.html @@ -12,7 +12,6 @@ - +

Total Guild Rewards

+ + +

Characters

diff --git a/guild/templates/guild/playsession_detail.html b/guild/templates/guild/playsession_detail.html new file mode 100644 index 0000000..8a386ea --- /dev/null +++ b/guild/templates/guild/playsession_detail.html @@ -0,0 +1,77 @@ +{% extends 'base.html' %} + +{% load guild_extras %} + +{% block content %} + +
+
+ +

Session: {{ playsession.date }}

+

Adventure: + + {{ playsession.adventure.name }} + +

+
+
+
+ Edit +
+
+ Delete +
+
+ +
+ +

Characters

+ + + +

Rewards

+ +Add Reward + + + + + + + + + + + + {% for reward in playsession.reward_set.all %} + + + + + + + {% endfor %} + +
AmountResourceCharacter
{{ reward.amount }}{{ reward.resource.name }} + {% if reward.character %} + {{ reward.character.name }} + {% else %} + Every Guild Member + {% endif %} + + Remove +
+ +{% if playsession.reward_set.count == 0 %} +

No rewards earned.

+{% endif %} + +

Summary

+ +

{{playsession.summary|md|safe}}

+ +{% endblock content %} \ No newline at end of file diff --git a/guild/templates/guild/resource_detail.html b/guild/templates/guild/resource_detail.html new file mode 100644 index 0000000..7d860ae --- /dev/null +++ b/guild/templates/guild/resource_detail.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Resource: {{ resource.name }}

+
+
+ Edit +
+
+ +

+ {{ resource.description|linebreaks }} +

+{% endblock content %} \ No newline at end of file diff --git a/guild/templates/guild/resource_form.html b/guild/templates/guild/resource_form.html new file mode 100644 index 0000000..20792ba --- /dev/null +++ b/guild/templates/guild/resource_form.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} + {% if object %} + + {% else %} + + {% endif %} +
+{% endblock content %} \ No newline at end of file diff --git a/guild/templates/guild/reward_confirm_delete.html b/guild/templates/guild/reward_confirm_delete.html new file mode 100644 index 0000000..44cf3a2 --- /dev/null +++ b/guild/templates/guild/reward_confirm_delete.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block content %} + +

Reward Delete

+ +

Are you sure you want to delete + + {{ reward.amount }} {{ reward.resource.name }} - + {% if reward.character %} + {{ reward.character.name }} + {% else %} + Every Guild Member + {% endif %} +? +

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock content %} \ No newline at end of file diff --git a/guild/templates/guild/reward_form.html b/guild/templates/guild/reward_form.html new file mode 100644 index 0000000..a273132 --- /dev/null +++ b/guild/templates/guild/reward_form.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} + {% if object %} + + {% else %} + + {% endif %} +
+{% endblock content %} \ No newline at end of file diff --git a/guild/templates/guild/settings.html b/guild/templates/guild/settings.html new file mode 100644 index 0000000..3c11f91 --- /dev/null +++ b/guild/templates/guild/settings.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} + +{% block content %} +

Settings

+ +
+
+

Resources

+
+ +
+ + + + +{% endblock content %} \ No newline at end of file diff --git a/guild/urls.py b/guild/urls.py index 79eccc6..94cc7d2 100644 --- a/guild/urls.py +++ b/guild/urls.py @@ -3,6 +3,9 @@ import guild.views as views import guild.views.player as player_views import guild.views.character as character_views import guild.views.adventure as adventure_views +import guild.views.playsession as playsession_views +import guild.views.settings as settings_views +import guild.views.resource as resource_views from django.contrib.auth import views as auth_views urlpatterns = [ @@ -86,17 +89,52 @@ urlpatterns = [ ), path( "adventures//playsession/create/", - adventure_views.PlaySessionCreateView.as_view(), + playsession_views.PlaySessionCreateView.as_view(), name="create_playsession", ), path( "adventures//playsession//update/", - adventure_views.PlaySessionUpdateView.as_view(), + playsession_views.PlaySessionUpdateView.as_view(), name="playsession_update", ), path( "adventures//playsession//delete/", - adventure_views.PlaySessionDeleteView.as_view(), + playsession_views.PlaySessionDeleteView.as_view(), name="playsession_delete", ), + path( + "playsessions//", + playsession_views.PlaySessionDetailView.as_view(), + name="playsession_detail", + ), + path( + "playsessions//rewards/create/", + playsession_views.CreateRewardView.as_view(), + name="create_reward", + ), + path( + "playsessions//rewards//delete/", + playsession_views.RewardDeleteView.as_view(), + name="reward_delete", + ), + path( + "settings/", + settings_views.SettingsView.as_view(), + name="settings", + ), + path( + "resources/create/", + resource_views.CreateResourceView.as_view(), + name="create_resource", + ), + path( + "resources//", + resource_views.ResourceDetailView.as_view(), + name="resource_detail", + ), + path( + "resources//update/", + resource_views.ResourceUpdateView.as_view(), + name="resource_update", + ), ] diff --git a/guild/views/__init__.py b/guild/views/__init__.py index 605c93c..72fa1ea 100644 --- a/guild/views/__init__.py +++ b/guild/views/__init__.py @@ -3,7 +3,7 @@ from django.views.generic import TemplateView, ListView, DetailView from django.views.generic.edit import CreateView from django.contrib.auth.mixins import LoginRequiredMixin -from guild.models import Adventure, Character, Player +from guild.models import Adventure, Character, Player, Reward # Create your views here. @@ -21,4 +21,13 @@ class HomeView(LoginRequiredMixin, TemplateView): context["adventures"] = advs context["players"] = Player.objects.all() context["characters"] = Character.objects.all() + + context["rewards"] = Reward.objects.filter(character=None).order_by( + "-playsession__date" + ) + context["resources"] = {} + for reward in context["rewards"]: + if reward.resource.name not in context["resources"]: + context["resources"][reward.resource.name] = 0 + context["resources"][reward.resource.name] += reward.amount return context diff --git a/guild/views/adventure.py b/guild/views/adventure.py index 7c5c6b3..9705881 100644 --- a/guild/views/adventure.py +++ b/guild/views/adventure.py @@ -39,38 +39,3 @@ class AdventureUpdateView(LoginRequiredMixin, UpdateView): class AdventureDeleteView(LoginRequiredMixin, DeleteView): model = Adventure success_url = "/" - - -class PlaySessionCreateView(LoginRequiredMixin, CreateView): - model = PlaySession - form_class = PlaySessionForm - - def dispatch(self, request, *args, **kwargs): - self.adventure = get_object_or_404(Adventure, pk=kwargs["pk"]) - return super().dispatch(request, *args, **kwargs) - - def form_valid(self, form: BaseModelForm) -> HttpResponse: - form.instance.adventure = self.adventure - return super().form_valid(form) - - -class PlaySessionUpdateView(LoginRequiredMixin, UpdateView): - model = PlaySession - form_class = PlaySessionForm - pk_url_kwarg = "playsession_pk" - - def dispatch(self, request, *args, **kwargs): - self.adventure = get_object_or_404(Adventure, pk=kwargs["pk"]) - return super().dispatch(request, *args, **kwargs) - - -class PlaySessionDeleteView(LoginRequiredMixin, DeleteView): - model = PlaySession - pk_url_kwarg = "playsession_pk" - - def dispatch(self, request, *args, **kwargs): - self.adventure = get_object_or_404(Adventure, pk=kwargs["pk"]) - return super().dispatch(request, *args, **kwargs) - - def get_success_url(self) -> str: - return self.adventure.get_absolute_url() diff --git a/guild/views/character.py b/guild/views/character.py index 36836da..8aab3e0 100644 --- a/guild/views/character.py +++ b/guild/views/character.py @@ -1,8 +1,8 @@ from django.views.generic import TemplateView, ListView, DetailView from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.contrib.auth.mixins import LoginRequiredMixin - -from guild.models import Character +from django.db.models import Q +from guild.models import Character, Reward class CharacterListView(LoginRequiredMixin, ListView): @@ -18,6 +18,18 @@ class CreateCharacterView(LoginRequiredMixin, CreateView): class CharacterDetailView(LoginRequiredMixin, DetailView): model = Character + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["rewards"] = Reward.objects.filter( + Q(character=self.object) | Q(character=None) + ).order_by("-playsession__date") + context["resources"] = {} + for reward in context["rewards"]: + if reward.resource.name not in context["resources"]: + context["resources"][reward.resource.name] = 0 + context["resources"][reward.resource.name] += reward.amount + return context + class CharacterUpdateView(LoginRequiredMixin, UpdateView): model = Character diff --git a/guild/views/playsession.py b/guild/views/playsession.py new file mode 100644 index 0000000..a6720a7 --- /dev/null +++ b/guild/views/playsession.py @@ -0,0 +1,78 @@ +from typing import Any, Dict, Optional, Type +from django.forms.models import BaseModelForm +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.views.generic import TemplateView, ListView, DetailView +from django.views.generic.edit import CreateView, UpdateView, DeleteView +from django.contrib.auth.mixins import LoginRequiredMixin +from guild.forms import PlaySessionForm, RewardForm + +from guild.models import Adventure, PlaySession, Reward + + +class PlaySessionCreateView(LoginRequiredMixin, CreateView): + model = PlaySession + form_class = PlaySessionForm + + def dispatch(self, request, *args, **kwargs): + self.adventure = get_object_or_404(Adventure, pk=kwargs["pk"]) + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form: BaseModelForm) -> HttpResponse: + form.instance.adventure = self.adventure + return super().form_valid(form) + + +class PlaySessionDetailView(LoginRequiredMixin, DetailView): + model = PlaySession + + +class PlaySessionUpdateView(LoginRequiredMixin, UpdateView): + model = PlaySession + form_class = PlaySessionForm + pk_url_kwarg = "playsession_pk" + + def dispatch(self, request, *args, **kwargs): + self.adventure = get_object_or_404(Adventure, pk=kwargs["pk"]) + return super().dispatch(request, *args, **kwargs) + + +class PlaySessionDeleteView(LoginRequiredMixin, DeleteView): + model = PlaySession + pk_url_kwarg = "playsession_pk" + + def dispatch(self, request, *args, **kwargs): + self.adventure = get_object_or_404(Adventure, pk=kwargs["pk"]) + return super().dispatch(request, *args, **kwargs) + + def get_success_url(self) -> str: + return self.adventure.get_absolute_url() + + +class CreateRewardView(LoginRequiredMixin, CreateView): + model = Reward + form_class = RewardForm + + def dispatch(self, request, *args, **kwargs): + self.playsession = get_object_or_404(PlaySession, pk=kwargs["pk"]) + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form: BaseModelForm) -> HttpResponse: + form.instance.playsession = self.playsession + return super().form_valid(form) + + def get_success_url(self) -> str: + return reverse("guild:playsession_detail", kwargs={"pk": self.playsession.pk}) + + +class RewardDeleteView(LoginRequiredMixin, DeleteView): + model = Reward + pk_url_kwarg = "reward_pk" + + def dispatch(self, request, *args, **kwargs): + self.playsession = get_object_or_404(PlaySession, pk=kwargs["pk"]) + return super().dispatch(request, *args, **kwargs) + + def get_success_url(self) -> str: + return reverse("guild:playsession_detail", kwargs={"pk": self.playsession.pk}) diff --git a/guild/views/resource.py b/guild/views/resource.py new file mode 100644 index 0000000..c4dd501 --- /dev/null +++ b/guild/views/resource.py @@ -0,0 +1,28 @@ +from django.views.generic import TemplateView, ListView, DetailView +from django.views.generic.edit import CreateView, UpdateView, DeleteView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse + +from guild.models import Resource + + +class CreateResourceView(LoginRequiredMixin, CreateView): + model = Resource + fields = ["name", "description"] + + def get_success_url(self): + return reverse("guild:settings") + + +class ResourceDetailView(LoginRequiredMixin, DetailView): + model = Resource + + +class ResourceUpdateView(LoginRequiredMixin, UpdateView): + model = Resource + fields = ["name", "description"] + + +class ResourceDeleteView(LoginRequiredMixin, DeleteView): + model = Resource + success_url = "/" diff --git a/guild/views/settings.py b/guild/views/settings.py new file mode 100644 index 0000000..d953a83 --- /dev/null +++ b/guild/views/settings.py @@ -0,0 +1,12 @@ +from django.views.generic import TemplateView + +from guild.models import Resource + + +class SettingsView(TemplateView): + template_name = "guild/settings.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["resources"] = Resource.objects.all() + return context diff --git a/templates/base.html b/templates/base.html index ac9b7b0..3f41471 100644 --- a/templates/base.html +++ b/templates/base.html @@ -87,5 +87,10 @@ {% endblock %} + + \ No newline at end of file