views.py
  1  import json
  2  import logging
  3  from datetime import datetime, timedelta
  4  
  5  import b4mad_racing_website.fastlap_app  # noqa: F401
  6  import b4mad_racing_website.pitcrew_app  # noqa: F401
  7  from django.contrib import messages
  8  from django.contrib.auth.mixins import LoginRequiredMixin
  9  from django.forms.models import BaseModelForm
 10  from django.http import Http404, HttpResponse, HttpResponseNotAllowed, JsonResponse
 11  from django.shortcuts import get_object_or_404, redirect, render
 12  from django.urls import reverse
 13  from django.views.generic.base import RedirectView, TemplateView
 14  from django.views.generic.detail import DetailView
 15  from django.views.generic.edit import UpdateView
 16  from django.views.generic.list import ListView
 17  
 18  from telemetry.models import Car, Coach, Driver, Game, Lap, Session, Track
 19  from telemetry.racing_stats import RacingStats
 20  
 21  from .forms import ProfileForm
 22  from .models import Copilot, Profile
 23  
 24  logger = logging.getLogger(__name__)
 25  
 26  
 27  class HomePageView(TemplateView):
 28      template_name = "site/home.html"
 29  
 30      # add the 15 most current sessions to the context
 31      def get_context_data(self, **kwargs):
 32          context = super().get_context_data(**kwargs)
 33  
 34          stats = RacingStats()
 35          combos = stats.combos(range=0.25)
 36          context["combos"] = combos
 37  
 38          return context
 39  
 40  
 41  class AboutPageView(TemplateView):
 42      template_name = "site/about.html"
 43  
 44  
 45  class HelpPageView(TemplateView):
 46      template_name = "site/help.html"
 47  
 48  
 49  class HelpInstallPageView(TemplateView):
 50      template_name = "site/help-install.html"
 51  
 52  
 53  class CopilotsOverviewView(ListView):
 54      model = Copilot
 55      template_name = "copilots/overview.html"
 56  
 57  
 58  class CopilotDetailsView(DetailView):
 59      model = Copilot
 60      template_name = "copilots/details.html"
 61  
 62  
 63  class ProfileRedirectView(RedirectView):
 64      """This view will redirect the user to their own profile page or, if the user is not logged in, to the home page."""
 65  
 66      def get(self, request, *args, **kwargs):
 67          # lets check if the user is logged in
 68          if request.user.is_authenticated:
 69              # if the user is logged in, redirect to their profile page
 70              return redirect(reverse("profile", kwargs={"slug": request.user.username}))
 71  
 72          # if not, redirect to the home page
 73          return redirect(reverse("home"))
 74  
 75  
 76  class ProfileDetailView(DetailView):
 77      model = Profile
 78      template_name = "profile/details.html"
 79  
 80      def get(self, request, *args, **kwargs):
 81          # if the profile cant be found, redirect to the home page
 82          try:
 83              # if the user is logged in, create a profile if it doesn't exist
 84              if request.user.is_authenticated:
 85                  self.object, _created = Profile.objects.get_or_create(user=request.user)
 86              else:
 87                  raise Http404
 88          except Http404:
 89              return redirect(reverse("home"))
 90  
 91          # if the profile is not publicly visible and the user is not the owner of the profile, return 403
 92          if (not self.object.publicly_visible) and (not request.user == self.object.user):
 93              return redirect(reverse("home"))
 94  
 95          context = self.get_context_data(object=self.object)
 96          context["is_myself"] = self.object.user == request.user
 97  
 98          # Let's use the logged in user's profile name to find a Driver
 99          if self.request.user.is_authenticated:
