Skip to content

Commit

Permalink
Add Caver model and CRUD views; update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
anorthall committed Nov 2, 2023
1 parent 53c0033 commit d11c7e3
Show file tree
Hide file tree
Showing 27 changed files with 681 additions and 75 deletions.
8 changes: 7 additions & 1 deletion app/import/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ def clean_file(self) -> list[dict]:


class TripImportForm(BaseTripForm):
cavers = forms.CharField(
max_length=255,
label="Cavers",
help_text="A comma separated list of cavers.",
required=False,
)

class Meta:
model = Trip
fields = [
Expand All @@ -80,7 +87,6 @@ class Meta:
"privacy",
"clubs",
"expedition",
"cavers",
"horizontal_dist",
"vert_dist_down",
"vert_dist_up",
Expand Down
2 changes: 1 addition & 1 deletion app/import/tests/test_csvs/valid_trips.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Cave name,Cave entrance,Cave exit,Cave region,Cave country,Start date/time,End date/time,Type,Privacy,Clubs,Expedition,Cavers,Horizontal dist,Vertical dist down,Vertical dist up,Surveyed dist,Resurveyed dist,Aid dist,Notes
Trip 1, Lancaster Hole, County Pot, Yorkshire Dales, England,2022-06-17 12:00,2022-06-17 18:00,Sport,Default,NYMCC,,Andrew Northall,500m,25m,25m,,,,,
Trip 1, Lancaster Hole, County Pot, Yorkshire Dales, England,2022-06-17 12:00,2022-06-17 18:00,Sport,Default,NYMCC,,"Andrew Northall, John Smith",500m,25m,25m,,,,,
Trip 2, Lancaster Hole, County Pot, Yorkshire Dales, England,2022-06-20 4pm,2022-06-20 8pm,Sport,,NYMCC,,Andrew Northall,500m,25m,25m,,,,,
Trip 3, Ireby Fell Cavern, Ireby Fell Cavern, Yorkshire Dales, England,2022-06-20 4pm,2022-06-20 8pm,Sport,,NYMCC,,Andrew Northall,500m,25m,25m,,,,,
3 changes: 2 additions & 1 deletion app/import/tests/test_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ def test_save_with_valid_data(self):
self.assertEqual(
trip.end, datetime(2022, 6, 17, 17, 0, 0, tzinfo=ZoneInfo("UTC"))
)
self.assertEqual(trip.cavers, "Andrew Northall")
self.assertEqual(trip.cavers.all()[0].name, "Andrew Northall")
self.assertEqual(trip.cavers.all()[1].name, "John Smith")

trip = Trip.objects.get(cave_name="Trip 2")
self.assertEqual(trip.privacy, Trip.DEFAULT)
Expand Down
12 changes: 12 additions & 0 deletions app/import/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.utils.safestring import SafeString
from django.views.generic import FormView, View
from django_ratelimit.decorators import ratelimit
from logger.models import Caver

from . import services
from .forms import ImportUploadForm, TripImportFormset, TripImportFormsetHelper
Expand Down Expand Up @@ -76,8 +77,19 @@ def post(self, request, *args, **kwargs):
trip = form.save(commit=False)
trip.user = request.user
trip.save()

trip.followers.add(request.user)

cavers = form.cleaned_data["cavers"]
cavers = cavers.split(",")
for caver in cavers:
caver = caver.strip()
if caver:
caver_obj, _ = Caver.objects.get_or_create(
name=caver, user=request.user
)
trip.cavers.add(caver_obj)

# noinspection PyUnboundLocalVariable
messages.success(request, f"Successfully imported {count} trips!")
log_user_action(request.user, f"imported {count} trips from a CSV file")
Expand Down
28 changes: 19 additions & 9 deletions app/logger/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from factory.django import DjangoModelFactory
from faker import Faker

from .models import Trip
from .models import Caver, Trip

fake = Faker()

Expand All @@ -21,13 +21,6 @@
MAX_SHORT_TRIP_LENGTH_IN_MINUTES = MAX_SHORT_TRIP_LENGTH_IN_HOURS * 60


def _get_caver_list():
names_list = []
for _ in range(random.randgen.randint(0, 5)):
names_list.append(fake.name())
return ", ".join(names_list)


def _generate_distance(min, max, chance_of_none=0.2):
if (chance_of_none * 100) > random.randgen.randint(1, 100):
return None
Expand Down Expand Up @@ -166,6 +159,13 @@ def generate_club():
return f"{city} {middle} {suffix}"


class CaverFactory(DjangoModelFactory):
class Meta:
model = Caver

name = factory.Faker("name")


class TripFactory(DjangoModelFactory):
class Meta:
model = Trip
Expand Down Expand Up @@ -205,7 +205,6 @@ class Meta:
start_date=datetime.now() - timedelta(days=365 * 5),
)
clubs = factory.LazyFunction(generate_club)
cavers = factory.LazyFunction(_get_caver_list)
expedition = factory.LazyFunction(_generate_expedition)
privacy = factory.Faker(
"random_element",
Expand Down Expand Up @@ -257,6 +256,17 @@ def vert_dist_down(self):

return self.vert_dist_up

@factory.post_generation
def add_cavers(self, create, extracted, **kwargs):
if not create or not extracted:
return

num_cavers = random.randgen.randint(1, 10)
cavers = CaverFactory.create_batch(num_cavers, user=self.user)

for caver in cavers:
self.cavers.add(caver)

@classmethod
def _adjust_kwargs(cls, **kwargs):
kwargs["cave_name"] = kwargs["cave_name"].replace(".", "")
Expand Down
107 changes: 106 additions & 1 deletion app/logger/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Div, Field, Fieldset, Layout, Submit
from dal import autocomplete
from django import forms
from django.core.exceptions import ValidationError
from django.urls import reverse_lazy
from django.utils import timezone
from users.models import CavingUser

from .mixins import CleanCaveLocationMixin, DistanceUnitFormMixin
from .models import Trip, TripPhoto
from .models import Caver, Trip, TripPhoto

User = CavingUser

Expand Down Expand Up @@ -113,6 +114,7 @@ class Meta:
"hx-indicator": "",
}
),
"cavers": autocomplete.ModelSelect2Multiple("log:caver_autocomplete"),
}

