Skip to content

Commit

Permalink
Major refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
jacklinke committed Oct 11, 2024
1 parent 57177ee commit 9373395
Show file tree
Hide file tree
Showing 22 changed files with 296 additions and 159 deletions.
7 changes: 5 additions & 2 deletions example_project/example/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Generated by Django 4.2.16 on 2024-10-10 21:32
# Generated by Django 4.2.16 on 2024-10-11 16:59

from django.db import migrations, models
import django.db.models.deletion
import src.django_owm.validators


class Migration(migrations.Migration):
Expand Down Expand Up @@ -41,7 +42,8 @@ class Migration(migrations.Migration):
models.DecimalField(
decimal_places=2,
help_text="Latitude of the location, decimal (−90; 90)",
max_digits=5,
max_digits=4,
validators=[src.django_owm.validators.validate_latitude],
verbose_name="Latitude",
),
),
Expand All @@ -51,6 +53,7 @@ class Migration(migrations.Migration):
decimal_places=2,
help_text="Longitude of the location, decimal (−180; 180)",
max_digits=5,
validators=[src.django_owm.validators.validate_longitude],
verbose_name="Longitude",
),
),
Expand Down

This file was deleted.

46 changes: 23 additions & 23 deletions example_project/example/models.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
"""Models for testing the django_owm app."""

from src.django_owm.models import APICallLog as AbstractAPICallLog
from src.django_owm.models import CurrentWeather as AbstractCurrentWeather
from src.django_owm.models import DailyWeather as AbstractDailyWeather
from src.django_owm.models import HourlyWeather as AbstractHourlyWeather
from src.django_owm.models import MinutelyWeather as AbstractMinutelyWeather
from src.django_owm.models import WeatherAlert as AbstractWeatherAlert
from src.django_owm.models import WeatherErrorLog as AbstractWeatherErrorLog
from src.django_owm.models import WeatherLocation as AbstractWeatherLocation
from src.django_owm.models import AbstractAPICallLog
from src.django_owm.models import AbstractCurrentWeather
from src.django_owm.models import AbstractDailyWeather
from src.django_owm.models import AbstractHourlyWeather
from src.django_owm.models import AbstractMinutelyWeather
from src.django_owm.models import AbstractWeatherAlert
from src.django_owm.models import AbstractWeatherErrorLog
from src.django_owm.models import AbstractWeatherLocation


class WeatherLocation(AbstractWeatherLocation): # pylint: disable=R0903
class WeatherLocation(AbstractWeatherLocation):
"""Concrete model for WeatherLocation."""


class CurrentWeather(AbstractCurrentWeather): # pylint: disable=R0903
"""Concrete model for CurrentWeather."""
class CurrentWeather(AbstractCurrentWeather):
"""Concrete model for AbstractCurrentWeather."""


class MinutelyWeather(AbstractMinutelyWeather): # pylint: disable=R0903
"""Concrete model for MinutelyWeather."""
class MinutelyWeather(AbstractMinutelyWeather):
"""Concrete model for AbstractMinutelyWeather."""


class HourlyWeather(AbstractHourlyWeather): # pylint: disable=R0903
"""Concrete model for HourlyWeather."""
class HourlyWeather(AbstractHourlyWeather):
"""Concrete model for AbstractHourlyWeather."""


class DailyWeather(AbstractDailyWeather): # pylint: disable=R0903
"""Concrete model for DailyWeather."""
class DailyWeather(AbstractDailyWeather):
"""Concrete model for AbstractDailyWeather."""


class WeatherAlert(AbstractWeatherAlert): # pylint: disable=R0903
"""Concrete model for WeatherAlert."""
class WeatherAlert(AbstractWeatherAlert):
"""Concrete model for AbstractWeatherAlert."""


class WeatherErrorLog(AbstractWeatherErrorLog): # pylint: disable=R0903
"""Concrete model for WeatherErrorLog."""
class WeatherErrorLog(AbstractWeatherErrorLog):
"""Concrete model for AbstractWeatherErrorLog."""


class APICallLog(AbstractAPICallLog): # pylint: disable=R0903
"""Concrete model for APICallLog."""
class APICallLog(AbstractAPICallLog):
"""Concrete model for AbstractAPICallLog."""
1 change: 1 addition & 0 deletions example_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,5 @@
"OWM_USE_BUILTIN_ADMIN": True,
"OWM_SHOW_MAP": True,
"OWM_USE_UUID": False,
"OWM_USE_BUILTIN_CONCRETE_MODELS": False,
}
18 changes: 9 additions & 9 deletions src/django_owm/admin.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
"""Admin for the django_owm app."""

from django.apps import apps
from django.contrib import admin

from .app_settings import OWM_MODEL_MAPPINGS
from .app_settings import OWM_USE_BUILTIN_ADMIN
from .app_settings import get_model_from_string