100              try:
101                  context["driver"] = Driver.objects.get(name=self.request.user.profile.mqtt_drivername)
102              except Driver.DoesNotExist:
103                  context["driver"] = None
104                  messages.add_message(
105                      request, messages.WARNING, "We CANT find you mqtt driver name in the telemetry database."
106                  )
107  
108              # now get the last 5 sessons for this driver
109              if context["driver"]:
110                  context["sessions"] = Session.objects.filter(driver=context["driver"]).order_by("-created")[:5]
111  
112                  stats = RacingStats()
113                  circuit_combos = stats.driver_combos(context["driver"])
114                  context["circuit_combos"] = circuit_combos
115  
116                  rally_combos = stats.driver_combos(context["driver"], type="rally")
117                  context["rally_combos"] = rally_combos
118  
119          return self.render_to_response(context)
120  
121  
122  class ProfileUpdateView(LoginRequiredMixin, UpdateView):
123      model = Profile
124      # fields = ["newsletter_allowed", "publicly_visible", "mqtt_drivername"]
125      form_class = ProfileForm
126  
127      def form_valid(self, form: BaseModelForm) -> HttpResponse:
128          # This method is called when valid form data has been POSTed.
129  
130          # FIXME: this has to be done in the model instead of the view
131          # enable the coach and set it to copilots mode
132          mqtt_drivername = self.object.mqtt_drivername
133          driver = Driver.objects.filter(name=mqtt_drivername).first()
134          if driver:
135              self.object.driver = driver
136              self.object.save()
137              coach = Coach.objects.get_or_create(driver=driver)[0]
138              coach.enabled = True
139              coach.mode = Coach.MODE_COPILOTS
140              coach.save()
141          return super().form_valid(form)
142  
143  
144  class ProfileSubscriptionsUpdateView(LoginRequiredMixin, UpdateView):
145      model = Profile
146      fields = ["mqtt_drivername"]
147  
148      def get(self, request, *args, **kwargs):
149          return HttpResponseNotAllowed(["PUT", "DELETE"])
150  
151      def post(self, request, *args, **kwargs):
152          return HttpResponseNotAllowed(["PUT", "DELETE"])
153  
154      # pylint: disable=arguments-differ
155      def put(self, request, *args, **kwargs):
156          # Check if the user is updating their own profile
157          if request.user.username != kwargs["slug"]:
158              return JsonResponse({"status": "error", "message": "You can't update another user's profile"}, status=403)
159  
160          # Check if the content type is application/json
161          if request.content_type != "application/json":
162              return JsonResponse({"status": "error", "message": "Invalid content type"}, status=415)
163  
164          # Get the data from the request
165          try:
166              data = json.loads(request.body)
167              copilot_id = data.get("copilot_id")
168          except json.JSONDecodeError:
169              return JsonResponse({"status": "error", "message": "Invalid JSON data"}, status=400)
170  
171          # Check if the copilot exists
172          try:
173              _ = Copilot.objects.get(pk=copilot_id)
174          except Copilot.DoesNotExist:
175              return JsonResponse({"status": "error", "message": "Copilot does not exist"}, status=404)
176  
177          # Update the user's subscriptions based on the received data
178          request.user.profile.subscribe_copilot(copilot_id)
179          logger.debug("subscribe copilot %s to user %s, saving...", copilot_id, request.user.username)
180  
181          # Return a JSON response with status code 204
182          return JsonResponse({}, status=204)
183  
184      def delete(self, request, *args, **kwargs):
185          """This method will remove the copilot from the user's profile.subscriptions."""
186  
187          # Check if the user is updating their own profile
188          if request.user.username != kwargs["slug"]:
189              return JsonResponse({"status": "error", "message": "You can't update another user's profile"}, status=403)
190  
191          # Get the data from the request
192          try:
193              data = json.loads(request.body)
194              copilot_id = data.get("copilot_id")
195          except json.JSONDecodeError:
196              return JsonResponse({"status": "error", "message": "Invalid JSON data"}, status=400)
197  
198          # Check if the copilot exists
199          try:
200              _ = Copilot.objects.get(pk=copilot_id)
201          except Copilot.DoesNotExist:
202              return JsonResponse({"status": "error", "message": "Copilot does not exist"}, status=404)
203  
204          # delete the copilot from the user's subscriptions
205          request.user.profile.unsubscribe_copilot(copilot_id)
206          logger.debug("subsubscript copilot %s from user %s, saving...", copilot_id, request.user.username)
207  
208          # Return a JSON response with status code 204
209          return JsonResponse({}, status=204)
210  
211  
212  def fastlap(request, template_name="fastlap.html", fastlap_id="", **kwargs):
213      driver_name = ""
214      if request.user.is_authenticated:
215          user_name = request.user.first_name
216          driver = Driver.objects.filter(name=user_name).first()
217          if driver:
218              driver_name = driver.name
219      context = dict()
220      dash_context = request.session.get("django_plotly_dash", dict())
221      dash_context["driver_name"] = driver_name
222      request.session["django_plotly_dash"] = dash_context
223      return render(request, template_name=template_name, context=context)
224  
225  
226  def session(request, template_name="session.html", **kwargs):
227      session_id = kwargs.get("session_id", None)
228      lap = kwargs.get("lap", None)
229      session = get_object_or_404(Session, session_id=session_id)
230      context = {}
231      # if the session has any laps
232      if session.laps.count() > 0:
233          # get all laps with the same game_id / car_id / track_id
234          lap = session.laps.first()
235          track_id = lap.track_id
236          car_id = lap.car_id
237          context["track"] = lap.track
238          context["car"] = lap.car
239  
240          compare_laps = (
241              Lap.objects.filter(car_id=car_id, track_id=track_id)
242              .filter(valid=True)
243              .filter(time__gte=0)
244              .filter(fast_lap__isnull=False)
245              .order_by("time")[:5]
246          )
247      else:
248          compare_laps = []
249  
250      game = session.game
251      if game.name in ["Richard Burns Rally"]:
252          map_data = True
253      else:
254          map_data = False
255  
256      context["session"] = session
257      context["lap_number"] = lap
258      context["compare_laps"] = compare_laps
259      context["map_data"] = map_data
260  
261      return render(request, template_name=template_name, context=context)
262  
263  
264  def sessions(request, template_name="sessions.html", **kwargs):
265      game_id = kwargs.get("game_id", None)
266      car_id = kwargs.get("car_id", None)
267      track_id = kwargs.get("track_id", None)
268  
269      context = {}
270  
271      sessions = []
272      filter = {}
273      if game_id:
274          filter["game_id"] = game_id
275          context["game"] = Game.objects.get(pk=game_id)
276      if car_id:
277          filter["laps__car_id"] = car_id
278          context["car"] = Car.objects.get(pk=car_id)
279      if track_id:
280          filter["laps__track_id"] = track_id
281          context["track"] = Track.objects.get(pk=track_id)
282  
283      # Calculate the start date based on the range
284      start_date = datetime.now() - timedelta(days=14)
285  
286      # Filter laps based on the end time within the range
287      filter["end__gte"] = start_date
288  
289      # get the sessions that are
290      # eager load laps and game
291      sessions = Session.objects.filter(**filter).order_by("-created")
292      sessions = sessions.prefetch_related("game", "laps__car", "laps__track")
293      sessions = sessions.distinct()
294  
295      context["sessions"] = sessions
296  
297      return render(request, template_name=template_name, context=context)
298  
299  
300  def pitcrew_view(request, template_name="pitcrew.html", driver_name="", **kwargs):
301      "Example view that inserts content into the dash context passed to the dash application"
302  
303      # driver_name = request.GET.get("driver", None)
304      if driver_name.lower() == "jim":
305          raise Exception("no jim allowed")
306  
307      driver = get_object_or_404(Driver, name=driver_name)
308      coach = Coach.objects.get_or_create(driver=driver)[0]
309  
310      # https://github.com/GibbsConsulting/django-plotly-dash/issues/378
311      context = {"init_args": {"power-switch": {"value": coach.enabled}}}
312  
313      # create some context to send over to Dash:
314      dash_context = request.session.get("django_plotly_dash", dict())
315      dash_context["driver_pk"] = driver.pk
316      dash_context["enabled"] = coach.enabled
317      request.session["django_plotly_dash"] = dash_context
318  
319      return render(request, template_name=template_name, context=context)
320  
321  
322  def pitcrew_index(request, template_name="pitcrew_index.html", **kwargs):
323      drivers = Driver.objects.order_by("name")
324      drivers_total = drivers.count()
325      context = {
326          "drivers": drivers,
327          "drivers_total": drivers_total,
328      }
329  
330      return render(request, template_name=template_name, context=context)