diff --git a/app/core/logging.py b/app/core/logging.py index 4114c312..419d32b8 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -1,6 +1,6 @@ import logging -from logger.models import Trip, TripPhoto, TripReport +from logger.models import Trip, TripPhoto from users.models import CavingUser ActionLogger = logging.getLogger("user_actions") @@ -33,16 +33,6 @@ def log_trip_action(user: CavingUser, trip: Trip, verb: str, extra: str = ""): _log_action(user, f"{verb} a trip to {trip}{extra}") -def log_tripreport_action( - user: CavingUser, report: TripReport, verb: str, extra: str = "" -): - trip = _format_trip_for_logging(report.trip) - if extra: - extra = f": {extra}" - - _log_action(user, f"{verb} a trip report for the trip to {trip}{extra}") - - def log_tripphoto_action( user: CavingUser, photo: TripPhoto, verb: str, extra: str = "" ): diff --git a/app/core/management/commands/make_test_data.py b/app/core/management/commands/make_test_data.py index 92417957..44699d4b 100644 --- a/app/core/management/commands/make_test_data.py +++ b/app/core/management/commands/make_test_data.py @@ -7,8 +7,7 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from logger.factories import TripFactory, TripReportFactory -from logger.models import Trip +from logger.factories import TripFactory from users.factories import UserFactory User = get_user_model() @@ -32,13 +31,6 @@ def add_arguments(self, parser): help="Number of trips to generate", ) - parser.add_argument( - "--reports", - type=int, - default=-1, - help="Number of trip reports to generate", - ) - parser.add_argument( "--friends", type=int, @@ -78,9 +70,6 @@ def handle(self, *args, **options): self.options = options - if options["reports"] < 0: - options["reports"] = options["trips"] // 10 - if options["seed"] != 0: random.seed(options["seed"]) factory.random.reseed_random(options["seed"]) @@ -94,12 +83,10 @@ def handle(self, *args, **options): self._generate_friendships(user_pks) trips = self._generate_trips(user_pks) - reports = self._generate_trip_reports(user_pks) if options["verbosity"] >= 1: self.stdout.write( - f"Done! Generated {len(user_pks)} users, {len(trips)} " - f"trips and {len(reports)} reports." + f"Done! Generated {len(user_pks)} users and {len(trips)} trips." ) def __get_active_users(self, user_pks=None): @@ -110,19 +97,6 @@ def __get_active_users(self, user_pks=None): users = list(User.objects.filter(pk__in=user_pks, is_active=True)) return users - def __find_trip_without_report(self, trips=None): - if trips is None: - trips = list(Trip.objects.all()) - - trip = None - while trip is None: - trip = random.choice(trips) - if not trip.has_report: - trip = trip - break - - return trip - def _generate_users(self): """Generate num_users amount of users""" num_users = self.options["users"] @@ -190,32 +164,6 @@ def _add_comments_to_trip(self, trip, users): return num_comments - def _generate_trip_reports(self, user_pks=None): - """Generate num_reports amount of trip reports amongst the users specified""" - num_reports = self.options["reports"] - if self.options["verbosity"] >= 1: - self.stdout.write(f"Generating {num_reports} reports...") - users = self.__get_active_users(user_pks) - user_pks = [user.pk for user in users] - - reports = [] - trips = list(Trip.objects.filter(user__pk__in=user_pks)) - for _ in range(num_reports): - trip = self.__find_trip_without_report(trips) - if trip is None: - break - - report = TripReportFactory(trip=trip, user=trip.user) - trips.remove(trip) - reports.append(report) - - if self.options["verbosity"] >= 2: - self.stdout.write( - f"Created report with PK {report.pk} for user {trip.user.email}." - ) - - return reports - def _generate_friendships(self, user_pks=None): """Generate friendships between users""" num_friends = self.options["friends"] diff --git a/app/logger/admin.py b/app/logger/admin.py index 95b431a0..b5be0c2b 100644 --- a/app/logger/admin.py +++ b/app/logger/admin.py @@ -2,10 +2,12 @@ from django.contrib import admin from django.forms import ModelForm from logger.forms import DistanceUnitFormMixin +from tinymce.models import HTMLField +from tinymce.widgets import TinyMCE from unfold.admin import ModelAdmin, TabularInline from unfold.widgets import UnfoldAdminTextInputWidget -from .models import Trip, TripPhoto, TripReport +from .models import Trip, TripPhoto class TripAdminForm(DistanceUnitFormMixin, ModelForm): @@ -24,22 +26,10 @@ def has_add_permission(self, request, obj=None): return False -class TripReportInline(TabularInline): - model = TripReport - fk_name = "trip" - extra = 0 - max_num = 0 - show_change_link = True - fields = ("title",) - - def has_add_permission(self, request, obj=None): - return False - - @admin.register(Trip) class TripAdmin(ModelAdmin): form = TripAdminForm - inlines = [TripPhotoInline, TripReportInline] + inlines = [TripPhotoInline] search_fields = ( "cave_name", "cave_entrance", @@ -75,7 +65,10 @@ class TripAdmin(ModelAdmin): formfield_overrides = { DistanceField: { "widget": UnfoldAdminTextInputWidget, - } + }, + HTMLField: { + "widget": TinyMCE, + }, } fieldsets = ( ( @@ -138,6 +131,7 @@ class TripAdmin(ModelAdmin): }, ), ("Notes", {"fields": ("notes",)}), + ("Trip report", {"fields": ("trip_report",)}), ) @@ -177,22 +171,3 @@ class TripPhotoAdmin(ModelAdmin): }, ), ) - - -@admin.register(TripReport) -class TripReportAdmin(ModelAdmin): - list_display = ("user", "title", "trip", "added") - list_display_links = ("title",) - list_filter = ("added", "updated") - ordering = ("-added",) - search_fields = ( - "title", - "user__username", - "user__name", - "user__email", - "trip__uuid", - ) - search_help_text = ( - "Search by title or trip UUID, or by author name, email or username." - ) - readonly_fields = ("added", "updated") diff --git a/app/logger/factories.py b/app/logger/factories.py index 9b48d07a..84ed6ab1 100644 --- a/app/logger/factories.py +++ b/app/logger/factories.py @@ -7,7 +7,7 @@ from factory.django import DjangoModelFactory from faker import Faker -from .models import Trip, TripReport +from .models import Trip fake = Faker() @@ -261,22 +261,3 @@ def vert_dist_down(self): def _adjust_kwargs(cls, **kwargs): kwargs["cave_name"] = kwargs["cave_name"].replace(".", "") return kwargs - - -class TripReportFactory(DjangoModelFactory): - class Meta: - model = TripReport - - title = factory.Faker("sentence", nb_words=5) - content = factory.Faker("text", max_nb_chars=4000) - trip = factory.Iterator(Trip.objects.filter(report=None)) - - @classmethod - def _adjust_kwargs(cls, **kwargs): - kwargs["title"] = kwargs["title"].replace(".", "") - - user = kwargs.get("user", None) - if user is None: - kwargs["user"] = kwargs["trip"].user - - return kwargs diff --git a/app/logger/forms.py b/app/logger/forms.py index f4d36b23..bc3bf11d 100644 --- a/app/logger/forms.py +++ b/app/logger/forms.py @@ -1,6 +1,5 @@ from datetime import timedelta -from crispy_bootstrap5.bootstrap5 import FloatingField from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Div, Field, Fieldset, Layout, Submit from django import forms @@ -10,47 +9,11 @@ from users.models import CavingUser from .mixins import CleanCaveLocationMixin, DistanceUnitFormMixin -from .models import Trip, TripPhoto, TripReport +from .models import Trip, TripPhoto User = CavingUser -# noinspection PyTypeChecker -class TripReportForm(forms.ModelForm): - class Meta: - model = TripReport - fields = [ - "title", - "content", - "privacy", - ] - - def __init__(self, user, *args, **kwargs): - super().__init__(*args, **kwargs) - self.user = user - self.fields["content"].label = "" - self.helper = FormHelper() - self.helper.form_method = "post" - self.helper.layout = Layout( - Fieldset( - "Trip report", - FloatingField("title"), - "content", - "privacy", - css_class="mt-4", - ), - ) - - if self.instance.pk: - self.helper.add_input( - Submit("submit", "Update report", css_class="btn-lg w-100 mt-4") - ) - else: - self.helper.add_input( - Submit("submit", "Create report", css_class="btn-lg w-100 mt-4") - ) - - # noinspection PyTypeChecker class BaseTripForm(forms.ModelForm): """ @@ -137,6 +100,7 @@ class Meta: "custom_field_3", "custom_field_4", "custom_field_5", + "trip_report", ] widgets = { "start": forms.DateTimeInput(attrs={"type": "datetime-local"}), @@ -156,6 +120,7 @@ def __init__(self, user, *args, **kwargs): self.user = user self.has_custom_fields = False self.fields["notes"].label = "" + self.fields["trip_report"].label = "" self.helper = FormHelper() self.helper.form_method = "post" @@ -245,6 +210,11 @@ def __init__(self, user, *args, **kwargs): "notes", css_class="mt-4", ), + Fieldset( + "Trip report", + "trip_report", + css_class="mt-4", + ), ) if self.instance.pk: diff --git a/app/logger/migrations/0028_trip_trip_report.py b/app/logger/migrations/0028_trip_trip_report.py new file mode 100644 index 00000000..857c5157 --- /dev/null +++ b/app/logger/migrations/0028_trip_trip_report.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-10-20 09:29 + +import tinymce.models +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("logger", "0027_remove_tripreport_unique_slug_per_user_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="trip", + name="trip_report", + field=tinymce.models.HTMLField(blank=True), + ), + ] diff --git a/app/logger/migrations/0029_migrate_trip_report_content_to_trips.py b/app/logger/migrations/0029_migrate_trip_report_content_to_trips.py new file mode 100644 index 00000000..43e8293e --- /dev/null +++ b/app/logger/migrations/0029_migrate_trip_report_content_to_trips.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.6 on 2023-10-20 09:34 + +from django.db import migrations + + +def migrate_trip_report_content_to_trips(apps, schema_editor): + trip_report = apps.get_model("logger", "TripReport") + trip = apps.get_model("logger", "Trip") + + for report in trip_report.objects.all(): + trip.objects.filter(pk=report.trip.pk).update(trip_report=report.content) + if report.privacy == "Friends": + trip.objects.filter(pk=report.trip.pk).update(privacy="Friends") + elif report.privacy == "Private": + trip.objects.filter(pk=report.trip.pk).update(privacy="Private") + + +class Migration(migrations.Migration): + dependencies = [ + ("logger", "0028_trip_trip_report"), + ] + + operations = [ + migrations.RunPython( + migrate_trip_report_content_to_trips, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/app/logger/migrations/0030_alter_trip_notes_alter_trip_trip_report.py b/app/logger/migrations/0030_alter_trip_notes_alter_trip_trip_report.py new file mode 100644 index 00000000..5e775462 --- /dev/null +++ b/app/logger/migrations/0030_alter_trip_notes_alter_trip_trip_report.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.6 on 2023-10-20 09:44 + +import tinymce.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("logger", "0029_migrate_trip_report_content_to_trips"), + ] + + operations = [ + migrations.AlterField( + model_name="trip", + name="notes", + field=models.TextField( + blank=True, + help_text="Trip notes should contain brief details of the trip, and may be private depending on your account settings. For full trip reports, use the trip report field below.", + ), + ), + migrations.AlterField( + model_name="trip", + name="trip_report", + field=tinymce.models.HTMLField( + blank=True, + help_text="Trip reports are full, article style reports of a trip and will be visible to anyone who can view the trip.", + ), + ), + ] diff --git a/app/logger/migrations/0031_delete_tripreport.py b/app/logger/migrations/0031_delete_tripreport.py new file mode 100644 index 00000000..c3ac075f --- /dev/null +++ b/app/logger/migrations/0031_delete_tripreport.py @@ -0,0 +1,15 @@ +# Generated by Django 4.2.6 on 2023-10-20 10:05 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("logger", "0030_alter_trip_notes_alter_trip_trip_report"), + ] + + operations = [ + migrations.DeleteModel( + name="TripReport", + ), + ] diff --git a/app/logger/mixins.py b/app/logger/mixins.py index 32467762..1ffcd078 100644 --- a/app/logger/mixins.py +++ b/app/logger/mixins.py @@ -1,30 +1,15 @@ from django.core.exceptions import PermissionDenied, ValidationError -from django.http import Http404 -from django.shortcuts import get_object_or_404 from django.views.generic import DetailView from logger.templatetags.logger_tags import distformat from maps.services import get_lat_long_from -from .models import Trip, TripReport - class TripContextMixin: - """Mixin to add trip context to Trip and TripReport views.""" + """Mixin to add trip context to Trip views.""" def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - - if isinstance(self.object, TripReport): - report = self.object - trip = report.trip - context["is_report"] = True # For includes/trip_header.html - elif isinstance(self.object, Trip): - trip = self.object - report = None - if hasattr(trip, "report"): - report = trip.report - else: # pragma: no cover - raise TypeError("Object is not a Trip or TripReport") + trip = self.object object_owner = trip.user if not object_owner == self.request.user: @@ -34,18 +19,14 @@ def get_context_data(self, *args, **kwargs): if object_owner.allow_friend_username: context["can_add_friend"] = True - if report: - context["can_view_report"] = report.is_viewable_by(self.request.user) - context["trip"] = trip - context["report"] = report context["object_owner"] = object_owner return context # noinspection PyAttributeOutsideInit class ViewableObjectDetailView(DetailView): - """A DetailView that considers permissions for objects like Trip and TripReport""" + """A DetailView that considers permissions for objects like a Trip""" def dispatch(self, request, *args, **kwargs): """Get the object and test permissions before dispatching the view""" @@ -61,20 +42,6 @@ def get(self, request, *args, **kwargs): return self.render_to_response(context) -# noinspection PyAttributeOutsideInit -class ReportObjectMixin: - """Mixin to get report objects from a Trip UUID""" - - def get_object(self, *args, **kwargs): - self.trip = get_object_or_404(Trip, uuid=self.kwargs.get("uuid")) - - if hasattr(self.trip, "report"): - self.object = self.trip.report - return self.object - else: - raise Http404("No report found for this trip") - - class DistanceUnitFormMixin: def __init__(self, *args, **kwargs): """ diff --git a/app/logger/models/__init__.py b/app/logger/models/__init__.py index fa9873d3..6e0dce2a 100644 --- a/app/logger/models/__init__.py +++ b/app/logger/models/__init__.py @@ -1,3 +1,2 @@ from .trip import * # noqa: F403 from .tripphoto import * # noqa: F403 -from .tripreport import * # noqa: F403 diff --git a/app/logger/models/trip.py b/app/logger/models/trip.py index 2ade7316..320024b7 100644 --- a/app/logger/models/trip.py +++ b/app/logger/models/trip.py @@ -6,6 +6,7 @@ from django.contrib.gis.db import models from django.core.exceptions import ValidationError from django.urls import reverse +from tinymce.models import HTMLField from ..validators import ( above_zero_dist_validator, @@ -240,6 +241,24 @@ class Trip(models.Model): blank=True, ) + # Notes and trip report + notes = models.TextField( + blank=True, + help_text=( + "Trip notes should contain brief details of the trip, and may be " + "private depending on your account settings. For full trip reports, " + "use the trip report field below." + ), + ) + + trip_report = HTMLField( + blank=True, + help_text=( + "Trip reports are full, article style reports of a trip and will be " + "visible to anyone who can view the trip." + ), + ) + # Metadata likes = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, related_name="liked_trips" @@ -258,7 +277,6 @@ class Trip(models.Model): "you, regardless of who can view the trip." ), ) - notes = models.TextField(blank=True) added = models.DateTimeField("trip added on", auto_now_add=True) updated = models.DateTimeField("trip last updated", auto_now=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) @@ -451,10 +469,6 @@ def is_public(self): return True return False - @property - def has_report(self): - return hasattr(self, "report") and self.report is not None - @property def number(self): """Returns the 'index' of the trip by date""" diff --git a/app/logger/models/tripreport.py b/app/logger/models/tripreport.py deleted file mode 100644 index 46e4c52e..00000000 --- a/app/logger/models/tripreport.py +++ /dev/null @@ -1,81 +0,0 @@ -from django.conf import settings -from django.db import models -from django.urls import reverse -from tinymce.models import HTMLField - -from .trip import Trip - - -# noinspection PyUnresolvedReferences -class TripReport(models.Model): - # Relationships - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - trip = models.OneToOneField( - Trip, on_delete=models.CASCADE, primary_key=True, related_name="report" - ) - likes = models.ManyToManyField( - settings.AUTH_USER_MODEL, blank=True, related_name="liked_reports" - ) - - # Content - title = models.CharField(max_length=100) - content = HTMLField() - - # Metadata - added = models.DateTimeField("report added on", auto_now_add=True) - updated = models.DateTimeField("report last updated", auto_now=True) - - # Privacy - DEFAULT = "Default" - PUBLIC = "Public" - FRIENDS = "Friends" - PRIVATE = "Private" - PRIVACY_CHOICES = [ - (DEFAULT, "Anyone who can view the trip"), - (PUBLIC, "Anyone, even if the trip is private"), - (FRIENDS, "Only my friends"), - (PRIVATE, "Only me"), - ] - - privacy = models.CharField( - "Who can view this report?", - max_length=10, - choices=PRIVACY_CHOICES, - default=DEFAULT, - ) - - def __str__(self): - return self.title - - def get_absolute_url(self): - return reverse("log:report_detail", args=[self.trip.uuid]) - - def is_viewable_by(self, user_viewing): - """Returns whether or not user_viewing can view this report""" - if user_viewing == self.user: - return True - - if self.privacy == self.PUBLIC: - return True - - if self.privacy == self.FRIENDS: - if user_viewing in self.user.friends.all(): - return True - - if self.privacy == self.DEFAULT: - return self.trip.is_viewable_by(user_viewing) - - return False - - @property - def is_private(self): - if self.privacy == self.DEFAULT: - return self.trip.is_private - elif self.privacy == self.PUBLIC: - return False - return True - - @property - def is_public(self): - if self.is_private is False: - return True diff --git a/app/logger/tests/test_pages_load.py b/app/logger/tests/test_pages_load.py index 1bfc7bf4..f83ff3d1 100644 --- a/app/logger/tests/test_pages_load.py +++ b/app/logger/tests/test_pages_load.py @@ -2,7 +2,7 @@ from django.test import Client, TestCase, tag from django.urls import reverse from django.utils import timezone -from logger.models import Trip, TripReport +from logger.models import Trip User = get_user_model() @@ -23,13 +23,6 @@ def setUp(self): user=self.user, cave_name="Test Trip", start=timezone.now() ) - self.report = TripReport.objects.create( - user=self.user, - trip=self.trip, - title="Test Report", - content="Test Report Content", - ) - self.client = Client() def test_index_page_loads(self): @@ -96,20 +89,6 @@ def test_export_page_loads(self): response = self.client.get(reverse("export:index")) self.assertEqual(response.status_code, 200) - def test_trip_report_detail_page_loads(self): - """Test that the trip report detail page loads""" - self.client.force_login(self.user) - response = self.client.get(self.report.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_trip_report_update_page_loads(self): - """Test that the trip report update page loads""" - self.client.force_login(self.user) - response = self.client.get( - reverse("log:report_update", args=[self.report.trip.uuid]) - ) - self.assertEqual(response.status_code, 200) - def test_htmx_feed_page_loads(self): """Test that the HTMX feed page loads""" self.client.force_login(self.user) diff --git a/app/logger/tests/test_trip_reports.py b/app/logger/tests/test_trip_reports.py deleted file mode 100644 index d206117b..00000000 --- a/app/logger/tests/test_trip_reports.py +++ /dev/null @@ -1,288 +0,0 @@ -import logging - -from django.contrib.auth import get_user_model -from django.test import Client, TestCase, tag -from django.urls import reverse -from django.utils import timezone as tz - -from ..models import Trip, TripReport - -User = get_user_model() - - -@tag("logger", "tripreports", "fast", "views") -class TripReportTests(TestCase): - def setUp(self): - """Reduce log level to avoid 404 error""" - logger = logging.getLogger("django.request") - self.previous_level = logger.getEffectiveLevel() - logger.setLevel(logging.ERROR) - - self.client = Client() - - self.user = User.objects.create_user( - email="enabled@user.app", - username="enabled", - password="testpassword", - name="Joe", - ) - self.user.is_active = True - self.user.save() - - self.user2 = User.objects.create_user( - email="user2@user.app", - username="user2", - password="testpassword", - name="User 2", - ) - self.user2.is_active = True - self.user2.save() - - self.trip = Trip.objects.create( - user=self.user, - cave_name="Test Cave", - start=tz.now(), - ) - - def tearDown(self): - """Reset the log level back to normal""" - logger = logging.getLogger("django.request") - logger.setLevel(self.previous_level) - - def test_trip_report_create_view(self): - """Test the trip report create view in GET and POST""" - self.client.force_login(self.user) - response = self.client.get(reverse("log:report_create", args=[self.trip.uuid])) - self.assertEqual(response.status_code, 200) - - response = self.client.post( - reverse("log:report_create", args=[self.trip.uuid]), - { - "title": "Test Report", - "content": "Test content.", - "privacy": TripReport.PUBLIC, - }, - ) - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, self.trip.report.get_absolute_url()) - self.assertEqual(TripReport.objects.count(), 1) - self.assertEqual(TripReport.objects.get().title, "Test Report") - self.assertEqual(TripReport.objects.get().content, "Test content.") - self.assertEqual(TripReport.objects.get().privacy, TripReport.PUBLIC) - self.assertEqual(TripReport.objects.get().trip, self.trip) - - def test_trip_report_create_view_redirects_if_a_report_already_exists(self): - """Test the trip report create view redirects if a report already exists""" - report = TripReport.objects.create( - title="Test Report", - content="Test content.", - privacy=TripReport.PUBLIC, - trip=self.trip, - user=self.user, - ) - - self.client.force_login(self.user) - response = self.client.get( - reverse("log:report_create", args=[report.trip.uuid]) - ) - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, report.get_absolute_url()) - - @tag("privacy") - def test_users_cannot_edit_a_trip_report_for_other_users(self): - """Test users cannot edit a trip report which does not belong to them.""" - user = User.objects.create_user( - email="new@user.app", - password="password", - username="testuser", - name="Test", - ) - user.is_active = True - user.save() - - report = TripReport.objects.create( - title="Test Report", - content="Test content.", - privacy=TripReport.PUBLIC, - trip=self.trip, - user=self.user, - ) - - self.client.login(email="new@user.app", password="password") - response = self.client.get( - reverse("log:report_update", args=[report.trip.uuid]) - ) - self.assertEqual(response.status_code, 403) - - response = self.client.post( - reverse("log:report_delete", args=[report.trip.uuid]) - ) - self.assertEqual(response.status_code, 403) - - response = self.client.get(reverse("log:report_create", args=[self.trip.uuid])) - self.assertEqual(response.status_code, 403) - - def test_users_can_view_and_edit_their_own_trip_reports(self): - """Test users can view and edit their own trip reports.""" - report = TripReport.objects.create( - title="Test Report", - content="Test content.", - privacy=TripReport.PUBLIC, - trip=self.trip, - user=self.user, - ) - - self.client.force_login(self.user) - response = self.client.get(report.get_absolute_url()) - self.assertEqual(response.status_code, 200) - self.assertContains(response, "Test Report") - self.assertContains(response, "Test content.") - - response = self.client.get( - reverse("log:report_update", args=[report.trip.uuid]) - ) - self.assertEqual(response.status_code, 200) - self.assertContains(response, "Test Report") - self.assertContains(response, "Test content.") - - response = self.client.post( - reverse("log:report_update", args=[report.trip.uuid]), - { - "title": "Test Report Updated", - "content": "Test content updated.", - "privacy": TripReport.PUBLIC, - }, - ) - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, report.get_absolute_url()) - - report.refresh_from_db() - self.assertEqual(report.title, "Test Report Updated") - self.assertEqual(report.content, "Test content updated.") - - response = self.client.post( - reverse("log:report_delete", args=[report.trip.uuid]) - ) - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, self.trip.get_absolute_url()) - self.assertEqual(TripReport.objects.count(), 0) - - def test_trip_report_link_appears_on_trip_list(self): - """Test the trip report link appears on the trip list page.""" - self.client.force_login(self.user) - report = TripReport.objects.create( - title="Test Report", - content="Test content.", - privacy=TripReport.PUBLIC, - trip=self.trip, - user=self.user, - ) - - response = self.client.get(reverse("log:user", args=[self.user.username])) - self.assertEqual(response.status_code, 200) - self.assertContains(response, report.get_absolute_url()) - - def test_add_trip_report_link_appears_when_no_report_has_been_added(self): - """Test the add trip report link appears on the trip detail page""" - self.client.force_login(self.user) - response = self.client.get(self.trip.get_absolute_url()) - self.assertEqual(response.status_code, 200) - self.assertContains( - response, reverse("log:report_create", args=[self.trip.uuid]) - ) - - def test_add_trip_report_does_not_appear_when_report_added(self): - """Test the add trip report link does not appear on the detail page""" - self.client.force_login(self.user) - TripReport.objects.create( - title="Test Report", - content="Test content.", - privacy=TripReport.PUBLIC, - trip=self.trip, - user=self.user, - ) - - response = self.client.get(self.trip.get_absolute_url()) - self.assertEqual(response.status_code, 200) - self.assertNotContains( - response, reverse("log:report_create", args=[self.trip.uuid]) - ) - - def test_view_and_edit_trip_report_links_appear_when_a_report_has_been_added(self): - """Test the view and edit trip report links appear on the trip detail page""" - self.client.force_login(self.user) - report = TripReport.objects.create( - title="Test Report", - content="Test content.", - privacy=TripReport.PUBLIC, - trip=self.trip, - user=self.user, - ) - - response = self.client.get(self.trip.get_absolute_url()) - self.assertEqual(response.status_code, 200) - self.assertContains(response, report.get_absolute_url()) - self.assertContains( - response, reverse("log:report_update", args=[report.trip.uuid]) - ) - - @tag("privacy") - def test_trip_report_detail_view_with_various_privacy_settings(self): - """Test that the trip report detail view respects privacy settings""" - trip = Trip.objects.filter(user=self.user).first() - report = TripReport.objects.create( - user=self.user, - trip=trip, - title="Test report", - content="Test report content", - ) - self.client.force_login(self.user2) - - report.privacy = TripReport.PRIVATE - report.save() - response = self.client.get(report.get_absolute_url()) - self.assertEqual(response.status_code, 403) - - report.privacy = TripReport.FRIENDS - report.save() - response = self.client.get(report.get_absolute_url()) - self.assertEqual(response.status_code, 403) - - self.user.friends.add(self.user2) - self.user2.friends.add(self.user) - response = self.client.get(report.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - report.privacy = TripReport.PUBLIC - report.save() - response = self.client.get(report.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - report.privacy = TripReport.DEFAULT - report.save() - trip.privacy = Trip.FRIENDS - trip.save() - response = self.client.get(report.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - trip.privacy = Trip.PRIVATE - trip.save() - response = self.client.get(report.get_absolute_url()) - self.assertEqual(response.status_code, 403) - - trip.privacy = Trip.PUBLIC - trip.save() - response = self.client.get(report.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_deleting_a_trip_report_that_does_not_exist(self): - """Test that deleting a trip report that does not exist returns 404""" - self.client.force_login(self.user) - response = self.client.post(reverse("log:report_delete", args=[self.trip.uuid])) - self.assertEqual(response.status_code, 404) - - def test_accessing_a_trip_report_for_a_trip_which_does_not_have_one(self): - """Test that accessing a trip report for a trip without a report returns 404""" - self.client.force_login(self.user) - response = self.client.get(reverse("log:report_detail", args=[self.trip.uuid])) - self.assertEqual(response.status_code, 404) diff --git a/app/logger/tests/test_trips.py b/app/logger/tests/test_trips.py index 9833aa17..53bb091c 100644 --- a/app/logger/tests/test_trips.py +++ b/app/logger/tests/test_trips.py @@ -9,7 +9,7 @@ from django.utils.timezone import datetime as dt from django.utils.timezone import timedelta as td -from ..models import Trip, TripReport +from ..models import Trip User = get_user_model() @@ -95,14 +95,6 @@ def setUp(self): aid_dist="600m", ) - # Trip report - self.report = TripReport.objects.create( - trip=self.trip, - user=self.trip.user, - title="Test Report", - content="Test Report", - ) - def tearDown(self): """Reset the log level back to normal""" logger = logging.getLogger("django.request") @@ -264,37 +256,10 @@ def test_trip_is_viewable_by_with_user_that_is_a_friend(self): trip_friends.save() self.assertTrue(trip_friends.is_viewable_by(self.user2)) - @tag("privacy") - def test_trip_report_is_private_and_is_public(self): - """Test the trip report is_private and is_public functions""" - self.trip.privacy = Trip.PRIVATE - self.trip.save() - - self.report.privacy = TripReport.DEFAULT - self.report.save() - - self.assertEqual(self.report.trip, self.trip) - self.assertTrue(self.report.is_private) - self.assertFalse(self.report.is_public) - - self.report.privacy = TripReport.PRIVATE - self.report.save() - self.assertTrue(self.report.is_private) - self.assertFalse(self.report.is_public) - - self.report.privacy = TripReport.PUBLIC - self.report.save() - self.assertFalse(self.report.is_private) - self.assertTrue(self.report.is_public) - def test_trip_str(self): """Test the Trip model __str__ function""" self.assertEqual(str(self.trip), self.trip.cave_name) - def test_trip_report_str(self): - """Test the TripReport model __str__ function""" - self.assertEqual(str(self.report), self.report.title) - def test_trip_validates_start_time_before_end_time(self): """Test the Trip model validates start time before end time""" self.trip.start = tz.now() + td(days=1) @@ -718,7 +683,6 @@ def test_sidebar_displays_properly_when_viewing_another_users_trip(self): self.assertNotContains(response, reverse("log:trip_update", args=[trip.uuid])) self.assertNotContains(response, reverse("log:trip_delete", args=[trip.uuid])) - self.assertNotContains(response, reverse("log:report_create", args=[trip.uuid])) @tag("privacy") def test_add_as_friend_link_does_not_appear_when_disabled(self): @@ -742,30 +706,3 @@ def test_add_as_friend_link_does_not_appear_when_already_friends(self): response = self.client.get(trip.get_absolute_url()) self.assertNotContains(response, "Add as friend") self.assertNotContains(response, reverse("users:friend_add")) - - def test_trip_report_link_appears_in_sidebar_for_other_users(self): - """Test that the trip report link appears in the sidebar for other users""" - trip = Trip.objects.filter(user=self.user).first() - report = TripReport.objects.create( - user=self.user, - trip=trip, - title="Test report", - content="Test report content", - ) - response = self.client.get(trip.get_absolute_url()) - self.assertContains(response, report.get_absolute_url()) - - @tag("privacy") - def test_trip_report_link_does_not_appear_in_sidebar_when_private(self): - """Test that the trip report link does not appear in the sidebar when private""" - trip = Trip.objects.filter(user=self.user).first() - - report = TripReport.objects.create( - user=self.user, - trip=trip, - title="Test report", - content="Test report content", - privacy=TripReport.PRIVATE, - ) - response = self.client.get(trip.get_absolute_url()) - self.assertNotContains(response, report.get_absolute_url()) diff --git a/app/logger/urls.py b/app/logger/urls.py index ec7eeede..1e1abbce 100644 --- a/app/logger/urls.py +++ b/app/logger/urls.py @@ -48,14 +48,9 @@ views.TripPhotosUpdate.as_view(), name="trip_photos_update", ), - path("report/add//", views.ReportCreate.as_view(), name="report_create"), path( - "report/edit//", views.ReportUpdate.as_view(), name="report_update" + "report//", views.TripReportRedirect.as_view(), name="report_detail" ), - path( - "report/delete//", views.ReportDelete.as_view(), name="report_delete" - ), - path("report//", views.ReportDetail.as_view(), name="report_detail"), path("search/", views.Search.as_view(), name="search"), path("feed/htmx/", views.HTMXTripFeed.as_view(), name="feed_htmx_view"), path( diff --git a/app/logger/views/__init__.py b/app/logger/views/__init__.py index 50e90e62..b489b95a 100644 --- a/app/logger/views/__init__.py +++ b/app/logger/views/__init__.py @@ -1,6 +1,5 @@ from .feed import * # noqa: F403 from .search import * # noqa: F403 from .tripphotos import * # noqa: F403 -from .tripreports import * # noqa: F403 from .trips import * # noqa: F403 from .userprofile import * # noqa: F403 diff --git a/app/logger/views/tripreports.py b/app/logger/views/tripreports.py deleted file mode 100644 index 765bffe3..00000000 --- a/app/logger/views/tripreports.py +++ /dev/null @@ -1,119 +0,0 @@ -from core.logging import log_tripreport_action -from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.messages.views import SuccessMessageMixin -from django.core.exceptions import PermissionDenied -from django.http import Http404 -from django.shortcuts import get_object_or_404, redirect -from django.utils.decorators import method_decorator -from django.views import View -from django.views.generic import CreateView, UpdateView -from django_ratelimit.decorators import ratelimit - -from ..forms import TripReportForm -from ..mixins import ReportObjectMixin, TripContextMixin, ViewableObjectDetailView -from ..models import Trip, TripReport - - -@method_decorator( - ratelimit(key="user", rate="20/h", method=ratelimit.UNSAFE), name="dispatch" -) -class ReportCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView): - model = TripReport - form_class = TripReportForm - template_name = "logger/trip_report_create.html" - success_message = "The trip report has been created." - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.trip = None - - def dispatch(self, request, *args, **kwargs): - self.trip = self.get_trip() - return super().dispatch(request, *args, **kwargs) - - def form_valid(self, form): - candidate = form.save(commit=False) - candidate.user = self.request.user - candidate.trip = self.trip - candidate.save() - log_tripreport_action(self.request.user, candidate, "added") - return super().form_valid(form) - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(**kwargs) - context["trip"] = self.trip - context["object_owner"] = self.trip.user - return context - - def get_trip(self): - trip = get_object_or_404(Trip, uuid=self.kwargs["uuid"]) - if not trip.user == self.request.user: - raise PermissionDenied - return trip - - def get(self, request, *args, **kwargs): - if self.trip.has_report: - return redirect(self.trip.report.get_absolute_url()) - - return super().get(self, request, *args, **kwargs) - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["user"] = self.request.user - return kwargs - - -class ReportDetail(ReportObjectMixin, TripContextMixin, ViewableObjectDetailView): - model = TripReport - template_name = "logger/trip_report_detail.html" - - -class ReportUpdate( - LoginRequiredMixin, ReportObjectMixin, SuccessMessageMixin, UpdateView -): - model = TripReport - form_class = TripReportForm - template_name = "logger/trip_report_update.html" - success_message = "The trip report has been updated." - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(**kwargs) - context["trip"] = self.trip - context["object_owner"] = self.get_object().user - return context - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["user"] = self.request.user - return kwargs - - def get_object(self, *args, **kwargs): - obj = super().get_object(*args, **kwargs) - if obj.user == self.request.user: - return obj - raise PermissionDenied - - def form_valid(self, form): - log_tripreport_action(self.request.user, self.object, "updated") - return super().form_valid(form) - - -class ReportDelete(LoginRequiredMixin, View): - def post(self, request, uuid): - try: - report = get_object_or_404(Trip, uuid=uuid).report - except TripReport.DoesNotExist: - raise Http404 - - if not report.user == request.user: - raise PermissionDenied - - trip = report.trip - log_tripreport_action(request.user, report, "deleted") - report.delete() - messages.success( - request, - f"The trip report for the trip to {trip.cave_name} has been deleted.", - ) - return redirect(trip.get_absolute_url()) diff --git a/app/logger/views/trips.py b/app/logger/views/trips.py index 7f8978cf..dd42fa14 100644 --- a/app/logger/views/trips.py +++ b/app/logger/views/trips.py @@ -73,7 +73,7 @@ class TripDetail(TripContextMixin, ViewableObjectDetailView): def get_queryset(self): qs = ( Trip.objects.all() - .select_related("user", "report") + .select_related("user") .prefetch_related( "photos", "likes", @@ -188,3 +188,11 @@ def post(self, request, uuid): f"The trip to {trip.cave_name} has been deleted.", ) return redirect("log:user", username=request.user.username) + + +class TripReportRedirect(RedirectView): + """Redirect to support old TripReport Model URLs which are now Trip URLs""" + + def get_redirect_url(self, *args, **kwargs): + trip = get_object_or_404(Trip, uuid=kwargs.get("uuid")) + return trip.get_absolute_url() diff --git a/app/logger/views/userprofile.py b/app/logger/views/userprofile.py index 6ba7562f..a4e6d774 100644 --- a/app/logger/views/userprofile.py +++ b/app/logger/views/userprofile.py @@ -44,7 +44,7 @@ def setup(self, *args, **kwargs): def get_queryset(self): trips = ( Trip.objects.filter(user=self.profile_user) - .select_related("report", "user") + .select_related("user") .prefetch_related("photos") .order_by(*self.get_ordering()) ).annotate( diff --git a/app/staff/views.py b/app/staff/views.py index 5bb288b9..d96ec0f0 100644 --- a/app/staff/views.py +++ b/app/staff/views.py @@ -1,7 +1,7 @@ from comments.models import Comment from django.contrib.auth import get_user_model from django.views.generic import RedirectView, TemplateView -from logger.models import Trip, TripPhoto, TripReport +from logger.models import Trip, TripPhoto from .mixins import ModeratorRequiredMixin from .statistics import get_integer_field_statistics, get_time_statistics @@ -16,7 +16,6 @@ def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) trips = Trip.objects.all() - trip_reports = TripReport.objects.all() users = User.objects.all() comments = Comment.objects.all() photos = TripPhoto.objects.filter(is_valid=True) @@ -26,8 +25,6 @@ def get_context_data(self, *args, **kwargs): statistics = [ get_time_statistics(trips), get_time_statistics(trips, metric="Updated", lookup="updated__gte"), - get_time_statistics(trip_reports), - get_time_statistics(trip_reports, metric="Updated", lookup="updated__gte"), get_time_statistics(comments), get_time_statistics(photos_valid, metric="Valid", lookup="added__gte"), get_time_statistics( diff --git a/app/static/css/core.css b/app/static/css/core.css index 83f8a38a..80487893 100644 --- a/app/static/css/core.css +++ b/app/static/css/core.css @@ -20,6 +20,9 @@ body { max-width: 38em; } +#mainContent { + min-width: 0; +} /* Messages diff --git a/app/templates/base_trips.html b/app/templates/base_trips.html index f9df44b6..42430807 100644 --- a/app/templates/base_trips.html +++ b/app/templates/base_trips.html @@ -6,4 +6,4 @@ {% block sidebar %}{% include "logger/_sidebar_trips.html" %}{% endblock %} {% block mobile_menu %}{% include "logger/_sidebar_trips.html" %}{% endblock %} -{% block modals %}{% include "logger/_trip_and_report_delete_modals.html" %}{% endblock %} +{% block modals %}{% include "logger/_trip_delete_modal.html" %}{% endblock %} diff --git a/app/templates/logger/_sidebar_trips.html b/app/templates/logger/_sidebar_trips.html index 689ed619..62c3b666 100644 --- a/app/templates/logger/_sidebar_trips.html +++ b/app/templates/logger/_sidebar_trips.html @@ -46,25 +46,6 @@ Photos - Report - {% if trip.report %} - - View trip report - - - - Edit trip report - - - - Delete trip report - - {% else %} - - Add trip report - - {% endif %} - {% elif trip and user != object_owner %} {% comment %}Viewing a trip but not the owner of it{% endcomment %} @@ -79,12 +60,6 @@ View trip - {% if can_view_report %} - - View trip report - - {% endif %} - {% if can_add_friend and user.is_authenticated %} Add as friend diff --git a/app/templates/logger/_trip_and_report_delete_modals.html b/app/templates/logger/_trip_and_report_delete_modals.html deleted file mode 100644 index 3d1b1d3e..00000000 --- a/app/templates/logger/_trip_and_report_delete_modals.html +++ /dev/null @@ -1,47 +0,0 @@ -{% if trip and user == object_owner %} - -{% endif %} - -{% if trip.report and user == object_owner %} - -{% endif %} diff --git a/app/templates/logger/_trip_delete_modal.html b/app/templates/logger/_trip_delete_modal.html new file mode 100644 index 00000000..581b127e --- /dev/null +++ b/app/templates/logger/_trip_delete_modal.html @@ -0,0 +1,23 @@ +{% if trip and user == object_owner %} + +{% endif %} diff --git a/app/templates/logger/profile.html b/app/templates/logger/profile.html index 589a6bdb..2e598f87 100644 --- a/app/templates/logger/profile.html +++ b/app/templates/logger/profile.html @@ -167,9 +167,6 @@ {% if trip.cave_coordinates and user == profile_user %}   {% endif %} - {% if trip.report %} -   - {% endif %} {% if user == profile_user %} {% endif %} diff --git a/app/templates/logger/trip_detail.html b/app/templates/logger/trip_detail.html index c39978f9..13a51f44 100644 --- a/app/templates/logger/trip_detail.html +++ b/app/templates/logger/trip_detail.html @@ -76,7 +76,6 @@

{{ trip.cave_name }}

{% if trip.notes %} -
@@ -94,8 +93,26 @@

{{ trip.cave_name }}

{% endif %} + {% if trip.trip_report %} + {% if trip.notes %} +
+ {% endif %} +
+
+ + Trip report +
+
{{ trip.trip_report|safe }}
+
+
+ {% endif %} + {% if show_photos %} -
+ {% if trip.notes or trip.trip_report %} +
+ {% endif %} + +
{% for photo in valid_photos %}
@@ -178,7 +195,7 @@ {% endif %} {% endif %} - {% if trip.notes or show_photos %}
{% endif %} + {% if trip.notes or trip.trip_report or show_photos %}
{% endif %}
diff --git a/app/templates/logger/trip_report_create.html b/app/templates/logger/trip_report_create.html deleted file mode 100644 index 0f95b661..00000000 --- a/app/templates/logger/trip_report_create.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base_trips.html" %} -{% load crispy_forms_tags %} - -{% block title %}Add a trip report{% endblock %} -{% block display_title %}Add a trip report{% endblock %} -{% block header_scripts %}{{ form.media }}{% endblock %} - -{% block main %} - - - {% crispy form %} -{% endblock %} diff --git a/app/templates/logger/trip_report_detail.html b/app/templates/logger/trip_report_detail.html deleted file mode 100644 index 56f0588d..00000000 --- a/app/templates/logger/trip_report_detail.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "base_trips.html" %} - -{% block title %}{{ tripreport.title }}{% endblock %} -{% block meta_tags %}{% endblock %} -{% block description %}A trip report for a trip to {{ trip.cave_name }} that took place on {{ trip.start|date }}.{% endblock %} -{% block display_title %}Trip to {{ trip.cave_name }}{% endblock %} -{% block display_title_right %}@{{ tripreport.user.username }}{% endblock %} - -{% block main %} -

{{ tripreport.title }}

- - {{ tripreport.content|safe }} - -
-
- - Created: {{ tripreport.added|date:"jS M Y" }} - -
-
- - Updated: {{ tripreport.updated|date:"jS M Y" }} - -
-
-{% endblock main %} diff --git a/app/templates/logger/trip_report_update.html b/app/templates/logger/trip_report_update.html deleted file mode 100644 index f16daf04..00000000 --- a/app/templates/logger/trip_report_update.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "base_trips.html" %} -{% load crispy_forms_tags %} -{% block title %}Edit trip report{% endblock %} -{% block display_title %}Edit trip report{% endblock %} -{% block header_scripts %}{{ form.media }}{% endblock %} - -{% block main %} -
- You are editing a trip report for the trip to {{ trip.cave_name }} on {{ trip.start|date }}. Would you - like to view the trip? -
- - {% crispy form %} -{% endblock %} diff --git a/app/users/migrations/0034_alter_cavinguser_allow_comments.py b/app/users/migrations/0034_alter_cavinguser_allow_comments.py new file mode 100644 index 00000000..6cf46c9e --- /dev/null +++ b/app/users/migrations/0034_alter_cavinguser_allow_comments.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.6 on 2023-10-20 10:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0033_cavinguser_has_verified_email"), + ] + + operations = [ + migrations.AlterField( + model_name="cavinguser", + name="allow_comments", + field=models.BooleanField( + default=True, + help_text="If enabled, other users will be able to comment on your trips. Disabling this setting will not delete any existing comments, but will hide them until it is re-enabled.", + verbose_name="Allow comments on your trips", + ), + ), + ] diff --git a/app/users/models.py b/app/users/models.py index 9a9bd535..f908c250 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -12,7 +12,7 @@ from django.urls import reverse from django.utils import timezone as django_tz from django_countries.fields import CountryField -from logger.models import Trip, TripReport +from logger.models import Trip from timezone_field import TimeZoneField @@ -223,8 +223,8 @@ class CavingUser(AbstractBaseUser, PermissionsMixin): "Allow comments on your trips", default=True, help_text=( - "If enabled, other users will be able to comment on your trips " - "and trip reports. Disabling this setting will not delete any existing " + "If enabled, other users will be able to comment on your trips. " + "Disabling this setting will not delete any existing " "comments, but will hide them until it is re-enabled." ), ) @@ -398,10 +398,6 @@ def mutual_friends(self, other_user): def trips(self): return Trip.objects.filter(user=self) - @property - def reports(self): - return TripReport.objects.filter(user=self) - @property def has_trips(self): return self.trips.count() > 0 diff --git a/app/users/tests/test_custom_user_model.py b/app/users/tests/test_custom_user_model.py index c2bc5f45..cdb6c65f 100644 --- a/app/users/tests/test_custom_user_model.py +++ b/app/users/tests/test_custom_user_model.py @@ -6,7 +6,7 @@ from django.test import Client, TestCase, tag from django.urls import reverse from django.utils import timezone -from logger.models import Trip, TripReport +from logger.models import Trip from ..models import avatar_upload_path @@ -125,24 +125,6 @@ def test_user_get_full_name_function(self): """Test the get_full_name function of the CavingUser model""" self.assertEqual(self.user.get_full_name(), self.user.name) - def test_user_trip_reports_function(self): - """Test the reports function of the CavingUser model""" - trip = Trip.objects.create( - user=self.user, - cave_name="Test Cave", - start=timezone.now(), - ) - - trip_report = TripReport.objects.create( - user=self.user, - trip=trip, - title="Test Report", - content="Test content", - ) - - self.assertEqual(self.user.reports.count(), 1) - self.assertEqual(self.user.reports.first(), trip_report) - def test_avatar_upload_path(self): instance = mock.MagicMock() instance.uuid = uuid.uuid4()