if OWM_USE_BUILTIN_ADMIN:
WeatherLocationModel = get_model_from_string(OWM_MODEL_MAPPINGS.get("WeatherLocation"))
CurrentWeatherModel = get_model_from_string(OWM_MODEL_MAPPINGS.get("CurrentWeather"))
MinutelyWeatherModel = get_model_from_string(OWM_MODEL_MAPPINGS.get("MinutelyWeather"))
HourlyWeatherModel = get_model_from_string(OWM_MODEL_MAPPINGS.get("HourlyWeather"))
DailyWeatherModel = get_model_from_string(OWM_MODEL_MAPPINGS.get("DailyWeather"))
WeatherAlertModel = get_model_from_string(OWM_MODEL_MAPPINGS.get("WeatherAlert"))
WeatherErrorLogModel = get_model_from_string(OWM_MODEL_MAPPINGS.get("WeatherErrorLog"))
APICallLogModel = get_model_from_string(OWM_MODEL_MAPPINGS.get("APICallLog"))
WeatherLocationModel = apps.get_model(OWM_MODEL_MAPPINGS.get("WeatherLocation"))
CurrentWeatherModel = apps.get_model(OWM_MODEL_MAPPINGS.get("CurrentWeather"))
MinutelyWeatherModel = apps.get_model(OWM_MODEL_MAPPINGS.get("MinutelyWeather"))
HourlyWeatherModel = apps.get_model(OWM_MODEL_MAPPINGS.get("HourlyWeather"))
DailyWeatherModel = apps.get_model(OWM_MODEL_MAPPINGS.get("DailyWeather"))
WeatherAlertModel = apps.get_model(OWM_MODEL_MAPPINGS.get("WeatherAlert"))
WeatherErrorLogModel = apps.get_model(OWM_MODEL_MAPPINGS.get("WeatherErrorLog"))
APICallLogModel = apps.get_model(OWM_MODEL_MAPPINGS.get("APICallLog"))

if WeatherLocationModel:

Expand Down
26 changes: 20 additions & 6 deletions src/django_owm/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@

# Example:
# DJANGO_OWM = {
# 'OWM_API_KEY': '', # Developer should provide their API key in settings.py
# 'OWM_API_KEY': None, # Developer should provide their API key in settings.py
# 'OWM_API_RATE_LIMITS': {
# 'one_call': {
# 'calls_per_minute': 60,
# 'calls_per_month': 1000000,
# },
# # Future APIs can be added here
# # Future APIs will be added here
# },
# 'OWM_MODEL_MAPPINGS': {
# # Map abstract model names to appname.ModelName
# 'OWM_MODEL_MAPPINGS': { # Maps abstract model names to appname.ModelName
# 'WeatherLocation': 'myapp.MyWeatherLocation',
# 'CurrentWeather': 'myapp.MyCurrentWeather',
# 'MinutelyWeather': 'myapp.MyMinutelyWeather',
Expand All @@ -30,15 +29,16 @@
# },
# 'OWM_BASE_MODEL': models.Model, # Base model for OWM models
# 'OWM_USE_BUILTIN_ADMIN': True, # Use built-in admin for OWM models
# 'OWM_SHOW_MAP': False, # Show map in admin for WeatherLocation
# 'OWM_USE_BUILTIN_CONCRETE_MODELS': False, # Use built-in concrete models
# 'OWM_SHOW_MAP': False, # Show map in admin for AbstractWeatherLocation
# 'OWM_USE_UUID': False, # Use UUIDs with OWM models
# }


class Model(models.Model):
"""Simply provides a base model with a Meta class."""

class Meta: # pylint: disable=R0903
class Meta:
"""Meta options for the base model."""

abstract = True
Expand All @@ -56,6 +56,20 @@ class Meta: # pylint: disable=R0903
OWM_SHOW_MAP = DJANGO_OWM.get("OWM_SHOW_MAP", False)
OWM_USE_UUID = DJANGO_OWM.get("OWM_USE_UUID", False)

OWM_USE_BUILTIN_CONCRETE_MODELS = DJANGO_OWM.get("OWM_USE_BUILTIN_CONCRETE_MODELS", False)

if OWM_USE_BUILTIN_CONCRETE_MODELS:
OWM_MODEL_MAPPINGS = {
"WeatherLocation": "django_owm.WeatherLocation",
"CurrentWeather": "django_owm.CurrentWeather",
"MinutelyWeather": "django_owm.MinutelyWeather",
"HourlyWeather": "django_owm.HourlyWeather",
"DailyWeather": "django_owm.DailyWeather",
"WeatherAlert": "django_owm.WeatherAlert",
"WeatherErrorLog": "django_owm.WeatherErrorLog",
"APICallLog": "django_owm.APICallLog",
}


def get_model_from_string(model_string):
"""Get a model class from a string like 'app_label.model_name'."""
Expand Down
70 changes: 68 additions & 2 deletions src/django_owm/forms.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,86 @@
"""Forms for django_owm."""

from decimal import ROUND_HALF_UP
from decimal import Decimal
from decimal import DecimalException
from typing import Optional
from typing import Union

from django import forms
from django.apps import apps
from django.forms.fields import DecimalField

from .app_settings import OWM_MODEL_MAPPINGS
from .app_settings import get_model_from_string
from .validators import validate_latitude
from .validators import validate_longitude