def __init__(self, user, *args, **kwargs):
Expand Down Expand Up @@ -361,3 +363,106 @@ def clean_user(self):
except User.DoesNotExist:
raise ValidationError("Username not found.")
return user


class LinkCaverForm(forms.Form):
account = forms.ChoiceField(
label="Account to link",
required=True,
)

def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super().__init__(*args, **kwargs)
self.fields["account"].choices = self._get_account_choices()
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.add_input(Submit("submit", "Save linked account"))

def _get_account_choices(self):
choices = []
already_linked = []

cavers = Caver.objects.filter(user=self.user)
for caver in cavers:
if caver.linked_account:
already_linked.append(caver.linked_account)

for friend in self.user.friends.all():
if friend not in already_linked:
choices.append([friend.pk, f"{friend.name} -- @{friend.username}"])

return choices

def clean_account(self):
account = self.cleaned_data.get("account")
if not account:
raise ValidationError("Please select an account.")

try:
account = User.objects.get(pk=account)
except User.DoesNotExist:
raise ValidationError("Account not found.")

if account == self.user:
raise ValidationError("You cannot link your own account.")

if account not in self.user.friends.all():
raise ValidationError("You can only link accounts of your friends.")

return account


class RenameCaverForm(forms.Form):
name = forms.CharField(
label="New name",
required=True,
help_text="The new name of this caver as it will appear on your trips.",
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()

def clean_name(self):
name = self.cleaned_data.get("name").strip()
if len(name) < 3:
raise ValidationError("Please enter at least three characters.")
return name


class MergeCaverForm(forms.Form):
caver = forms.ChoiceField(
label="Record to merge",
required=True,
widget=forms.Select,
)

def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super().__init__(*args, **kwargs)
self.fields["caver"].choices = self._get_caver_choices()
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.add_input(Submit("submit", "Merge caver"))

def _get_caver_choices(self):
choices = []

cavers = Caver.objects.filter(user=self.user).order_by("name")
for caver in cavers:
choices.append([caver.uuid, f"{caver.name}"])

return choices

def clean_caver(self):
caver = self.cleaned_data.get("caver")
if not caver:
raise ValidationError("Please select a caver record to merge.")

try:
caver = Caver.objects.get(uuid=caver, user=self.user)
except Caver.DoesNotExist:
raise ValidationError("Caver record not found.")

return caver
25 changes: 25 additions & 0 deletions app/logger/migrations/0038_caver_linked_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.6 on 2023-11-01 09:00

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("logger", "0037_rename_caver_fields"),
]

