616 lines
21 KiB
Python
616 lines
21 KiB
Python
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.db.models import Count, Case, When, IntegerField, Sum, F, DecimalField
|
|
from django.db.models.functions import Coalesce
|
|
from django.http import JsonResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponse
|
|
from django.core.paginator import Paginator
|
|
from django.forms import modelformset_factory
|
|
from .models import Tenant, Voter, Interaction, Event, EventParticipation, Volunteer, VolunteerEvent, CampaignSettings, ParticipationStatus, VoterLikelihood, ScheduledCall, VolunteerRole, EventType, TenantUserRole
|
|
from .forms import VoterForm, EventForm, VolunteerForm, ScheduledCallForm, InteractionForm
|
|
from datetime import datetime, date, time, timedelta
|
|
from django.utils import timezone
|
|
import pytz
|
|
import re
|
|
from django.conf import settings
|
|
from django.template.defaultfilters import date as date_filter
|
|
import csv
|
|
|
|
# Import necessary modules for Twilio
|
|
from twilio.rest import Client
|
|
from twilio.base.exceptions import TwilioRestException
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Helper function to get the current tenant
|
|
def get_current_tenant(request):
|
|
if request.user.is_authenticated:
|
|
tenant_role = TenantUserRole.objects.filter(user=request.user).first()
|
|
if tenant_role:
|
|
return tenant_role.tenant
|
|
return None
|
|
|
|
def get_tenant_campaign_settings(tenant):
|
|
if tenant:
|
|
try:
|
|
return CampaignSettings.objects.get(tenant=tenant)
|
|
except CampaignSettings.DoesNotExist:
|
|
pass
|
|
return None
|
|
|
|
@login_required
|
|
def dashboard(request):
|
|
user_tenants = Tenant.objects.filter(user_roles__user=request.user)
|
|
|
|
if not user_tenants.exists():
|
|
return redirect('admin:index')
|
|
|
|
selected_tenant_id = request.session.get('selected_tenant_id')
|
|
|
|
if selected_tenant_id:
|
|
selected_tenant = get_object_or_404(Tenant, pk=selected_tenant_id)
|
|
if selected_tenant not in user_tenants:
|
|
# If the selected tenant is not among the user's tenants, reset session and show selection
|
|
del request.session['selected_tenant_id']
|
|
return redirect('dashboard')
|
|
else:
|
|
# If no tenant is selected, and there's only one available, select it automatically
|
|
if user_tenants.count() == 1:
|
|
selected_tenant = user_tenants.first()
|
|
request.session['selected_tenant_id'] = selected_tenant.id
|
|
else:
|
|
# Otherwise, prompt the user to select a tenant
|
|
return render(request, 'core/index.html', {'tenants': user_tenants, 'selected_tenant': None})
|
|
|
|
campaign_settings = get_tenant_campaign_settings(selected_tenant)
|
|
|
|
# Total Voters
|
|
total_voters = Voter.objects.filter(tenant=selected_tenant).count()
|
|
|
|
# Total Interactions
|
|
total_interactions = Interaction.objects.filter(voter__tenant=selected_tenant).count()
|
|
|
|
return render(request, 'core/index.html', {
|
|
'total_voters': total_voters,
|
|
'total_interactions': total_interactions,
|
|
'campaign_settings': campaign_settings,
|
|
'selected_tenant': selected_tenant,
|
|
'tenants': user_tenants, # Pass all tenants for potential switching
|
|
})
|
|
|
|
@login_required
|
|
def select_campaign(request, tenant_id):
|
|
# Ensure the tenant exists and the user has access to it
|
|
tenant = get_object_or_404(Tenant, pk=tenant_id, user_roles__user=request.user)
|
|
request.session['selected_tenant_id'] = tenant.id
|
|
return redirect('dashboard')
|
|
|
|
# Placeholder views for other functions
|
|
@login_required
|
|
def voter_list(request):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
voters = Voter.objects.filter(tenant=tenant)
|
|
return render(request, 'core/voter_list.html', {'voters': voters})
|
|
|
|
@login_required
|
|
def voter_detail(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
voter = get_object_or_404(Voter, pk=pk, tenant=tenant)
|
|
return render(request, 'core/voter_detail.html', {'voter': voter})
|
|
|
|
@login_required
|
|
def voter_create(request):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
if request.method == 'POST':
|
|
form = VoterForm(request.POST)
|
|
if form.is_valid():
|
|
voter = form.save(commit=False)
|
|
voter.tenant = tenant
|
|
voter.save()
|
|
return redirect('voter_detail', pk=voter.pk)
|
|
else:
|
|
form = VoterForm()
|
|
return render(request, 'core/voter_form.html', {'form': form})
|
|
|
|
@login_required
|
|
def voter_update(request, voter_id):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
voter = get_object_or_404(Voter, pk=voter_id, tenant=tenant)
|
|
if request.method == 'POST':
|
|
form = VoterForm(request.POST, instance=voter)
|
|
if form.is_valid():
|
|
form.save()
|
|
return redirect('voter_detail', pk=voter.pk)
|
|
else:
|
|
form = VoterForm(instance=voter)
|
|
return render(request, 'core/voter_form.html', {'form': form, 'voter': voter})
|
|
|
|
@login_required
|
|
def voter_delete(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
voter = get_object_or_404(Voter, pk=pk, tenant=tenant)
|
|
if request.method == 'POST':
|
|
voter.delete()
|
|
return redirect('voter_list')
|
|
return render(request, 'core/voter_confirm_delete.html', {'voter': voter})
|
|
|
|
@login_required
|
|
def interaction_list(request):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
interactions = Interaction.objects.filter(voter__tenant=tenant)
|
|
return render(request, 'core/interaction_list.html', {'interactions': interactions})
|
|
|
|
@login_required
|
|
def interaction_detail(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
interaction = get_object_or_404(Interaction, pk=pk, voter__tenant=tenant)
|
|
return render(request, 'core/interaction_detail.html', {'interaction': interaction})
|
|
|
|
@login_required
|
|
def interaction_create(request, voter_id):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
voter = get_object_or_404(Voter, pk=voter_id, tenant=tenant)
|
|
if request.method == 'POST':
|
|
form = InteractionForm(request.POST)
|
|
if form.is_valid():
|
|
interaction = form.save(commit=False)
|
|
interaction.voter = voter
|
|
# Assign volunteer if relevant or leave null
|
|
interaction.save()
|
|
return redirect('voter_detail', pk=voter.pk)
|
|
else:
|
|
form = InteractionForm()
|
|
return render(request, 'core/interaction_form.html', {'form': form, 'voter': voter})
|
|
|
|
@login_required
|
|
def interaction_update(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
interaction = get_object_or_404(Interaction, pk=pk, voter__tenant=tenant)
|
|
if request.method == 'POST':
|
|
form = InteractionForm(request.POST, instance=interaction)
|
|
if form.is_valid():
|
|
form.save()
|
|
return redirect('voter_detail', pk=interaction.voter.pk)
|
|
else:
|
|
form = InteractionForm(instance=interaction)
|
|
return render(request, 'core/interaction_form.html', {'form': form, 'interaction': interaction})
|
|
|
|
@login_required
|
|
def interaction_delete(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
interaction = get_object_or_404(Interaction, pk=pk, voter__tenant=tenant)
|
|
if request.method == 'POST':
|
|
interaction.delete()
|
|
return redirect('voter_detail', pk=interaction.voter.pk)
|
|
return render(request, 'core/interaction_confirm_delete.html', {'interaction': interaction})
|
|
|
|
|
|
@login_required
|
|
def donation_list(request):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
donations = Interaction.objects.filter(voter__tenant=tenant)
|
|
return render(request, 'core/donation_list.html', {'donations': donations})
|
|
|
|
@login_required
|
|
def donation_detail(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
donation = get_object_or_404(Interaction, pk=pk, voter__tenant=tenant)
|
|
return render(request, 'core/donation_detail.html', {'donation': donation})
|
|
|
|
@login_required
|
|
def donation_create(request, voter_pk):
|
|
return HttpResponse("Placeholder for donation_create")
|
|
|
|
@login_required
|
|
def donation_update(request, voter_pk, pk):
|
|
return HttpResponse("Placeholder for donation_update")
|
|
|
|
@login_required
|
|
def donation_delete(request, voter_pk, pk):
|
|
return HttpResponse("Placeholder for donation_delete")
|
|
|
|
@login_required
|
|
def likelihood_create(request, voter_pk):
|
|
return HttpResponse("Placeholder for likelihood_create")
|
|
|
|
@login_required
|
|
def likelihood_update(request, voter_pk, pk):
|
|
return HttpResponse("Placeholder for likelihood_update")
|
|
|
|
@login_required
|
|
def likelihood_delete(request, voter_pk, pk):
|
|
return HttpResponse("Placeholder for likelihood_delete")
|
|
|
|
@login_required
|
|
def event_list(request):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
events = Event.objects.filter(tenant=tenant)
|
|
return render(request, 'core/event_list.html', {'events': events})
|
|
|
|
@login_required
|
|
def event_detail(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
event = get_object_or_404(Event, pk=pk, tenant=tenant)
|
|
return render(request, 'core/event_detail.html', {'event': event})
|
|
|
|
@login_required
|
|
def event_create(request):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
if request.method == 'POST':
|
|
form = EventForm(request.POST)
|
|
if form.is_valid():
|
|
event = form.save(commit=False)
|
|
event.tenant = tenant
|
|
event.save()
|
|
return redirect('event_detail', pk=event.pk)
|
|
else:
|
|
form = EventForm()
|
|
return render(request, 'core/event_form.html', {'form': form})
|
|
|
|
@login_required
|
|
def event_update(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
event = get_object_or_404(Event, pk=pk, tenant=tenant)
|
|
if request.method == 'POST':
|
|
form = EventForm(request.POST, instance=event)
|
|
if form.is_valid():
|
|
form.save()
|
|
return redirect('event_detail', pk=event.pk)
|
|
else:
|
|
form = EventForm(instance=event)
|
|
return render(request, 'core/event_form.html', {'form': form, 'event': event})
|
|
|
|
@login_required
|
|
def event_delete(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
event = get_object_or_404(Event, pk=pk, tenant=tenant)
|
|
if request.method == 'POST':
|
|
event.delete()
|
|
return redirect('event_list')
|
|
return render(request, 'core/event_confirm_delete.html', {'event': event})
|
|
|
|
@login_required
|
|
def event_add_participant(request, pk):
|
|
return HttpResponse("Placeholder for event_add_participant")
|
|
|
|
@login_required
|
|
def event_edit_participant(request, event_pk, pk):
|
|
return HttpResponse("Placeholder for event_edit_participant")
|
|
|
|
@login_required
|
|
def event_delete_participant(request, event_pk, pk):
|
|
return HttpResponse("Placeholder for event_delete_participant")
|
|
|
|
@login_required
|
|
def event_participation_create(request, voter_id):
|
|
return HttpResponse("Placeholder for event_participation_create")
|
|
|
|
@login_required
|
|
def event_participation_update(request, pk):
|
|
return HttpResponse("Placeholder for event_participation_update")
|
|
|
|
@login_required
|
|
def event_participation_delete(request, pk):
|
|
return HttpResponse("Placeholder for event_participation_delete")
|
|
|
|
@login_required
|
|
def volunteer_list(request):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
volunteers = Volunteer.objects.filter(tenant=tenant)
|
|
return render(request, 'core/volunteer_list.html', {'volunteers': volunteers})
|
|
|
|
@login_required
|
|
def volunteer_detail(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
volunteer = get_object_or_404(Volunteer, pk=pk, tenant=tenant)
|
|
return render(request, 'core/volunteer_detail.html', {'volunteer': volunteer})
|
|
|
|
@login_required
|
|
def volunteer_create(request):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
if request.method == 'POST':
|
|
form = VolunteerForm(request.POST)
|
|
if form.is_valid():
|
|
volunteer = form.save(commit=False)
|
|
volunteer.tenant = tenant
|
|
volunteer.save()
|
|
return redirect('volunteer_detail', pk=volunteer.pk)
|
|
else:
|
|
form = VolunteerForm()
|
|
return render(request, 'core/volunteer_form.html', {'form': form})
|
|
|
|
@login_required
|
|
def volunteer_edit(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
volunteer = get_object_or_404(Volunteer, pk=pk, tenant=tenant)
|
|
if request.method == 'POST':
|
|
form = VolunteerForm(request.POST, instance=volunteer)
|
|
if form.is_valid():
|
|
form.save()
|
|
return redirect('volunteer_detail', pk=volunteer.pk)
|
|
else:
|
|
form = VolunteerForm(instance=volunteer)
|
|
return render(request, 'core/volunteer_form.html', {'form': form, 'voter': volunteer.pk})
|
|
|
|
@login_required
|
|
def volunteer_delete(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
volunteer = get_object_or_404(Volunteer, pk=pk, tenant=tenant)
|
|
if request.method == 'POST':
|
|
volunteer.delete()
|
|
return redirect('volunteer_list')
|
|
return render(request, 'core/volunteer_confirm_delete.html', {'volunteer': volunteer})
|
|
|
|
@login_required
|
|
def voter_geocode(request, voter_pk):
|
|
return HttpResponse("Placeholder for voter_geocode")
|
|
|
|
@login_required
|
|
def export_voters_csv(request):
|
|
return HttpResponse("Placeholder for export_voters_csv")
|
|
|
|
@login_required
|
|
def create_scheduled_call(request, voter_id):
|
|
return HttpResponse("Placeholder for create_scheduled_call")
|
|
|
|
@login_required
|
|
def scheduled_call_list(request):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
scheduled_calls = ScheduledCall.objects.filter(tenant=tenant)
|
|
return render(request, 'core/scheduled_call_list.html', {'scheduled_calls': scheduled_calls})
|
|
|
|
@login_required
|
|
def scheduled_call_detail(request, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
scheduled_call = get_object_or_404(ScheduledCall, pk=pk, tenant=tenant)
|
|
return render(request, 'core/scheduled_call_detail.html', {'scheduled_call': scheduled_call})
|
|
|
|
@login_required
|
|
def scheduled_call_create(request, voter_pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
voter = get_object_or_404(Voter, pk=voter_pk, tenant=tenant)
|
|
if request.method == 'POST':
|
|
form = ScheduledCallForm(request.POST)
|
|
if form.is_valid():
|
|
scheduled_call = form.save(commit=False)
|
|
scheduled_call.voter = voter
|
|
scheduled_call.tenant = tenant
|
|
scheduled_call.save()
|
|
return redirect('scheduled_call_detail', pk=scheduled_call.pk)
|
|
else:
|
|
form = ScheduledCallForm(initial={'voter': voter})
|
|
return render(request, 'core/scheduled_call_form.html', {'form': form, 'voter': voter})
|
|
|
|
@login_required
|
|
def scheduled_call_edit(request, voter_pk, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
scheduled_call = get_object_or_404(ScheduledCall, pk=pk, voter__pk=voter_pk, tenant=tenant)
|
|
if request.method == 'POST':
|
|
form = ScheduledCallForm(request.POST, instance=scheduled_call)
|
|
if form.is_valid():
|
|
form.save()
|
|
return redirect('scheduled_call_detail', pk=scheduled_call.pk)
|
|
else:
|
|
form = ScheduledCallForm(instance=scheduled_call)
|
|
return render(request, 'core/scheduled_call_form.html', {'form': form, 'voter': scheduled_call.voter})
|
|
|
|
@login_required
|
|
def scheduled_call_delete(request, voter_pk, pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
scheduled_call = get_object_or_404(ScheduledCall, pk=pk, voter__pk=voter_pk, tenant=tenant)
|
|
if request.method == 'POST':
|
|
scheduled_call.delete()
|
|
return redirect('scheduled_call_list')
|
|
return render(request, 'core/scheduled_call_confirm_delete.html', {'scheduled_call': scheduled_call})
|
|
|
|
@login_required
|
|
def bulk_schedule_calls(request):
|
|
return HttpResponse("Placeholder for bulk_schedule_calls")
|
|
|
|
@login_required
|
|
def voter_search_json(request):
|
|
return HttpResponse("Placeholder for voter_search_json")
|
|
|
|
@login_required
|
|
def import_participants(request):
|
|
return HttpResponse("Placeholder for import_participants")
|
|
|
|
@login_required
|
|
def import_participants_map_fields(request):
|
|
return HttpResponse("Placeholder for import_participants_map_fields")
|
|
|
|
@login_required
|
|
def process_participants_import(request):
|
|
return HttpResponse("Placeholder for process_participants_import")
|
|
|
|
@login_required
|
|
def match_participants(request):
|
|
return HttpResponse("Placeholder for match_participants")
|
|
|
|
@login_required
|
|
def interest_add(request):
|
|
return HttpResponse("Placeholder for interest_add")
|
|
|
|
@login_required
|
|
def interest_delete(request):
|
|
return HttpResponse("Placeholder for interest_delete")
|
|
|
|
@login_required
|
|
def volunteer_add(request):
|
|
return HttpResponse("Placeholder for volunteer_add")
|
|
|
|
@login_required
|
|
def volunteer_assign_event(request, volunteer_pk):
|
|
return HttpResponse("Placeholder for volunteer_assign_event")
|
|
|
|
@login_required
|
|
def volunteer_remove_event(request, volunteer_pk, event_pk):
|
|
return HttpResponse("Placeholder for volunteer_remove_event")
|
|
|
|
@login_required
|
|
def volunteer_search_json(request):
|
|
return HttpResponse("Placeholder for volunteer_search_json")
|
|
|
|
@login_required
|
|
def bulk_send_sms(request):
|
|
return HttpResponse("Placeholder for bulk_send_sms")
|
|
|
|
@login_required
|
|
def create_interaction_for_voter(request, voter_pk):
|
|
return HttpResponse("Placeholder for create_interaction_for_voter")
|
|
|
|
@login_required
|
|
def complete_call(request, call_pk):
|
|
return HttpResponse("Placeholder for complete_call")
|
|
|
|
@login_required
|
|
def door_visits(request):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
# Get voters for the current tenant that have door_visit set to True
|
|
door_to_door_voters = Voter.objects.filter(tenant=tenant, door_visit=True)
|
|
|
|
# Dictionary to store visited households with the latest visit date
|
|
visited_households = {}
|
|
|
|
for voter in door_to_door_voters:
|
|
# Construct a unique key for the household (e.g., street address and city)
|
|
household_key = f"{voter.address_street.lower().strip()}-{voter.city.lower().strip()}"
|
|
|
|
# Find the latest interaction for this voter
|
|
latest_interaction = Interaction.objects.filter(voter=voter).order_by('-date').first()
|
|
|
|
# Update visited_households with the latest interaction date for the household
|
|
if household_key not in visited_households or (latest_interaction and latest_interaction.date > visited_households[household_key]['last_visit_date']):
|
|
visited_households[household_key] = {
|
|
'voter': voter,
|
|
'last_visit_date': latest_interaction.date if latest_interaction else None,
|
|
'voters_in_household': []
|
|
}
|
|
|
|
# Ensure 'last_visit_date' is always present in the dictionary for comparison
|
|
if 'last_visit_date' not in visited_households[household_key]:
|
|
visited_households[household_key]['last_visit_date'] = None
|
|
|
|
visited_households[household_key]['voters_in_household'].append(voter)
|
|
|
|
# Sort households by the last visit date, with None dates appearing last
|
|
sorted_households = sorted(visited_households.values(), key=lambda x: x['last_visit_date'] if x['last_visit_date'] is not None else datetime.min.replace(tzinfo=pytz.UTC), reverse=True)
|
|
|
|
# Render the door_visits.html template with the sorted household data
|
|
return render(request, 'core/door_visits.html', {'households': sorted_households})
|
|
|
|
@login_required
|
|
def door_visit_history(request, voter_pk):
|
|
tenant = get_current_tenant(request)
|
|
if not tenant:
|
|
return redirect('admin:index')
|
|
|
|
voter = get_object_or_404(Voter, pk=voter_pk, tenant=tenant)
|
|
interactions = Interaction.objects.filter(voter=voter).order_by('-date')
|
|
|
|
context = {
|
|
'voter': voter,
|
|
'interactions': interactions
|
|
}
|
|
return render(request, 'core/door_visit_history.html', context)
|
|
|
|
@login_required
|
|
def voter_advanced_search(request):
|
|
return HttpResponse("Placeholder for voter_advanced_search")
|
|
|
|
@login_required
|
|
def volunteer_event_create(request, event_id):
|
|
return HttpResponse("Placeholder for volunteer_event_create")
|
|
|
|
@login_required
|
|
def volunteer_event_delete(request, pk):
|
|
return HttpResponse("Placeholder for volunteer_event_delete")
|
|
|
|
@login_required
|
|
def call_queue(request):
|
|
return HttpResponse("Placeholder for call_queue")
|
|
|
|
@login_required
|
|
def profile(request):
|
|
return HttpResponse("Placeholder for profile")
|
|
|
|
@login_required
|
|
def volunteer_bulk_send_sms(request):
|
|
return HttpResponse("Placeholder for volunteer_bulk_send_sms") |