def quantize_to_2_decimal_places(value: Optional[Union[Decimal, str]]) -> Optional[Decimal]:
"""Quantize a Decimal value to 2 decimal places."""
if value is not None:
if isinstance(value, str):
try:
value = Decimal(value)
except DecimalException:
return value
if isinstance(value, Decimal):
return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
else:
raise ValueError("Value must be a Decimal or a string that can be converted to a Decimal.")
return value


class TrimmedDecimalField(DecimalField):
"""Custom DecimalField that trims the value to 2 decimal places."""

def to_python(self, value):
"""Trim the value to 2 decimal places."""
value = super().to_python(value)
if value is not None:
return quantize_to_2_decimal_places(value)
return value


class WeatherLocationForm(forms.ModelForm):
"""Form for creating or updating a Weather Location."""

latitude = TrimmedDecimalField(
max_digits=5,
decimal_places=2,
required=True,
label="Latitude",
help_text="Enter the latitude of the location (e.g., 40.7128)",
)
longitude = TrimmedDecimalField(
max_digits=5,
decimal_places=2,
required=True,
label="Longitude",
help_text="Enter the longitude of the location (e.g., -74.0060)",
)

class Meta:
"""Meta class for WeatherLocationForm."""

model = get_model_from_string(OWM_MODEL_MAPPINGS.get("WeatherLocation"))
model = apps.get_model(OWM_MODEL_MAPPINGS.get("WeatherLocation"))
fields = ["name", "latitude", "longitude"]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["name"].required = False

def clean_latitude(self):
"""Clean the input latitude value."""
latitude = self.cleaned_data.get("latitude")
if latitude is not None:
# Validate the latitude value is a Decimal within the valid range
validate_latitude(latitude)
return latitude

def clean_longitude(self):
"""Clean the input longitude value."""
longitude = self.cleaned_data.get("longitude")
if longitude is not None:
# Validate the longitude value is a Decimal within the valid range
validate_longitude(longitude)
return longitude
16 changes: 10 additions & 6 deletions src/django_owm/management/commands/create_location.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Management command to create a new weather location."""

from decimal import ROUND_HALF_UP
from decimal import Decimal

from django.apps import apps
from django.core.management.base import BaseCommand

Expand All @@ -9,19 +12,20 @@
class Command(BaseCommand):
"""Management command to create a new weather location."""

help = "Create a new weather location."
help = "Create a new weather location by entering a location name, latitude, and longitude."

def handle(self, *args, **options):
"""Handle the command."""
WeatherLocationModel = apps.get_model(OWM_MODEL_MAPPINGS.get("WeatherLocation")) # pylint: disable=C0103
WeatherLocationModel = apps.get_model(OWM_MODEL_MAPPINGS.get("WeatherLocation"))

name = input("Enter location name: ")
latitude = input("Enter latitude: ")
longitude = input("Enter longitude: ")
timezone = input("Enter timezone (optional): ")

location = WeatherLocationModel.objects.create(
name=name, latitude=latitude, longitude=longitude, timezone=timezone if timezone else None
)
# Trim the latitude and longitude to 2 decimal places
latitude = Decimal(latitude).quantize(Decimal("1e-2"), rounding=ROUND_HALF_UP)
longitude = Decimal(longitude).quantize(Decimal("1e-2"), rounding=ROUND_HALF_UP)

location = WeatherLocationModel.objects.create(name=name, latitude=latitude, longitude=longitude)

self.stdout.write(self.style.SUCCESS(f"Successfully created location '{location.name}' with ID {location.id}."))
7 changes: 4 additions & 3 deletions src/django_owm/management/commands/delete_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.apps import apps
from django.core.management.base import BaseCommand
from django.core.management.base import CommandError

from ...app_settings import OWM_MODEL_MAPPINGS

Expand All @@ -18,13 +19,13 @@ def add_arguments(self, parser):
def handle(self, *args, **options):
"""Handle the command."""
location_id = options["location_id"]
WeatherLocationModel = apps.get_model(OWM_MODEL_MAPPINGS.get("WeatherLocation")) # pylint: disable=C0103
WeatherLocationModel = apps.get_model(OWM_MODEL_MAPPINGS.get("WeatherLocation"))

try:
location = WeatherLocationModel.objects.get(pk=location_id)
except WeatherLocationModel.DoesNotExist:
except WeatherLocationModel.DoesNotExist as exc:
self.stderr.write(self.style.ERROR(f"Location with ID {location_id} does not exist."))
return
raise CommandError(f"Location with ID {location_id} does not exist.") from exc

confirmation = input(f"Are you sure you want to delete location '{location.name}'? (y/N): ")

Expand Down
2 changes: 1 addition & 1 deletion src/django_owm/management/commands/list_locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Command(BaseCommand):

def handle(self, *args, **options):
"""Handle the command."""
WeatherLocationModel = apps.get_model(OWM_MODEL_MAPPINGS.get("WeatherLocation")) # pylint: disable=C0103
WeatherLocationModel = apps.get_model(OWM_MODEL_MAPPINGS.get("WeatherLocation"))
locations = WeatherLocationModel.objects.all()

if not locations:
Expand Down
Loading

0 comments on commit 9373395

Please sign in to comment.