Django forms DateTimeInput widget- how to specify max date? - python

I have a form displayed in my Django project, and one of the form fields is a DateTimeInput widget, defined with:
presentation_date = mDateTimeField(required=False, label="Presentation date", widget=forms.DateTimeInput(format='%d/%m/%Y %H:%M'))
This widget currently displays all dates between the start of last year to the end of this year (01/01/2016- 31/12/2017). However, at the end of last year this caused some issues, as it meant that users were unable to select dates for the beginning of this year. What I want to do is extend the range of the dates available for selection in the widget by one year (i.e the new range would be 01/01/2016- 31/12/2018).
I understand that it is possible to do this using form validation (for example, by writing a view such as clean_presentation_date() and performing the validation inside that view, but this won't solve the issue I'm having- as currently, dates before 01/01/2016 or after 31/12/2017 (displayed in the datetimepicker calendar that's shown when the user selects the field in the form) are 'greyed out' and it is not possible to select them. This is exactly the functionality that I want, but I just want to change the values of the dates on which it is performed, so that at least the whole of the next calendar year is always selectable.
I have searched for maxDate & minDate variables within the forms.py file where the form with this field is defined, but can't find anything that looks like it is what's restricting which dates are available to be selected, and which are not selectable.
The view that's showing the page with this form on it is defined with:
def concept(request, project_id):
project = Project.objects.prefetch_related('budget_versions').get(id=project_id)
deposit = Deposit.objects.get_or_create(project=project)[0]
presentations = project.budget_versions.select_related('meeting').prefetch_related('budget_items', 'cci_items', 'presenters').filter(version_number__isnull=False).annotate(vn=F('version_number') * -1).order_by('presentation_date', 'created', '-vn')
end_details = EndDetails.objects.get_or_create(project=project)[0]
presentation_formset = BudgetPresentationFormset(prefix="presentations", instance=project, queryset=presentations)
drawing_formset = DrawingUploadFormset(prefix="drawings", queryset=Drawing.objects.filter(budget__in=presentations).order_by('budget__presentation_date', 'budget__created'))
context = {
'project': project,
'presentations': presentations,
'presentation_formset': presentation_formset,
'drawing_formset': drawing_formset,
'deposit_form': DepositInfoForm(instance=deposit),
'ended_form': EndDetailsForm(instance=end_details),
'budget_notes_form': BudgetNotesForm(instance=project.budget_overview),
}
The particular form I'm looking at is the presentations one, defined in forms.py:
class FirstPresentationForm(ValidatedForm):
""" UNUSED """
who_presenting = forms.CharField()
details = forms.CharField(required=False, label='Original option costed')
presentation_date = mDateTimeField(required=False, label="Presentation date", widget=forms.DateTimeInput(format='%d/%m/%Y %H:%M')) #ERF(19/12/2016 # 1210) Remove 'datetimepicker' from field..., attrs=({'class':'datetimepicker'})))
class Meta:
model = Budget
fields = ('presentation_date','who_presenting','details')
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', {})
project = instance.project
who_presenting = [pe.employee.id for pe in project.assigned.select_related('employee').filter(role=Role.P)]
#Make into an array with two employee IDs, or None. If two results were found, they will already be in the right format
if len(who_presenting)==1:
who_presenting.append(None)
elif not who_presenting:
who_presenting = None
if instance.presentation_date:
pres_meeting, created = Meeting.objects.get_or_create(project=project, purpose='6')
self.pres_meeting_id = pres_meeting.id
self.pres_meeting_creator = pres_meeting.event_creator or ''
if created:
pres_meeting.date = instance.presentation_date
pres_meeting.save()
initial = kwargs.get('initial', {})
initial={
'who_presenting': who_presenting,
}
kwargs['initial'] = initial
super(FirstPresentationForm, self).__init__(*args, **kwargs)
self.fields['who_presenting'] = AutoFlexiSelect(model='e', required=False, choices=get_choices('DESIGN_EMPLOYEE_CHOICES'), current_id=who_presenting, label="Who is presenting")
self.fields['presentation_date'].widget.attrs.update({'data-meeting-id': getattr(self,'pres_meeting_id', ''), 'data-meeting-creator': getattr(self,'pres_meeting_creator', '')})
def save(self, commit=True):
project = self.instance.project
data = self.cleaned_data
try: ProjectEmployee.objects.filter(project=project, role=Role.P).delete() #Delete current records, if any, to prevent build up on editing this field
except ObjectDoesNotExist: pass
if data['who_presenting']:
designers = data['who_presenting']
# Get the ids from the form field (received as a string)
designers = [re.sub('\D+', '', s) for s in designers.split(',')]
who_presenting_1 = Employee.objects.get(id=designers[0])
who_presenting_2 = designers[1] #If only one employee in selected choice, this will be None
if who_presenting_2: who_presenting_2 = Employee.objects.get(id=designers[1]) #If two employees in selected choice, sets the second one
pe = ProjectEmployee(project=project, employee=who_presenting_1, role=Role.P)
pe.save()
if who_presenting_2: #If a second designer, delete as well
pe = ProjectEmployee(project=project, employee=who_presenting_2, role=Role.P)
pe.save()
if 'presentation_date' in self.changed_data:
from events.models import refresh_group_cache
print '************'
pres_meeting = Meeting.objects.get(project=project, purpose='6')
self.instance.meeting = pres_meeting
self.instance.save()
print 'Updating date', data['presentation_date'], pres_meeting.id
pres_meeting.date = data['presentation_date']
pres_meeting.save()
refresh_group_cache(pres_meeting.event_creator.calendar_id, pres_meeting.date.year, pres_meeting.date.month, pres_meeting.event_id, pres_meeting)
return super(FirstPresentationForm, self).save(commit=commit)
I have tried passing/ setting max_date as an attribute of the presentation_date variable, but it doesn't appear to have an attribute/ property by this name...
How can I specify a particular date as this widget's maxDate, so that the user is able to select dates up until December of next year, rather than just up until December of this year?
Edit
The mDateTimeField that's using the widget is also defined in forms.py, with:
class mDateTimeField(forms.DateTimeField):
def __init__(self, *args, **kwargs):
kwargs.setdefault('input_formats', DATE_INPUT_FORMATS)
super(mDateTimeField, self).__init__(*args, **kwargs)

This should help you:
widgets = {
'start_date': forms.DateInput(attrs={
'class':'datepicker', 'data-min': YOUR_MIN_DATE,
'data-max': YOUR_MAX_DATE}),
}
Usage:
minDate: $(this).data('min'),
maxDate: $(this).data('max'),

Related

Verify a Django model field inside a Django model

I have a Django model called Attendance that has the clock in and clock in times of an employee along with the status of that entry, to see whether it's authorized or not. I then, am making another model called Payroll. I want this to check inside the Attendance entries to see all the Authorized entries and then do some action on them. How do I check all the status fields for all the entries in Attendance?
EDIT: Updated to better elaborate my question.
To better elaborate my question, this is how I've setup my Attendance model:
class CWorkAttendance(models.Model):
AUTO_ATT = "AU"
MANUAL_ATT = "MA"
WORK_ENTRY_TYPES = (
(AUTO_ATT, "Auto-Attendance"),
(MANUAL_ATT, "Manual-Attendance"),
)
AUTHORIZED = "AU"
UNAUTHORIZED = "UA"
WORK_ENTRY_STATUSES = (
(AUTHORIZED, "Athorized"),
(UNAUTHORIZED, "Un-Authorized"),
)
#Thank you motatoes
def face_locations_in(self, instance):
now = datetime.datetime.now()
return "attendance/{}/{}/in".format(instance.work_employee, now.strftime("%Y/%m/%d"))
def face_locations_out(self, instance):
now = datetime.datetime.now()
return "attendance/{}/{}/out".format(instance.work_employee, now.strftime("%Y/%m/%d"))
work_employee = models.ForeignKey('CEmployees', on_delete=models.CASCADE,)
work_start_time = models.DateTimeField()
work_end_time = models.DateTimeField(null=True)
work_duration = models.IntegerField(null=True)
work_entry_type = models.CharField(max_length=2,choices=WORK_ENTRY_TYPES)
work_entry_status = models.CharField(max_length=2, choices=WORK_ENTRY_STATUSES, default=WORK_ENTRY_STATUSES[1][0])
employee_face_captured_in = models.ImageField(upload_to=face_locations_in,)#////////
employee_face_captured_out = models.ImageField(upload_to=face_locations_out,)
If you look closely at the work_entry_status, it's a choice CharField that will contain the status of the entry (UNAUTHORIZED by default).
I want to create a Payroll model that will check for all the rows in the CWorkAttendance model and check their work_entry_status fields to see if they are Authorized, which is what I want to learn how to do.
If those fields are authorized, I want the grab the row's work_employee, work_duration and also some details from the original CEmployees row for the employee.
This is what I want my Payslip/Payroll model to look like:
class Payslip(models.Model):
GENERATED = "GEN"
CONFIRMED = "CON"
PAYSLIP_STATUS = (
(GENERATED, "Generated-UNSAVED"),
(CONFIRMED, "Confirmed-SAVED"),
)
payslip_number = models.IntegerField()#MM/YY/AUTO_GENERATED_NUMBER(AUTO_INCREMENT)
payslip_employee = models.ForeignKey('CEmployees', on_delete=models.CASCADE,)#Choose the employee from the master table CEmployees
payslip_generation_date = models.DateTimeField(default=datetime.datetime.now())#Date of the payroll generation
payslip_total_hours = models.IntegerField()#Total hours that the employee worked
payslip_from_date = models.DateField()"""The date from when the payslip will be made. The payslip will be manual for now, so generate it after choosing a a date to generate from."""
payslip_total_basic_seconds = models.IntegerField()#Total seconds the employee worked
payslip_total_ot_seconds = models.IntegerField()#Total overtime seconds the employee worked
payslip_basic_hourly_rate = models.IntegerField()#The basic hourly rate of the employee mentioned here. Take from the master employees table.
payslip_basic_ot_rate = models.IntegerField()#Taking the basic overtime rate from the master table
payslip_total_amount = models.FloatField()#The total amount of the payslip
payslip_entry_status = models.CharField(max_length=3, default=GENERATED)#The status of the pay slip.
Thanks,
Not sure if I understand your requirements well, so let me know if I misunderstood.
# `employee` is the work_employee in question
# if you don't want to filter by employee, remove `work_employee=employee`
attendances = CWorkAttendance.objects.filter(work_entry_status=CWorkAttendance.AUTHORIZED, work_employee=employee)
for attendances in attendances:
# do things with this attendance record
attendance.work_duration
attendance.employee
# ....
Update
Since you would like to do it manually, I would suggest having a separate view to generate the Payslip. The important thing is to know the date_from and the date_to for this payslip. I imagine that it is the managers who would have access to this view, so you would need the proper access controls set for it. I also think you need to have a payslip_to_date even if you are going to generate it until the current date, which will be useful for record keeping. I assume you have that column in the code below.
views.py:
from django.views import View
class GeneratePayslip(View):
"""
make sure you have the right access controls set to this view
"""
def post(self, request, **kwargs):
employee_id = kwags.POST.get("employee_id")
date_from = kwargs.POST.get("from_date")
date_to = kwargs.POST.get("to_date")
# we fetch all the objects within range
attendances = CWorkAttendance.objects.filter( \
work_entry_status=CWorkAttendance.AUTHORIZED, \
work_employee_id=employee_id, \
work_start_time__gte=date_from, \
work_end_time__lte=date_to \
)
hours = 0
for attendance in attendances:
# perform calculations to compute total sum and rate
pass
# create the payslip object here ..
# redirect to a success page and return
If you wanted to do it automatically later on, you may want to generate payslips automatically, once a month. For that you could use something like Celery to have periodic tasks that run in the background, for each employee. If this is the case you could move the above code to a file such as utils.py. you can create a method which takes employee_id, from_date, to_date, and then generate the payslip object, returning the payslip_id to the calling method

Passing a date to a queryset based on a button click in Django

I have RoastManifest View that querys date_scheduled (field from my RoastManifest model) and returns distinct values. I pass that queryset to a template and loop over the values and present them as buttons on the HTML file. I am able to click any button and pass the corresponding date into the URL,((localhost/schedule/2019-06-20) when I click on the June, 20 2019 button) thus redirecting me to my RoastManifestDetailView. Now I want to be able to filter my RoastManifestDetailView based only on the date passed to the URL (or which date button was clicked).
I have tried RoastManifest.obejects.filter(date_scheduled__date=date.today())just to see if I could return anything schedule for today but I keep getting Fielderrors (Unsupported lookup 'date' for DateField or join on the field not permitted.). Please note I know that is not the exact queryset for me. I wish to pass in a variable into the queryset.
This is the model:
(NOTE: roast_order is in there only to allow for use of adminsortable2 library)
class RoastManifest(models.Model):
def batch_number_incrementer():
current_max = RoastManifest.objects.order_by('-batch_number').first()
if current_max:
return current_max.batch_number + 1
else:
return 8000000
batch_number = models.IntegerField(unique=True,
default=batch_number_incrementer,
)
product = models.ForeignKey(Product, related_name="products",
on_delete=models.PROTECT)
date_scheduled = models.DateField()
roaster_profile = models.ForeignKey(RoasterProfile,
on_delete=models.PROTECT)
roast_order = models.PositiveSmallIntegerField(default=0, blank=False, null=False)
class Meta:
ordering = ('roast_order',)
This is how I pull the individual days scheduled:
class RoastManifestListView(ListView):
model = RoastManifest
template_name = 'schedule/scheduled_days.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['days'] = RoastManifest.objects.order_by('date_scheduled').values('date_scheduled').distinct()
return context
This is the view I am having trouble with:
class RoastManifestDetailView(TemplateView):
template_name = 'schedule/roastmanifest_list.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["roasts"] = RoastManifest.objects.get(date_scheduled__date=date.today())
return context
I want to be able to click on a day (button) and pass that date into the query, thus returning everything scheduled for that specific day.
With the above RoastManifestDetailView I am currently getting a FieldError
Looking at the models above, I believe the correct query would be: RoastManifest.obejects.filter(date_scheduled=date.today())... your query was looking for a field called date on the scheduled_date field, but there is no such property.
When you are ready to query based on the clicked button, there are several ways to do it but the easiest (and "correct-ish-est" way) would be to pass the date in the URL as a query param: <button href="<URL to route>?date={item.scheduled_date}" ...> or something like that (you will probably have to play with date formats a bit to in your settings but you want something like 2019-06-21), and in your view you can get the value of that param using: date = request.GET.get('date', None) which will return the string value of the date query param which you can then use in queries and things (again possibly after some conversion) or None if there is no parameter with that name.

Add a text field to a form- populate it with a 'default value' using Python/Django

I am working on a Python/Django project, and one of the fields on a form is currently broken (it's a datetimepicker field- the issue is that you can't select dates beyond 01/01/2017). It seems this is because the library being used to add the datetimepicker is no longer supported).
I will look into implementing a datetimepicker myself, but need to allow the user to select dates beyond the 01/01/2017 on this form- in order to save the dates and times for scheduled meetings, etc.
My thoughts for the moment are to create a temporary fix, by adding simple EasyText fields to the form for the date/ time. I am aware that this means that the data entered will not be validated, so there is room for user input error, but it will enable the user to set dates/ times for meeting at least temporarily, until I fix the issue with the current datetimepicker field.
The form that I want to add these fields to is defined in forms.py with:
class PostDepMeetingForm(ValidatedForm):
postdep_meeting_date = MoonDateTimeField(required=False, widget=forms.DateTimeInput(format='%d/%m/%Y %H:%M', attrs=({'class':'datetimepicker'})))
planning_who = forms.CharField(required=False)
general_notes = EasyText(label='Survey notes')
#ERF(16/12/2016 # 1345) Add a text area for the meeting date & time
meeting_date_time = EasyText(label='Meeting date & time')
class Meta:
model = Survey
fields = ('postdep_meeting_date', 'planning_who','building_regs','general_notes', 'meeting_date_time')
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', {})
project = instance.project
try:
postdep_meeting = project.meetings.get(purpose=3)
self.postdep_meeting_id = postdep_meeting.id
self.postdep_meeting_creator = postdep_meeting.event_creator or ''
postdep_meeting_date = postdep_meeting.date
general_notes = postdep_meeting.notes
#ERF(16/12/2016 # 1420) Try setting the meeting_date_time EasyText's default value to postdep_meeting.date & postdep_meeting.time
#meeting_date_time
except ObjectDoesNotExist:
postdep_meeting_date = None
general_notes = None
try: planning_who = project.assigned.select_related('employee').get(role=Role.PL).employee.id
except ObjectDoesNotExist: planning_who = None
initial = kwargs.get('initial', {})
initial={
'postdep_meeting_date': postdep_meeting_date,
'general_notes': general_notes,
'planning_who': planning_who,
}
kwargs['initial'] = initial
super(PostDepMeetingForm, self).__init__(*args, **kwargs)
self.fields['planning_who'] = AutoFlexiSelect(model='e', choices=get_choices('ARCHITECT_CHOICES') + [('*', 'Other')], label="Who", current_id=planning_who,)
self.fields['postdep_meeting_date'].widget.attrs.update({'data-meeting-id': getattr(self,'postdep_meeting_id', ''), 'data-meeting-creator': getattr(self,'postdep_meeting_id', '')})
def save(self, commit=True):
project = self.instance.project
data = self.cleaned_data
if 'postdep_meeting_date' in self.changed_data:
postdep_meeting = Meeting.objects.get_or_create(project=project, purpose=3)[0]
postdep_meeting.date = data['postdep_meeting_date']
postdep_meeting.notes = data['general_notes']
postdep_meeting.save()
if data['planning_who']:
try: project.assigned.select_related('employee').get(role=Role.PL).delete()
except ObjectDoesNotExist: pass
pe = ProjectEmployee.objects.create(role=Role.PL, project=project, employee_id=data['planning_who'])
return super(PostDepMeetingForm, self).save(commit=commit)
I have added the EasyText field, called meeting_date_time to the form, and can see it displayed on the form when I view the page in the browser.
What I'm unsure about how to do now- is save any input entered by the user, and have whatever they have input into that field displayed there automatically the next time they browse to that page... Will I need to add a new field to the project model to save this information?
Edit
As requested in a comment, ValidatedForm is defined with:
class ValidatedForm(forms.ModelForm):
error_css_class = 'error-field'
required_css_class = 'required'
def __init__(self, *args, **kwargs):
super(ValidatedForm, self).__init__(*args, **kwargs)
# for k, field in self.fields.items():
# if 'required' in field.error_messages:
# # print 'field error', field, field
# # field.widget.attrs['class'] = 'error-field'
# print 'field error', k
# field.error_messages['required'] = '*'
# else:
# print 'Field erroR', field
and EasyText is a 'text area' for storing user input on the form, defined with:
def EasyText(field_class="medium", readonly=False, placeholder='', extras={}, **kwargs):
textarea = forms.CharField(widget=TextareaWidget(attrs={'class':field_class, 'readonly':readonly, 'placeholder': placeholder}))
if extras:
textarea.widget.attrs.update(**extras)
try: textarea.label = kwargs['label']
except KeyError: pass
try: textarea.required = kwargs['required']
except KeyError: textarea.required = False
return textarea

Django MultipleChoiceField choices of users throws error

I am trying to write a form that allows the user to select as many users from a specific group as they want. However when I try to use the list of users as an option I get an error saying that 'User' object does not support indexing.
Its a fairly standard form, the main difference is that the group is filtered based on a kwarg passed to the form. The form is passed a project_id (project object primary key) and it then finds the group associated with that project and generates the field.
From forms.py
class ModifyTeamForm(forms.Form):
action = ChoiceField(choices=[('remove', 'Remove users'), ('promote', 'Promote to lead.')])
def __init__(self, *args, **kwargs):
# The project to get the team for
project_id = kwargs.pop('project_id', None)
super(ModifyTeamForm, self).__init__(*args, **kwargs)
project = Project.objects.get(pk=project_id)
# Team for this project
team = User.objects.filter(groups__name=project.project_name)
# Create a form field to select current team members
current_team = MultipleChoiceField(required=True, choices = team, widget=CheckboxSelectMultiple)
# Add the field
self.fields['current_team'] = current_team
My views.py
#login_required
def team(request, project_id):
if request.method == "POST":
# Not yet implemented
return
else:
form = ModifyTeamForm(project_id=project_id)
template = loader.get_template('projects/team.html')
context = RequestContext(request, {
'form': form,
})
return HttpResponse(template.render(context))
It's because MultipleChoiceField.choices is expected to be a 2d Array effectively (https://docs.djangoproject.com/en/1.7/ref/forms/fields/#django.forms.ChoiceField.choices).
So you could do something like this:
team = [(u.pk, u.email) for u in User.objects.filter(groups__name=project.project_name)]
And that will return you a list continaing the combintation of
[('user1.pk', 'user1.email'), ('user2.pk', 'user2.email'),...]
which will be useable as the choices.

django: exclude certain form elements based on a condition

I have some form fields that I want to include/exclude based on whether or not a certain condition is met. I know how to include and exclude form elements, but I am having difficulty doing it when I want it elements to show based on the outcome of a function.
Here is my form:
class ProfileForm(ModelForm):
# this_team = get Team instance from team.id passed in
# how?
def draft_unlocked(self):
teams = Team.objects.order_by('total_points')
count = 0
for team in teams:
if team.pk == this_team.pk:
break
count += 1
now = datetime.datetime.now().weekday()
if now >= count:
# show driver_one, driver_two, driver_three
else:
# do not show driver_one, driver_two, driver_three
class Meta:
model = Team
What I am trying to accomplish is, based on the standings of total points, a team should not be able to change their driver until their specified day. As in, the last team in the standings can add/drop a driver on Monday, second to last team can add/drop on Tuesday, and so on...
So the first problem -- how do I get the Team instance inside the form itself from the id that was passed in. And, how do I include/exclude based on the result of draft_unlocked().
Or perhaps there is a better way to do all of this?
Thanks a lot everyone.
This is actually fairly straightforward (conditional field settings) - here's a quick example:
from django.forms import Modelform
from django.forms.widgets import HiddenInput
class SomeForm(ModelForm):
def __init__(self, *args, **kwargs):
# call constructor to set up the fields. If you don't do this
# first you can't modify fields.
super(SomeForm, self).__init__(*args, **kwargs)
try:
# make somefunc return something True
# if you can change the driver.
# might make sense in a model?
can_change_driver = self.instance.somefunc()
except AttributeError:
# unbound form, what do you want to do here?
can_change_driver = True # for example?
# if the driver can't be changed, use a input=hidden
# input field.
if not can_change_driver:
self.fields["Drivers"].widget = HiddenInput()
class Meta:
model = SomeModel
So, key points from this:
self.instance represents the bound object, if the form is bound. I believe it is passed in as a named argument, therefore in kwargs, which the parent constructor uses to create self.instance.
You can modify the field properties after you've called the parent constructor.
widgets are how forms are displayed. HiddenInput basically means <input type="hidden" .../>.
There is one limitation; I can tamper with the input to change a value if I modify the submitted POST/GET data. If you don't want this to happen, something to consider is overriding the form's validation (clean()) method. Remember, everything in Django is just objects, which means you can actually modify class objects and add data to them at random (it won't be persisted though). So in your __init__ you could:
self.instance.olddrivers = instance.drivers.all()
Then in your clean method for said form:
def clean(self):
# validate parent. Do this first because this method
# will transform field values into model field values.
# i.e. instance will reflect the form changes.
super(SomeForm, self).clean()
# can we modify drivers?
can_change_driver = self.instance.somefunc()
# either we can change the driver, or if not, we require
# that the two lists are, when sorted, equal (to allow for
# potential non equal ordering of identical elements).
# Wrapped code here for niceness
if (can_change_driver or
(sorted(self.instance.drivers.all()) ==
sorted(self.instance.olddrivers))):
return True
else:
raise ValidationError() # customise this to your liking.
You can do what you need by adding your own init where you can pass in the id when you instantiate the form class:
class ProfileForm(ModelForm):
def __init__(self, team_id, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
this_team = Team.objects.get(pk=team_id)
teams = Team.objects.order_by('total_points')
count = 0
for team in teams:
if team.pk == this_team.pk:
break
count += 1
now = datetime.datetime.now().weekday()
if now >= count:
# show driver_one, driver_two, driver_three
else:
# do not show driver_one, driver_two, driver_three
class Meta:
model = Team
#views.py
def my_view(request, team_id):
profile_form = ProfileForm(team_id, request.POST or None)
#more code here
Hope that helps you out.

Categories

Resources