mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-25 03:11:13 -05:00
Add ReviewEvent model and ReviewSubmissionService for review management
- Created a new ReviewEvent model to track review events with fields for content, rating, moderation status, and timestamps. - Added ForeignKey relationships to connect ReviewEvent with ContentSubmission, User, and Review. - Implemented ReviewSubmissionService to handle review submissions, including creation, updates, and moderation workflows. - Introduced atomic transactions to ensure data integrity during review submissions and updates. - Added logging for review submission and moderation actions for better traceability. - Implemented validation to prevent duplicate reviews and ensure only the review owner can update their review.
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
# Generated by Django 4.2.8 on 2025-11-08 21:32
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
import pgtrigger.compiler
|
||||
import pgtrigger.migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("moderation", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("pghistory", "0006_delete_aggregateevent"),
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
("reviews", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ReviewEvent",
|
||||
fields=[
|
||||
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
|
||||
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("pgh_label", models.TextField(help_text="The event label.")),
|
||||
("id", models.BigIntegerField()),
|
||||
(
|
||||
"created",
|
||||
model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
("object_id", models.PositiveIntegerField()),
|
||||
("title", models.CharField(max_length=200)),
|
||||
("content", models.TextField()),
|
||||
(
|
||||
"rating",
|
||||
models.IntegerField(
|
||||
help_text="Rating from 1 to 5 stars",
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(1),
|
||||
django.core.validators.MaxValueValidator(5),
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
"visit_date",
|
||||
models.DateField(
|
||||
blank=True, help_text="Date the user visited", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"wait_time_minutes",
|
||||
models.PositiveIntegerField(
|
||||
blank=True, help_text="Wait time in minutes", null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"helpful_votes",
|
||||
models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text="Number of users who found this review helpful",
|
||||
),
|
||||
),
|
||||
(
|
||||
"total_votes",
|
||||
models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text="Total number of votes (helpful + not helpful)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"moderation_status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("pending", "Pending"),
|
||||
("approved", "Approved"),
|
||||
("rejected", "Rejected"),
|
||||
],
|
||||
default="pending",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"moderation_notes",
|
||||
models.TextField(blank=True, help_text="Notes from moderator"),
|
||||
),
|
||||
("moderated_at", models.DateTimeField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="review",
|
||||
name="submission",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="ContentSubmission that created this review",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="reviews",
|
||||
to="moderation.contentsubmission",
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="review",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="insert_insert",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func='INSERT INTO "reviews_reviewevent" ("content", "content_type_id", "created", "helpful_votes", "id", "moderated_at", "moderated_by_id", "moderation_notes", "moderation_status", "modified", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "submission_id", "title", "total_votes", "user_id", "visit_date", "wait_time_minutes") VALUES (NEW."content", NEW."content_type_id", NEW."created", NEW."helpful_votes", NEW."id", NEW."moderated_at", NEW."moderated_by_id", NEW."moderation_notes", NEW."moderation_status", NEW."modified", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rating", NEW."submission_id", NEW."title", NEW."total_votes", NEW."user_id", NEW."visit_date", NEW."wait_time_minutes"); RETURN NULL;',
|
||||
hash="b35102b3c04881bef39a259f1105a6032033b6d7",
|
||||
operation="INSERT",
|
||||
pgid="pgtrigger_insert_insert_7a7c1",
|
||||
table="reviews_review",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name="review",
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name="update_update",
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
|
||||
func='INSERT INTO "reviews_reviewevent" ("content", "content_type_id", "created", "helpful_votes", "id", "moderated_at", "moderated_by_id", "moderation_notes", "moderation_status", "modified", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "submission_id", "title", "total_votes", "user_id", "visit_date", "wait_time_minutes") VALUES (NEW."content", NEW."content_type_id", NEW."created", NEW."helpful_votes", NEW."id", NEW."moderated_at", NEW."moderated_by_id", NEW."moderation_notes", NEW."moderation_status", NEW."modified", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rating", NEW."submission_id", NEW."title", NEW."total_votes", NEW."user_id", NEW."visit_date", NEW."wait_time_minutes"); RETURN NULL;',
|
||||
hash="252cddc558c9724c0ef840a91c1d0ebd03a1b7a2",
|
||||
operation="UPDATE",
|
||||
pgid="pgtrigger_update_update_b34c8",
|
||||
table="reviews_review",
|
||||
when="AFTER",
|
||||
),
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="reviewevent",
|
||||
name="content_type",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
limit_choices_to={"model__in": ("park", "ride")},
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="contenttypes.contenttype",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="reviewevent",
|
||||
name="moderated_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="reviewevent",
|
||||
name="pgh_context",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="pghistory.context",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="reviewevent",
|
||||
name="pgh_obj",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="events",
|
||||
related_query_name="+",
|
||||
to="reviews.review",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="reviewevent",
|
||||
name="submission",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="ContentSubmission that created this review",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to="moderation.contentsubmission",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="reviewevent",
|
||||
name="user",
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
related_query_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -4,8 +4,10 @@ from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from model_utils.models import TimeStampedModel
|
||||
import pghistory
|
||||
|
||||
|
||||
@pghistory.track()
|
||||
class Review(TimeStampedModel):
|
||||
"""
|
||||
User reviews for parks or rides.
|
||||
@@ -90,6 +92,16 @@ class Review(TimeStampedModel):
|
||||
related_name='moderated_reviews'
|
||||
)
|
||||
|
||||
# Link to ContentSubmission (Sacred Pipeline integration)
|
||||
submission = models.ForeignKey(
|
||||
'moderation.ContentSubmission',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='reviews',
|
||||
help_text="ContentSubmission that created this review"
|
||||
)
|
||||
|
||||
# Photos related to this review (via media.Photo model with generic relation)
|
||||
photos = GenericRelation('media.Photo')
|
||||
|
||||
@@ -124,24 +136,6 @@ class Review(TimeStampedModel):
|
||||
def is_pending(self):
|
||||
"""Check if review is pending moderation."""
|
||||
return self.moderation_status == self.MODERATION_PENDING
|
||||
|
||||
def approve(self, moderator, notes=''):
|
||||
"""Approve the review."""
|
||||
from django.utils import timezone
|
||||
self.moderation_status = self.MODERATION_APPROVED
|
||||
self.moderated_by = moderator
|
||||
self.moderated_at = timezone.now()
|
||||
self.moderation_notes = notes
|
||||
self.save()
|
||||
|
||||
def reject(self, moderator, notes=''):
|
||||
"""Reject the review."""
|
||||
from django.utils import timezone
|
||||
self.moderation_status = self.MODERATION_REJECTED
|
||||
self.moderated_by = moderator
|
||||
self.moderated_at = timezone.now()
|
||||
self.moderation_notes = notes
|
||||
self.save()
|
||||
|
||||
|
||||
class ReviewHelpfulVote(TimeStampedModel):
|
||||
|
||||
378
django/apps/reviews/services.py
Normal file
378
django/apps/reviews/services.py
Normal file
@@ -0,0 +1,378 @@
|
||||
"""
|
||||
Review services for ThrillWiki.
|
||||
|
||||
This module provides business logic for review submissions through the Sacred Pipeline.
|
||||
All reviews must flow through ModerationService to ensure consistency with the rest of the system.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from django.db import transaction
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from apps.moderation.services import ModerationService
|
||||
from apps.reviews.models import Review
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReviewSubmissionService:
|
||||
"""
|
||||
Service class for creating and managing review submissions.
|
||||
|
||||
All reviews flow through the ContentSubmission pipeline, ensuring:
|
||||
- Consistent moderation workflow
|
||||
- FSM state machine transitions
|
||||
- 15-minute lock mechanism
|
||||
- Atomic transaction handling
|
||||
- Automatic versioning via pghistory
|
||||
- Audit trail via ContentSubmission
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create_review_submission(
|
||||
user,
|
||||
entity,
|
||||
rating,
|
||||
title,
|
||||
content,
|
||||
visit_date=None,
|
||||
wait_time_minutes=None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Create a review submission through the Sacred Pipeline.
|
||||
|
||||
This method creates a ContentSubmission with SubmissionItems for each review field.
|
||||
If the user is a moderator, the submission is auto-approved and the Review is created immediately.
|
||||
Otherwise, the submission enters the pending moderation queue.
|
||||
|
||||
Args:
|
||||
user: User creating the review
|
||||
entity: Entity being reviewed (Park or Ride)
|
||||
rating: Rating from 1-5 stars
|
||||
title: Review title
|
||||
content: Review content text
|
||||
visit_date: Optional date of visit
|
||||
wait_time_minutes: Optional wait time in minutes
|
||||
**kwargs: Additional metadata (source, ip_address, user_agent)
|
||||
|
||||
Returns:
|
||||
tuple: (ContentSubmission, Review or None)
|
||||
Review will be None if pending moderation
|
||||
|
||||
Raises:
|
||||
ValidationError: If validation fails
|
||||
"""
|
||||
# Check if user is moderator (for bypass)
|
||||
is_moderator = hasattr(user, 'role') and user.role.is_moderator if user else False
|
||||
|
||||
# Get entity ContentType
|
||||
entity_type = ContentType.objects.get_for_model(entity)
|
||||
|
||||
# Check for duplicate review
|
||||
existing = Review.objects.filter(
|
||||
user=user,
|
||||
content_type=entity_type,
|
||||
object_id=entity.id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise ValidationError(
|
||||
f"User has already reviewed this {entity_type.model}. "
|
||||
f"Use update method to modify existing review."
|
||||
)
|
||||
|
||||
# Build submission items for each review field
|
||||
items_data = [
|
||||
{
|
||||
'field_name': 'rating',
|
||||
'field_label': 'Rating',
|
||||
'old_value': None,
|
||||
'new_value': str(rating),
|
||||
'change_type': 'add',
|
||||
'is_required': True,
|
||||
'order': 0
|
||||
},
|
||||
{
|
||||
'field_name': 'title',
|
||||
'field_label': 'Title',
|
||||
'old_value': None,
|
||||
'new_value': title,
|
||||
'change_type': 'add',
|
||||
'is_required': True,
|
||||
'order': 1
|
||||
},
|
||||
{
|
||||
'field_name': 'content',
|
||||
'field_label': 'Review Content',
|
||||
'old_value': None,
|
||||
'new_value': content,
|
||||
'change_type': 'add',
|
||||
'is_required': True,
|
||||
'order': 2
|
||||
},
|
||||
]
|
||||
|
||||
# Add optional fields if provided
|
||||
if visit_date is not None:
|
||||
items_data.append({
|
||||
'field_name': 'visit_date',
|
||||
'field_label': 'Visit Date',
|
||||
'old_value': None,
|
||||
'new_value': str(visit_date),
|
||||
'change_type': 'add',
|
||||
'is_required': False,
|
||||
'order': 3
|
||||
})
|
||||
|
||||
if wait_time_minutes is not None:
|
||||
items_data.append({
|
||||
'field_name': 'wait_time_minutes',
|
||||
'field_label': 'Wait Time (minutes)',
|
||||
'old_value': None,
|
||||
'new_value': str(wait_time_minutes),
|
||||
'change_type': 'add',
|
||||
'is_required': False,
|
||||
'order': 4
|
||||
})
|
||||
|
||||
# Create submission through ModerationService
|
||||
submission = ModerationService.create_submission(
|
||||
user=user,
|
||||
entity=entity,
|
||||
submission_type='review',
|
||||
title=f"Review: {title[:50]}",
|
||||
description=f"User review for {entity_type.model}: {entity}",
|
||||
items_data=items_data,
|
||||
metadata={
|
||||
'rating': rating,
|
||||
'entity_type': entity_type.model,
|
||||
},
|
||||
auto_submit=True,
|
||||
source=kwargs.get('source', 'api'),
|
||||
ip_address=kwargs.get('ip_address'),
|
||||
user_agent=kwargs.get('user_agent', '')
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Review submission created: {submission.id} by {user.email} "
|
||||
f"for {entity_type.model} {entity.id}"
|
||||
)
|
||||
|
||||
# MODERATOR BYPASS: Auto-approve if user is moderator
|
||||
review = None
|
||||
if is_moderator:
|
||||
logger.info(f"Moderator bypass: Auto-approving submission {submission.id}")
|
||||
|
||||
# Approve through ModerationService (this triggers atomic transaction)
|
||||
submission = ModerationService.approve_submission(submission.id, user)
|
||||
|
||||
# Create the Review record
|
||||
review = ReviewSubmissionService._create_review_from_submission(
|
||||
submission=submission,
|
||||
entity=entity,
|
||||
user=user
|
||||
)
|
||||
|
||||
logger.info(f"Review auto-created for moderator: {review.id}")
|
||||
|
||||
return submission, review
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def _create_review_from_submission(submission, entity, user):
|
||||
"""
|
||||
Create a Review record from an approved ContentSubmission.
|
||||
|
||||
This is called internally when a submission is approved.
|
||||
Extracts data from SubmissionItems and creates the Review.
|
||||
|
||||
Args:
|
||||
submission: Approved ContentSubmission
|
||||
entity: Entity being reviewed
|
||||
user: User who created the review
|
||||
|
||||
Returns:
|
||||
Review: Created review instance
|
||||
"""
|
||||
# Extract data from submission items
|
||||
items = submission.items.all()
|
||||
review_data = {}
|
||||
|
||||
for item in items:
|
||||
if item.field_name == 'rating':
|
||||
review_data['rating'] = int(item.new_value)
|
||||
elif item.field_name == 'title':
|
||||
review_data['title'] = item.new_value
|
||||
elif item.field_name == 'content':
|
||||
review_data['content'] = item.new_value
|
||||
elif item.field_name == 'visit_date':
|
||||
from datetime import datetime
|
||||
review_data['visit_date'] = datetime.fromisoformat(item.new_value).date()
|
||||
elif item.field_name == 'wait_time_minutes':
|
||||
review_data['wait_time_minutes'] = int(item.new_value)
|
||||
|
||||
# Get entity ContentType
|
||||
entity_type = ContentType.objects.get_for_model(entity)
|
||||
|
||||
# Create Review
|
||||
review = Review.objects.create(
|
||||
user=user,
|
||||
content_type=entity_type,
|
||||
object_id=entity.id,
|
||||
submission=submission,
|
||||
moderation_status=Review.MODERATION_APPROVED,
|
||||
moderated_by=submission.reviewed_by,
|
||||
moderated_at=submission.reviewed_at,
|
||||
**review_data
|
||||
)
|
||||
|
||||
# pghistory will automatically track this creation
|
||||
|
||||
logger.info(
|
||||
f"Review created from submission: {review.id} "
|
||||
f"(submission: {submission.id})"
|
||||
)
|
||||
|
||||
return review
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def update_review_submission(review, user, **update_data):
|
||||
"""
|
||||
Update an existing review by creating a new submission.
|
||||
|
||||
This follows the Sacred Pipeline by creating a new ContentSubmission
|
||||
for the update, which must be approved before changes take effect.
|
||||
|
||||
Args:
|
||||
review: Existing Review to update
|
||||
user: User making the update (must be review owner)
|
||||
**update_data: Fields to update (rating, title, content, etc.)
|
||||
|
||||
Returns:
|
||||
ContentSubmission: The update submission
|
||||
|
||||
Raises:
|
||||
ValidationError: If user is not the review owner
|
||||
"""
|
||||
# Verify ownership
|
||||
if review.user != user:
|
||||
raise ValidationError("Only the review owner can update their review")
|
||||
|
||||
# Check if user is moderator (for bypass)
|
||||
is_moderator = hasattr(user, 'role') and user.role.is_moderator if user else False
|
||||
|
||||
# Get entity
|
||||
entity = review.content_object
|
||||
if not entity:
|
||||
raise ValidationError("Reviewed entity no longer exists")
|
||||
|
||||
# Build submission items for changed fields
|
||||
items_data = []
|
||||
order = 0
|
||||
|
||||
for field_name, new_value in update_data.items():
|
||||
if field_name in ['rating', 'title', 'content', 'visit_date', 'wait_time_minutes']:
|
||||
old_value = getattr(review, field_name)
|
||||
|
||||
# Only include if value actually changed
|
||||
if old_value != new_value:
|
||||
items_data.append({
|
||||
'field_name': field_name,
|
||||
'field_label': field_name.replace('_', ' ').title(),
|
||||
'old_value': str(old_value) if old_value else None,
|
||||
'new_value': str(new_value),
|
||||
'change_type': 'modify',
|
||||
'is_required': field_name in ['rating', 'title', 'content'],
|
||||
'order': order
|
||||
})
|
||||
order += 1
|
||||
|
||||
if not items_data:
|
||||
raise ValidationError("No changes detected")
|
||||
|
||||
# Create update submission
|
||||
submission = ModerationService.create_submission(
|
||||
user=user,
|
||||
entity=entity,
|
||||
submission_type='update',
|
||||
title=f"Review Update: {review.title[:50]}",
|
||||
description=f"User updating review #{review.id}",
|
||||
items_data=items_data,
|
||||
metadata={
|
||||
'review_id': str(review.id),
|
||||
'update_type': 'review',
|
||||
},
|
||||
auto_submit=True,
|
||||
source='api'
|
||||
)
|
||||
|
||||
logger.info(f"Review update submission created: {submission.id}")
|
||||
|
||||
# MODERATOR BYPASS: Auto-approve if moderator
|
||||
if is_moderator:
|
||||
submission = ModerationService.approve_submission(submission.id, user)
|
||||
|
||||
# Apply updates to review
|
||||
for item in submission.items.filter(status='approved'):
|
||||
setattr(review, item.field_name, item.new_value)
|
||||
|
||||
review.moderation_status = Review.MODERATION_APPROVED
|
||||
review.moderated_by = user
|
||||
review.save()
|
||||
|
||||
logger.info(f"Review update auto-approved for moderator: {review.id}")
|
||||
else:
|
||||
# Regular user: mark review as pending
|
||||
review.moderation_status = Review.MODERATION_PENDING
|
||||
review.save()
|
||||
|
||||
return submission
|
||||
|
||||
@staticmethod
|
||||
def apply_review_approval(submission):
|
||||
"""
|
||||
Apply an approved review submission.
|
||||
|
||||
This is called by ModerationService when a review submission is approved.
|
||||
For new reviews, creates the Review record.
|
||||
For updates, applies changes to existing Review.
|
||||
|
||||
Args:
|
||||
submission: Approved ContentSubmission
|
||||
|
||||
Returns:
|
||||
Review: The created or updated review
|
||||
"""
|
||||
entity = submission.entity
|
||||
user = submission.user
|
||||
|
||||
if submission.submission_type == 'review':
|
||||
# New review
|
||||
return ReviewSubmissionService._create_review_from_submission(
|
||||
submission, entity, user
|
||||
)
|
||||
elif submission.submission_type == 'update':
|
||||
# Update existing review
|
||||
review_id = submission.metadata.get('review_id')
|
||||
if not review_id:
|
||||
raise ValidationError("Missing review_id in submission metadata")
|
||||
|
||||
review = Review.objects.get(id=review_id)
|
||||
|
||||
# Apply approved changes
|
||||
for item in submission.items.filter(status='approved'):
|
||||
setattr(review, item.field_name, item.new_value)
|
||||
|
||||
review.moderation_status = Review.MODERATION_APPROVED
|
||||
review.moderated_by = submission.reviewed_by
|
||||
review.moderated_at = submission.reviewed_at
|
||||
review.save()
|
||||
|
||||
logger.info(f"Review updated from submission: {review.id}")
|
||||
return review
|
||||
else:
|
||||
raise ValidationError(f"Invalid submission type: {submission.submission_type}")
|
||||
Reference in New Issue
Block a user