operations = [
migrations.AddField(
model_name="caver",
name="linked_account",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="linked_cavers",
to=settings.AUTH_USER_MODEL,
),
),
]
26 changes: 26 additions & 0 deletions app/logger/models/trip.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.conf import settings
from django.contrib.gis.db import models
from django.core.exceptions import ValidationError
from django.db.models import Sum
from django.urls import reverse
from tinymce.models import HTMLField

Expand All @@ -22,6 +23,12 @@ class Caver(models.Model):
added = models.DateTimeField("caver added on", auto_now_add=True)
updated = models.DateTimeField("caver last updated", auto_now=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
linked_account = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name="linked_cavers",
)
uuid = models.UUIDField(
verbose_name="UUID",
default=uuid.uuid4,
Expand All @@ -33,10 +40,29 @@ class Caver(models.Model):
def __str__(self):
return self.name

def total_trip_duration(self):
return self.trip_set.aggregate(Sum("duration"))["duration__sum"]

def total_trip_duration_str(self):
td = self.total_trip_duration()
if td:
return humanize.precisedelta(
td, minimum_unit="minutes", suppress=["days", "months", "years"]
)
else:
return None

def save(self, *args, **kwargs):
self.name = self.name.strip()

if self.linked_account not in self.user.friends.all():
self.linked_account = None

super().save(*args, **kwargs)

def get_absolute_url(self):
return reverse("log:caver_detail", args=[self.uuid])


# noinspection PyUnresolvedReferences
class Trip(models.Model):
Expand Down
2 changes: 1 addition & 1 deletion app/logger/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def trip_search(*, terms, for_user, search_user=None, type=None, fields=None) ->
def _build_search_field_queries(terms, fields, for_user) -> Q:
queries = Q()
if "cavers" in fields or not fields:
queries |= Q(cavers__unaccent__icontains=terms)
queries |= Q(cavers__name__unaccent__icontains=terms)
if "cave_name" in fields or not fields:
queries |= Q(cave_name__unaccent__icontains=terms)
if "cave_entrance" in fields or not fields:
Expand Down
10 changes: 3 additions & 7 deletions app/logger/tests/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,7 @@ def test_private_trips_do_not_appear_in_search_results(self):
# First check the trip appears when the trip is public
test_finder = str(uuid.uuid4())
test_identifier = str(uuid.uuid4())
trip = TripFactory(
user=self.user, cave_name=test_finder, cavers=test_identifier
)
trip = TripFactory(user=self.user, cave_name=test_finder, notes=test_identifier)
trip.privacy = Trip.PUBLIC
trip.save()

Expand Down Expand Up @@ -181,9 +179,7 @@ def test_private_trips_appear_in_results_when_searching_own_trips(self):

test_finder = str(uuid.uuid4())
test_identifier = str(uuid.uuid4())
trip = TripFactory(
user=self.user, cave_name=test_finder, cavers=test_identifier
)
trip = TripFactory(user=self.user, cave_name=test_finder, notes=test_identifier)

trip.privacy = Trip.PRIVATE
trip.save()
Expand Down Expand Up @@ -265,8 +261,8 @@ def test_search_result_pagination(self):
for i in range(11):
TripFactory(
user=self.user,
cavers="Testing Pagination",
cave_name=f"Pagination Test {i}",
cave_entrance="Testing Pagination",
)

# Check that the first page contains 10 trips
Expand Down
Loading

0 comments on commit d11c7e3

Please sign in to comment.