Merge branch '85-fragentyp-ort-suchen' into 'master'
Resolve "Fragentyp: Ort suchen" Closes #85 See merge request enrichment-2024/qivip!103
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
| cryptography | 46.0.3 | UNKNOWN |
|
||||
| daphne | 4.1.2 | BSD License |
|
||||
| django-cleanup | 9.0.0 | MIT License |
|
||||
| geographiclib | 2.1 | MIT |
|
||||
| geopy | 2.4.1 | MIT License |
|
||||
| hyperlink | 21.0.0 | MIT License |
|
||||
| idna | 3.11 | UNKNOWN |
|
||||
| incremental | 24.7.2 | MIT License |
|
||||
@@ -861,12 +861,12 @@ def new_question(request):
|
||||
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
|
||||
return redirect('library:overview_quiz')
|
||||
|
||||
if question_type not in ['multiple_choice', 'true_false','input','order','guess']:
|
||||
if question_type not in ['multiple_choice', 'true_false','input','order','guess','map']:
|
||||
base_url = reverse('library:new_question')
|
||||
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
|
||||
|
||||
if request.method == 'POST':
|
||||
question_text = request.POST.get('question', '')
|
||||
question_text = request.POST.get('question', 'Kein Fragentext angegeben')
|
||||
|
||||
# Handle different question types
|
||||
if question_type == 'true_false':
|
||||
@@ -946,20 +946,27 @@ def new_question(request):
|
||||
max_value = request.POST.get('max')
|
||||
tolerance = request.POST.get('tolerance')
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'correct_answer': correct_answer,
|
||||
'min': min_value,
|
||||
'max': max_value,
|
||||
'tolerance': tolerance,
|
||||
'options': [
|
||||
{'value': correct_answer, 'is_correct': True}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'correct_answer': correct_answer,
|
||||
'min': min_value,
|
||||
'max': max_value,
|
||||
'tolerance': tolerance,
|
||||
'options': [
|
||||
{'value': correct_answer, 'is_correct': True}
|
||||
]
|
||||
}
|
||||
|
||||
elif question_type == 'map':
|
||||
target_location = request.POST.get('location')
|
||||
tolerance_radius = request.POST.get('tolerance_radius')
|
||||
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'target_location': target_location,
|
||||
'tolerance_radius': tolerance_radius
|
||||
}
|
||||
question = QivipQuestion()
|
||||
question.data = json.dumps(json_data)
|
||||
question.quiz_id = quiz
|
||||
@@ -1030,18 +1037,26 @@ def new_question(request):
|
||||
max_value = request.POST.get('max')
|
||||
tolerance = request.POST.get('tolerance')
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': '',
|
||||
'correct_answer': correct_answer,
|
||||
'min': min_value,
|
||||
'max': max_value,
|
||||
'tolerance': tolerance,
|
||||
'options': [
|
||||
{'value': correct_answer, 'is_correct': True}
|
||||
]
|
||||
}
|
||||
'type': question_type,
|
||||
'question': '',
|
||||
'correct_answer': correct_answer,
|
||||
'min': min_value,
|
||||
'max': max_value,
|
||||
'tolerance': tolerance,
|
||||
'options': [
|
||||
{'value': correct_answer, 'is_correct': True}
|
||||
]
|
||||
}
|
||||
standard_time_per_question = 30
|
||||
|
||||
elif question_type == 'map':
|
||||
question_data = {
|
||||
'type': question_type,
|
||||
'question': '',
|
||||
'target_location': "",
|
||||
'tolerance_radius': 90000
|
||||
}
|
||||
standard_time_per_question = 30
|
||||
|
||||
template_name = f'library/question/question_{question_type}.html'
|
||||
context = {
|
||||
@@ -1083,6 +1098,13 @@ def edit_question(request, pk):
|
||||
{'value': 'Falsch', 'is_correct': not correct_answer}
|
||||
]
|
||||
}
|
||||
elif question_type == 'map':
|
||||
question_data = {
|
||||
'type': question_type,
|
||||
'question': question_data.get('question', ''),
|
||||
'target_location': question_data.get('target_location', ''),
|
||||
'tolerance_radius': question_data.get('tolerance_radius', 90000)
|
||||
}
|
||||
else:
|
||||
# For multiple choice questions
|
||||
question_data.setdefault('type', question_type)
|
||||
@@ -1200,17 +1222,27 @@ def edit_question(request, pk):
|
||||
max_value = request.POST.get('max')
|
||||
tolerance = request.POST.get('tolerance')
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'correct_answer': correct_answer,
|
||||
'min': min_value,
|
||||
'max': max_value,
|
||||
'tolerance': tolerance,
|
||||
'options': [
|
||||
{'value': correct_answer, 'is_correct': True}
|
||||
]
|
||||
}
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'correct_answer': correct_answer,
|
||||
'min': min_value,
|
||||
'max': max_value,
|
||||
'tolerance': tolerance,
|
||||
'options': [
|
||||
{'value': correct_answer, 'is_correct': True}
|
||||
]
|
||||
}
|
||||
|
||||
elif question_type == 'map':
|
||||
target_location = request.POST.get('location')
|
||||
tolerance_radius = request.POST.get('tolerance_radius')
|
||||
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'target_location': target_location,
|
||||
'tolerance_radius': tolerance_radius
|
||||
}
|
||||
question.data = json.dumps(json_data)
|
||||
question.time_per_question = request.POST.get('time_per_question', '30')
|
||||
try:
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
from play.models import QuizGame, QuizGameParticipant, QuizAnswer
|
||||
from django.conf import settings
|
||||
from geopy.distance import geodesic
|
||||
|
||||
class GameConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
@@ -31,6 +32,13 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
# Check if game should be deleted
|
||||
await self.cleanup_game()
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def get_question_data(self):
|
||||
quiz_game = QuizGame.objects.get(join_code=self.join_code)
|
||||
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
|
||||
return json.loads(current_question.data)
|
||||
|
||||
async def receive(self, text_data):
|
||||
text_data_json = json.loads(text_data)
|
||||
message_type = text_data_json['type']
|
||||
@@ -84,14 +92,16 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
elif message_type == 'submit_answer':
|
||||
participant_id = text_data_json['participant_id']
|
||||
try:
|
||||
answer = text_data_json['answer']
|
||||
except:
|
||||
answer=-1
|
||||
answer = text_data_json['answer']
|
||||
time_remaining = text_data_json.get('time_remaining', 0)
|
||||
|
||||
# Save answer and update participant score
|
||||
await self.save_answer(participant_id, answer, time_remaining)
|
||||
|
||||
question_data = await self.get_question_data()
|
||||
if question_data['type'] == "map":
|
||||
await self.save_answer_map(participant_id, answer, time_remaining)
|
||||
else:
|
||||
await self.save_answer(participant_id, answer, time_remaining)
|
||||
|
||||
|
||||
# Notify host about the new answer
|
||||
await self.channel_layer.group_send(
|
||||
@@ -182,6 +192,138 @@ class GameConsumer(AsyncWebsocketConsumer):
|
||||
except QuizGame.DoesNotExist:
|
||||
return False
|
||||
|
||||
@database_sync_to_async
|
||||
def save_answer_map(self, participant_id, answer_location, time_remaining):
|
||||
answer_index = 0
|
||||
try:
|
||||
# Get all necessary data first
|
||||
participant = QuizGameParticipant.objects.get(
|
||||
quiz_game__join_code=self.join_code,
|
||||
participant_id=participant_id
|
||||
)
|
||||
quiz_game = participant.quiz_game
|
||||
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
|
||||
question_data = json.loads(current_question.data)
|
||||
target_location = question_data['target_location']
|
||||
tolerance_radius = question_data.get('tolerance_radius', 1000) # Default 1km if not set
|
||||
|
||||
|
||||
# Parse coordinates if they are strings
|
||||
def parse_coords(coord):
|
||||
if isinstance(coord, str):
|
||||
try:
|
||||
# Handle both comma and space separated coordinates
|
||||
if ',' in coord:
|
||||
lat, lng = map(float, [x.strip() for x in coord.split(',')])
|
||||
else:
|
||||
# Try splitting by space if no comma
|
||||
parts = coord.strip().split()
|
||||
if len(parts) >= 2:
|
||||
lat, lng = map(float, parts[:2])
|
||||
else:
|
||||
raise ValueError("Invalid coordinate format")
|
||||
return (lat, lng)
|
||||
except (ValueError, AttributeError) as e:
|
||||
print(f"Error parsing coordinates {coord}: {e}")
|
||||
return None
|
||||
elif isinstance(coord, (list, tuple)) and len(coord) >= 2:
|
||||
return (float(coord[0]), float(coord[1]))
|
||||
return None
|
||||
|
||||
# Parse the coordinates
|
||||
target_location = parse_coords(target_location)
|
||||
answer_location_coords = answer_location
|
||||
answer_location = parse_coords(answer_location)
|
||||
|
||||
|
||||
if not target_location or not answer_location:
|
||||
print("Invalid coordinates, cannot calculate score")
|
||||
is_correct = False
|
||||
score = 0
|
||||
else:
|
||||
|
||||
# Configuration - Make these easily adjustable
|
||||
MAX_DISTANCE_KM = 200 # Maximum distance for scoring (in km)
|
||||
DIRECT_HIT_BONUS = 200 # Extra points for direct hit within tolerance
|
||||
|
||||
try:
|
||||
# Calculate distance
|
||||
distance_km = geodesic(answer_location, target_location).kilometers
|
||||
tolerance_km = float(tolerance_radius) / 1000 # Convert meters to kilometers
|
||||
|
||||
print(f"Distance: {distance_km} km, Tolerance: {tolerance_km} km")
|
||||
|
||||
# Calculate base score with non-linear decrease
|
||||
if distance_km <= tolerance_km:
|
||||
# Direct hit - full points + bonus
|
||||
base_score = 1000 + DIRECT_HIT_BONUS
|
||||
is_correct = True
|
||||
else:
|
||||
if distance_km > MAX_DISTANCE_KM:
|
||||
# Beyond max distance - 0 points
|
||||
base_score = 0
|
||||
is_correct = False
|
||||
else:
|
||||
# Non-linear decrease using exponential falloff
|
||||
progress = (distance_km - tolerance_km) / (MAX_DISTANCE_KM - tolerance_km)
|
||||
falloff = progress ** 1.5 # 1.5 makes the falloff steeper at the beginning
|
||||
base_score = 1000 * (1 - falloff)
|
||||
is_correct = True
|
||||
|
||||
# Apply time penalty (reduces score based on time taken)
|
||||
if is_correct and base_score > 0:
|
||||
# Calculate time taken as a fraction of total time (0-1)
|
||||
time_taken_sec = (int(current_question.time_per_question) * 1000 - time_remaining) / 1000
|
||||
max_time_sec = int(current_question.time_per_question)
|
||||
time_fraction = min(1.0, time_taken_sec / max_time_sec) if max_time_sec > 0 else 1.0
|
||||
|
||||
# Reduce points based on time taken (up to 50% reduction for answering at the last second)
|
||||
time_penalty = base_score * 0.5 * time_fraction # Up to 50% penalty
|
||||
final_score = max(1, base_score - time_penalty) # At least 1 point if correct
|
||||
else:
|
||||
final_score = base_score # 0 points for incorrect answers
|
||||
|
||||
# Apply max score limit after all calculations (including bonus)
|
||||
final_score = min(final_score, 1200) # Cap at 1200 (1000 + 200 bonus)
|
||||
|
||||
score = int(round(final_score))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error calculating score: {e}")
|
||||
is_correct = False
|
||||
score = 0
|
||||
|
||||
# Save the answer
|
||||
answer_obj, created = QuizAnswer.objects.get_or_create(
|
||||
participant=participant,
|
||||
question_index=quiz_game.current_question_index,
|
||||
defaults={
|
||||
'answer_index': answer_index,
|
||||
'is_correct': is_correct,
|
||||
'score': score,
|
||||
'time_remaining': time_remaining
|
||||
}
|
||||
)
|
||||
|
||||
if not created:
|
||||
answer_obj.answer_index = answer_index
|
||||
answer_obj.is_correct = is_correct
|
||||
answer_obj.score = score
|
||||
answer_obj.time_remaining = time_remaining
|
||||
answer_obj.save()
|
||||
|
||||
participant.last_score = score
|
||||
participant.score += score
|
||||
participant.coordinate_answer= answer_location_coords
|
||||
participant.last_answer_correct = is_correct
|
||||
participant.save()
|
||||
|
||||
return True
|
||||
|
||||
except (QuizGameParticipant.DoesNotExist, IndexError):
|
||||
return False
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def save_answer(self, participant_id, answer, time_remaining):
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.7 on 2025-11-01 15:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('play', '0015_alter_quizgame_current_state'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quizgameparticipant',
|
||||
name='coordinate_answer',
|
||||
field=models.CharField(blank=True, max_length=256, null=True),
|
||||
),
|
||||
]
|
||||
@@ -50,6 +50,7 @@ class QuizGameParticipant(models.Model):
|
||||
avatar = models.CharField(max_length=200, blank=True, default="")
|
||||
last_heartbeat = models.DateTimeField(auto_now_add=True)
|
||||
last_answer = models.IntegerField(null=True, blank=True)
|
||||
coordinate_answer = models.CharField(null=True, blank=True,max_length=256)
|
||||
last_answer_correct = models.BooleanField(null=True, blank=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -125,3 +125,12 @@ div.qp-question-container {
|
||||
div.qp-question-container img {
|
||||
@apply max-w-[50vw] max-h-[40vh] mx-auto;
|
||||
}
|
||||
|
||||
div#mapquestion-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
div#map {
|
||||
@apply md:w-[600px] lg:w-[900px] h-[400px]
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
|
||||
<title>qivip</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
</head>
|
||||
<body class="flex flex-col min-h-screen">
|
||||
<main class="grow">
|
||||
|
||||
@@ -98,9 +98,6 @@
|
||||
Eingabe
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
||||
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}"
|
||||
class=" mt-1 bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
||||
Multiple Choice
|
||||
@@ -116,10 +113,15 @@
|
||||
Reihenfolge
|
||||
</a>
|
||||
|
||||
<a href="{% url 'library:new_question' %}?type=guess&quiz_id={{ quiz.id }}"
|
||||
<a href="{% url 'library:new_question' %}?type=guess&quiz_id={{ quiz.id }}"
|
||||
class=" mt-1 bg-pink-100 text-pink-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
||||
Schätzen
|
||||
</a>
|
||||
|
||||
<a href="{% url 'library:new_question' %}?type=map&quiz_id={{ quiz.id }}"
|
||||
class=" mt-1 bg-orange-100 text-orange-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
||||
Karte
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,8 +142,14 @@
|
||||
<div class="flex-grow">
|
||||
<p class="font-medium">{{ question.data.question }}</p>
|
||||
<div class="mt-1">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800 {% elif question.data.type == 'input' %} bg-green-100 text-green-800 {% elif question.data.type == 'true_false' %}bg-purple-100 text-purple-800{% elif question.data.type == 'order' %}bg-yellow-100 text-yellow-800{% else %}bg-pink-100 text-pink-800 {% endif %}">
|
||||
{% if question.data.type == 'multiple_choice' %}Multiple Choice{% elif question.data.type == 'input' %}Eingabe{% elif question.data.type == 'true_false' %}Wahr/Falsch{% elif question.data.type == 'order' %}Reihenfolge{% else %}Schätzen{% endif %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800 {% elif question.data.type == 'input' %} bg-green-100 text-green-800 {% elif question.data.type == 'map' %}bg-orange-100 text-orange-800 {% elif question.data.type == 'true_false' %}bg-purple-100 text-purple-800{% elif question.data.type == 'order' %}bg-yellow-100 text-yellow-800{% else %}bg-pink-100 text-pink-800 {% endif %}">
|
||||
{% if question.data.type == 'multiple_choice' %}Multiple Choice
|
||||
{% elif question.data.type == 'input' %}Eingabe
|
||||
{% elif question.data.type == 'map' %}Karte
|
||||
{% elif question.data.type == 'true_false' %}Wahr/Falsch
|
||||
{% elif question.data.type == 'order' %}Reihenfolge
|
||||
{% else %}Schätzen
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
@@ -173,8 +181,46 @@
|
||||
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% elif question.data.type == 'map' and detail %}
|
||||
<div class="mt-3">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
<span class="font-medium">Zielort:</span>
|
||||
<span id="target-location-{{ forloop.counter }}">
|
||||
{% if question.data.target_location %}
|
||||
{{ question.data.target_location }}
|
||||
{% else %}
|
||||
Kein Zielort festgelegt
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% if question.data.target_location %}
|
||||
<script>
|
||||
// Format the coordinates to 4 decimal places
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const targetEl = document.getElementById('target-location-{{ forloop.counter }}');
|
||||
if (targetEl) {
|
||||
const coords = targetEl.textContent.trim().split(',');
|
||||
if (coords.length === 2) {
|
||||
const lat = parseFloat(coords[0]).toFixed(4);
|
||||
const lng = parseFloat(coords[1]).toFixed(4);
|
||||
targetEl.textContent = `${lat}, ${lng}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% if question.data.tolerance_radius %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300 mt-1">
|
||||
<span class="font-medium">Toleranzradius:</span> {{ question.data.tolerance_radius }} Meter
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if question.image.image.url %}
|
||||
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4" />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif question.data.type == 'order' %}
|
||||
<div class="w-full gap-4 md:flex justify-between ">
|
||||
<div class="w-full gap-4 md:flex justify-between ">
|
||||
<ul class="space-y-1 ">
|
||||
{% for option in question.data.options|dictsort:"order" %}
|
||||
<li class="flex items-center text-sm">
|
||||
@@ -188,7 +234,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{% elif question.data.type == 'input' %}
|
||||
<div class="w-full gap-4 md:flex justify-between ">
|
||||
<ul class="space-y-1 ">
|
||||
|
||||
166
django/templates/library/question/question_map.html
Normal file
166
django/templates/library/question/question_map.html
Normal file
@@ -0,0 +1,166 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
<div id="mapquestion-container">
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold dark:text-white">{% if question %}Frage bearbeiten{% else %}Neue Karten Frage{% endif %}</h1>
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
|
||||
Zurück zum Quiz
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
|
||||
<form method="post" class="space-y-6" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Question Text -->
|
||||
<div>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700 dark:text-white">Frage</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="question" id="question" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ question.data.question }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if image %}
|
||||
<img src="{{ image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
|
||||
<label for="reset_image">
|
||||
<input type="checkbox" name="reset_ques_image" id="reset_ques_image">
|
||||
Bild zurücksetzen
|
||||
</label>
|
||||
{% endif %}
|
||||
<div>
|
||||
<label class="font-bold" for="question_image">Bild:</label>
|
||||
<input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="question_image" id="question_image">
|
||||
</div>
|
||||
|
||||
<label for="map" class="block text-sm font-medium text-gray-700 dark:text-white">Ziel auswählen</label>
|
||||
<div id="map"></div>
|
||||
|
||||
<label for="location" class="block text-sm font-medium text-gray-700 dark:text-white">Koordinaten</label>
|
||||
<div class="mt-1">
|
||||
<textarea id="location" name="location" rows="2" required
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
|
||||
>{{ question.data.target_location }}</textarea>
|
||||
</div>
|
||||
|
||||
<label for="tolerance_radius" class="block text-sm font-medium text-gray-700 dark:text-white">Radius (Meter) für volle Punktzahl</label>
|
||||
<div class="mt-1">
|
||||
<textarea id="tolerance_radius" name="tolerance_radius" rows="2" required
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
|
||||
>{{ question.data.tolerance_radius }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<label class="font-bold" for="time_per_question">Zeit pro Frage:</label>
|
||||
<select name="time_per_question" id="time_per_question" class="dark:text-white dark:bg-[#2a2f3a] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
|
||||
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
|
||||
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
|
||||
<option value="45" {% if standard_time_per_question == "45" or time_per_question == "45" %}selected{% endif %}>45 Sekunden</option>
|
||||
<option value="60" {% if standard_time_per_question == "60" or time_per_question == "60" %}selected{% endif %}>1 Minute</option>
|
||||
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
|
||||
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<label for="credits" class="block text-sm font-medium text-gray-700 dark:text-white">Credits</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="credits" rows="4"
|
||||
class="dark:bg-[#2a2f3a] shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md "
|
||||
>{{ credits }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{% if question %}Speichern{% else %}Erstellen{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let marker = null;
|
||||
let circle = null;
|
||||
const locationInput = document.getElementById('location');
|
||||
const radiusInput = document.getElementById('tolerance_radius');
|
||||
|
||||
const map = L.map('map').setView([51.72, 10.45], 3);
|
||||
|
||||
// Prüfe, ob Dark Mode aktiv ist
|
||||
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Wähle das passende Karten-Design
|
||||
const tileUrl = isDarkMode
|
||||
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png';
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap © CARTO'
|
||||
}).addTo(map);
|
||||
|
||||
function updateCircle(latlng) {
|
||||
const radius = parseInt(radiusInput.value) || 100;
|
||||
if (circle) {
|
||||
map.removeLayer(circle);
|
||||
}
|
||||
circle = L.circle(latlng, {
|
||||
color: 'blue',
|
||||
fillColor: '#87CEEB',
|
||||
fillOpacity: 0.2,
|
||||
radius: radius
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
function updateInput(latlng) {
|
||||
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
|
||||
if (marker) {
|
||||
updateCircle(marker.getLatLng());
|
||||
}
|
||||
}
|
||||
|
||||
function placeOrMoveMarker(latlng) {
|
||||
if (marker) {
|
||||
marker.setLatLng(latlng);
|
||||
} else {
|
||||
marker = L.marker(latlng, { draggable: true }).addTo(map);
|
||||
|
||||
marker.on('dragend', () => {
|
||||
updateInput(marker.getLatLng());
|
||||
});
|
||||
}
|
||||
updateInput(latlng);
|
||||
}
|
||||
|
||||
// Update circle when radius changes
|
||||
radiusInput.addEventListener('input', () => {
|
||||
if (marker) {
|
||||
updateCircle(marker.getLatLng());
|
||||
}
|
||||
});
|
||||
|
||||
// Place marker at existing coordinates if available
|
||||
const existingCoords = '{{ question.data.target_location }}';
|
||||
if (existingCoords) {
|
||||
const [lat, lng] = existingCoords.split(',').map(Number);
|
||||
placeOrMoveMarker({ lat, lng });
|
||||
map.setView([lat, lng], 6); // Zoom in to show the marker
|
||||
}
|
||||
|
||||
map.on('click', e => placeOrMoveMarker(e.latlng));
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -50,8 +50,7 @@
|
||||
|
||||
|
||||
</div>
|
||||
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
|
||||
|
||||
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
|
||||
<div class="grid grid-cols-2 gap-4" id="options-container">
|
||||
{% for option in question_data.options %}
|
||||
<button
|
||||
@@ -162,8 +161,93 @@ class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-
|
||||
Abschicken
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% elif question_data.type == "guess" %}
|
||||
|
||||
{% endif %}
|
||||
{% elif question_data.type == "map" and request.session.mode == "learn" %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">Ziel auswählen</div>
|
||||
<div id="map" class="h-96"></div>
|
||||
<input type="hidden" value="" id="singleanswer" name="singleanswer">
|
||||
<div class="mt-4">
|
||||
<button onclick="submitAnswerMap()" class="bg-blue-400 text-white px-4 py-2 rounded-md hover:bg-blue-500 transition-colors">
|
||||
Abgeben
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" id="location" name="location" value="">
|
||||
<script>
|
||||
let marker = null;
|
||||
const locationInput = document.getElementById('location');
|
||||
const map = L.map('map').setView([51.72, 10.45], 3);
|
||||
|
||||
// Prüfe, ob Dark Mode aktiv ist
|
||||
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Wähle das passende Karten-Design
|
||||
const tileUrl = isDarkMode
|
||||
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_nolabels/{z}/{x}/{y}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_nolabels/{z}/{x}/{y}.png';
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap © CARTO'
|
||||
}).addTo(map);
|
||||
|
||||
function updateInput(latlng) {
|
||||
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
|
||||
}
|
||||
|
||||
function placeOrMoveMarker(latlng) {
|
||||
if (marker) {
|
||||
marker.setLatLng(latlng);
|
||||
} else {
|
||||
marker = L.marker(latlng, { draggable: true }).addTo(map);
|
||||
marker.on('dragend', () => {
|
||||
updateInput(marker.getLatLng());
|
||||
});
|
||||
}
|
||||
updateInput(latlng);
|
||||
}
|
||||
|
||||
map.on('click', e => placeOrMoveMarker(e.latlng));
|
||||
|
||||
function submitAnswerMap() {
|
||||
const answer = document.getElementById("location").value;
|
||||
|
||||
if (answer === "") {
|
||||
alert("Bitte einen Ort auf der Karte auswählen!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add grey overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.width = '100%';
|
||||
overlay.style.height = '100%';
|
||||
overlay.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
|
||||
overlay.style.zIndex = '1000';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.style.justifyContent = 'center';
|
||||
overlay.style.alignItems = 'center';
|
||||
overlay.style.color = 'white';
|
||||
overlay.style.fontSize = '24px';
|
||||
overlay.style.fontWeight = 'bold';
|
||||
overlay.textContent = 'Antwort übermittelt';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const now = Date.now();
|
||||
const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
|
||||
|
||||
// Submit the answer
|
||||
submitAnswer(answer);
|
||||
}
|
||||
</script>
|
||||
{% elif question_data.type == "map" and request.session.mode != "learn" %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2 text-center">Bitte den gesuchten Ort auf der Karte auswählen</p>
|
||||
{% elif question_data.type == "guess" %}
|
||||
{% if request.session.mode == "learn" %}
|
||||
<form class="flex flex-col items-center gap-4 mt-4">
|
||||
<!-- Slider mit Beschriftung -->
|
||||
@@ -193,11 +277,7 @@ class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-
|
||||
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -232,7 +312,7 @@ class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
submitAnswer( -1 );
|
||||
submitAnswer(-1);
|
||||
window.location.href = data.redirect_url;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
<div class="w-full bg-gray-300 rounded-full h-4 my-4">
|
||||
<div id="time-bar" class="bg-blue-600 h-4 rounded-full transition-all duration-100 linear" style="width: 100%;"></div>
|
||||
</div>
|
||||
|
||||
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
|
||||
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
|
||||
<div class="grid grid-cols-2 gap-4" id="options-container">
|
||||
{% for option in question_data.options %}
|
||||
<button data-index="{{ forloop.counter0 }}"
|
||||
@@ -168,6 +167,58 @@
|
||||
Abschicken
|
||||
</button>
|
||||
</form>
|
||||
{% elif question_data.type == "map" %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">Ziel auswählen</div>
|
||||
<div id="map" class="h-96"></div>
|
||||
<input type="hidden" value="" id="location" name="location">
|
||||
<div class="mt-4">
|
||||
<button onclick="submitAnswerMap()" class="bg-blue-400 text-white px-4 py-2 rounded-md hover:bg-blue-500 transition-colors">
|
||||
Abgeben
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
let marker = null;
|
||||
const locationInput = document.getElementById('location');
|
||||
|
||||
const map = L.map('map').setView([51.72, 10.45], 3);
|
||||
|
||||
// Prüfe, ob Dark Mode aktiv ist
|
||||
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Wähle das passende Karten-Design
|
||||
const tileUrl = isDarkMode
|
||||
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_nolabels/{z}/{x}/{y}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_nolabels/{z}/{x}/{y}.png';
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap © CARTO'
|
||||
}).addTo(map);
|
||||
|
||||
function updateInput(latlng) {
|
||||
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
|
||||
}
|
||||
|
||||
function placeOrMoveMarker(latlng) {
|
||||
if (marker) {
|
||||
marker.setLatLng(latlng);
|
||||
} else {
|
||||
marker = L.marker(latlng, { draggable: true }).addTo(map);
|
||||
|
||||
marker.on('dragend', () => {
|
||||
updateInput(marker.getLatLng());
|
||||
});
|
||||
}
|
||||
updateInput(latlng);
|
||||
}
|
||||
|
||||
map.on('click', e => placeOrMoveMarker(e.latlng));
|
||||
</script>
|
||||
{% else %}
|
||||
<p>aaaaah gibts nicht</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,7 +244,7 @@
|
||||
window.location.href = data.redirect_url;
|
||||
{% if question_data.type == "order" %}
|
||||
submitOrderAnswer();
|
||||
{% else %}
|
||||
{% elif question_data.type != "map" %}
|
||||
submitAnswer(-1);
|
||||
{% endif %}
|
||||
}
|
||||
@@ -203,7 +254,22 @@
|
||||
wsUtil.handleClose(e);
|
||||
};
|
||||
|
||||
function submitAnswer(optionIndex) {
|
||||
function submitAnswer(optionIndex=0, has_options=true) {
|
||||
if (!has_options){
|
||||
answer = document.getElementById("singleanswer").value;
|
||||
|
||||
if (answer == ""){
|
||||
alert("Bitte eine Antwort eingeben!");
|
||||
return;
|
||||
}
|
||||
|
||||
gameSocket.send(JSON.stringify({
|
||||
type: 'submit_answer',
|
||||
participant_id: participantId,
|
||||
answer:answer,
|
||||
time_remaining: questionDuration
|
||||
}));
|
||||
}
|
||||
if (hasAnswered) return;
|
||||
let input = ""; // Declare input here
|
||||
|
||||
@@ -249,7 +315,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
function submitAnswerMap() {
|
||||
answer = document.getElementById("location").value;
|
||||
|
||||
if (answer == ""){
|
||||
alert("Bitte eine Antwort eingeben!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add grey overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.width = '100%';
|
||||
overlay.style.height = '100%';
|
||||
overlay.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
|
||||
overlay.style.zIndex = '1000';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.style.justifyContent = 'center';
|
||||
overlay.style.alignItems = 'center';
|
||||
overlay.style.color = 'white';
|
||||
overlay.style.fontSize = '24px';
|
||||
overlay.style.fontWeight = 'bold';
|
||||
overlay.textContent = 'Antwort übermittelt';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const now = Date.now();
|
||||
const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
|
||||
|
||||
gameSocket.send(JSON.stringify({
|
||||
type: 'submit_answer',
|
||||
participant_id: participantId,
|
||||
answer: answer,
|
||||
time_remaining: timeRemaining
|
||||
}));
|
||||
}
|
||||
|
||||
// Timer
|
||||
function updateTimer() {
|
||||
|
||||
@@ -9,9 +9,129 @@
|
||||
|
||||
<h2 class="text-xl font-bold mb-4">Aktuelle Frage</h2>
|
||||
<p class="text-lg mb-2">{{ question_data.question }}</p>
|
||||
{% if question_data.type != "order" and question_data.type != "guess" %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
|
||||
{% if question_data.type == "map" %}
|
||||
<!-- Map Question Display -->
|
||||
<div class="mb-4">
|
||||
<div id="map" class="h-96 w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
<script>
|
||||
|
||||
const participantAnswers = [
|
||||
{% for participant in participants %}
|
||||
{% if participant.coordinate_answer %}
|
||||
"{{ participant.coordinate_answer }}",
|
||||
{% endif %}
|
||||
{% endfor %}];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Parse the target location from the string format "lat,lng"
|
||||
const targetCoords = '{{ question_data.target_location|escapejs }}'.split(',');
|
||||
let targetLat, targetLng;
|
||||
|
||||
if (targetCoords.length === 2) {
|
||||
targetLat = parseFloat(targetCoords[0].trim());
|
||||
targetLng = parseFloat(targetCoords[1].trim());
|
||||
|
||||
// Normalize longitude to be within -180 to 180
|
||||
targetLng = ((targetLng % 360) + 540) % 360 - 180;
|
||||
|
||||
// Clamp latitude to valid range (-90 to 90)
|
||||
targetLat = Math.max(-90, Math.min(90, targetLat));
|
||||
|
||||
console.log('Normalized coordinates:', { lat: targetLat, lng: targetLng });
|
||||
}
|
||||
|
||||
// Validate coordinates
|
||||
if (isNaN(targetLat) || isNaN(targetLng)) {
|
||||
console.warn('Invalid coordinates, hiding map');
|
||||
document.getElementById('map-container').style.display = 'none';
|
||||
return; // Exit the function early
|
||||
}
|
||||
|
||||
const targetLocation = [targetLat, targetLng];
|
||||
console.log('Using coordinates:', targetLocation);
|
||||
|
||||
// Initialize map centered on target location
|
||||
const map = L.map('map').setView(targetLocation, 7);
|
||||
|
||||
// Check for dark mode
|
||||
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
// Choose appropriate map style
|
||||
const tileUrl = isDarkMode
|
||||
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}.png'
|
||||
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png';
|
||||
|
||||
L.tileLayer(tileUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap © CARTO',
|
||||
noWrap: true
|
||||
}).addTo(map);
|
||||
|
||||
participantAnswers.forEach(coordStr => {
|
||||
const coords = coordStr.split(',').map(Number);
|
||||
if (coords.length === 2 && !coords.some(isNaN)) {
|
||||
const marker = L.marker(coords).addTo(map).bindPopup('Antwort eines Teilnehmers');
|
||||
|
||||
// Linie vom Ziel zum Teilnehmermarker
|
||||
L.polyline([targetLocation, coords], {
|
||||
color: '#f59e0b',
|
||||
weight: 2, // Linienstärke
|
||||
dashArray: '5,5', // gestrichelte Linie
|
||||
opacity: 0.6
|
||||
}).addTo(map);
|
||||
}
|
||||
});
|
||||
|
||||
// Add target marker
|
||||
L.marker(targetLocation, {
|
||||
icon: L.divIcon({
|
||||
html: '🎯',
|
||||
className: 'text-3xl',
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
popupAnchor: [0, -32]
|
||||
})
|
||||
}).addTo(map)
|
||||
.bindPopup('Zielort')
|
||||
.openPopup();
|
||||
|
||||
// Add tolerance radius circle if available
|
||||
{% if question_data.tolerance_radius %}
|
||||
(function() {
|
||||
try {
|
||||
const toleranceRadius = parseFloat('{{ question_data.tolerance_radius|escapejs }}'.replace(',', '.'));
|
||||
if (!isNaN(toleranceRadius) && toleranceRadius > 0) {
|
||||
L.circle(targetLocation, {
|
||||
color: isDarkMode ? '#3b82f6' : '#2563eb',
|
||||
fillColor: isDarkMode ? '#3b82f6' : '#2563eb',
|
||||
fillOpacity: 0.2,
|
||||
radius: toleranceRadius
|
||||
}).addTo(map);
|
||||
console.log('Added tolerance radius:', toleranceRadius);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error adding tolerance radius:', e);
|
||||
}
|
||||
})();
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% elif question_data.type != "order" and question_data.type != "guess" %}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% for option in question_data.options %}
|
||||
|
||||
<div id="option-box-{{ forloop.counter0 }}" class=" dark:bg-transparent relative p-4 rounded-lg overflow-auto break-words hyphens-auto {% if option.is_correct %} border-2 border-green-500{% else %}border-2 border-red-500{% endif %}">
|
||||
@@ -25,7 +145,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif question_data.type == "guess" %}
|
||||
{% elif question_data.type == "guess" %}
|
||||
<div id="option-box-0" class="relative p-4 rounded-lg border-2 border-green-500 overflow-auto">
|
||||
<div id="option-fill-0" class="absolute left-0 top-0 h-full bg-green-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
|
||||
<div class="relative z-10">
|
||||
|
||||
@@ -12,3 +12,5 @@ qrcode~=7.4.2
|
||||
reportlab~=4.2.0
|
||||
pip-licenses ~= 5.0.0
|
||||
requests
|
||||
requests
|
||||
geopy
|
||||
|
||||
Reference in New Issue
Block a user