models.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import os
  2. import time
  3. from datetime import datetime, timedelta
  4. import dateutil
  5. from django.db import models
  6. from django.utils.translation import ugettext_lazy as _
  7. from django.core.urlresolvers import reverse
  8. from django.contrib.auth.models import User
  9. # from template_utils.markup import formatter
  10. import csv
  11. from directory.models import Point, Place, Town
  12. from utils.moon_phase import moon_phase
  13. class StandardMetadata(models.Model):
  14. """
  15. A basic (abstract) model for metadata.
  16. Included in each model file to maintain application separation.
  17. Subclass new models from 'StandardMetadata' instead of 'models.Model'.
  18. """
  19. created = models.DateTimeField(default=datetime.now, editable=False)
  20. updated = models.DateTimeField(default=datetime.now, editable=False)
  21. class Meta:
  22. abstract = True
  23. def save(self, *args, **kwargs):
  24. self.updated = datetime.now()
  25. super(StandardMetadata, self).save(*args, **kwargs)
  26. class WeatherAlert(models.Model):
  27. """ WeatherAlert model.
  28. Based on the NOAA weather alert feed. """
  29. zone_id = models.CharField(_("NOAA zone id"), max_length=6)
  30. zone = models.CharField(_("NOAA zone"), max_length=255)
  31. updated = models.DateTimeField(_("updated"))
  32. summary = models.TextField(_("summary"), blank=True, null=True)
  33. effective = models.DateTimeField(_("effective time"))
  34. expires = models.DateTimeField(_("expiration time"))
  35. status = models.CharField(_("status"), max_length=30, blank=True, null=True)
  36. msg_type = models.CharField(_("message type"), max_length=30, blank=True, null=True)
  37. urgency = models.CharField(_("urgency"), max_length=30, blank=True, null=True)
  38. severity = models.CharField(_("severity"), max_length=30, blank=True, null=True)
  39. certainty = models.CharField(_("certainty"), max_length=30, blank=True, null=True)
  40. area = models.TextField(_("affected area"), blank=True, null=True)
  41. class Meta:
  42. verbose_name = _("weather alert")
  43. verbose_name_plural = _("weather alerts")
  44. ordering = (
  45. "effective",
  46. "expires",
  47. )
  48. get_latest_by = "effective"
  49. def __unicode__(self):
  50. return u"Weather alert for %s until %s" % (self.zone, self.expires)
  51. class WeatherConditions(models.Model):
  52. """ WeatherConditions model."""
  53. town = models.ForeignKey(Town)
  54. observation_time = models.DateTimeField(_("observation time"))
  55. conditions = models.CharField(_("conditions"), max_length=140)
  56. temperature = models.FloatField(_("temperature (F)"))
  57. wind_dir = models.CharField(_("wind direction"), max_length=5)
  58. wind_speed = models.FloatField(_("wind speed (MPH)"))
  59. pressure = models.CharField(_("pressure (in)"), max_length=30)
  60. humidity = models.CharField(_("humitidy"), max_length=4)
  61. class Meta:
  62. verbose_name = _("weather condition")
  63. verbose_name_plural = _("weather conditions")
  64. ordering = ("observation_time",)
  65. get_latest_by = "observation_time"
  66. def __unicode__(self):
  67. return u"%s %s" % (self.town, self.observation_time)
  68. class WeatherForecast(models.Model):
  69. town = models.ForeignKey(Town)
  70. observation_time = models.DateTimeField(_("observation time (RFC822)"))
  71. period = models.CharField(_("period"), max_length=15)
  72. conditions = models.CharField(_("conditions"), max_length=255)
  73. high_temperature_f = models.IntegerField(
  74. _("high temperature (F)"), max_length=3, blank=True, null=True
  75. )
  76. low_temperature_f = models.IntegerField(
  77. _("low temperature (F)"), max_length=3, blank=True, null=True
  78. )
  79. wind_conditions = models.CharField(
  80. _("wind conditions"), max_length=100, blank=True, null=True
  81. )
  82. class Meta:
  83. verbose_name = _("weather condition")
  84. verbose_name_plural = _("weather conditions")
  85. ordering = ("observation_time",)
  86. get_latest_by = "observation_time"
  87. def __unicode__(self):
  88. return u"%s %s" % (self.town, self.observation_time)
  89. HEMISPHERES = (
  90. ("N", _("North")),
  91. ("S", _("South")),
  92. )
  93. MOON_PHASES = (
  94. (0, _("New")),
  95. (1, _("1st Q.")),
  96. (2, _("Full")),
  97. (3, _("3rd Q.")),
  98. )
  99. class AstroChart(models.Model):
  100. year = models.IntegerField(_("year"), max_length=4)
  101. dst_start = models.DateField(_("daylight savings start"))
  102. dst_end = models.DateField(_("daylight savings end"))
  103. hemisphere = models.CharField(max_length=1, choices=HEMISPHERES, default="N")
  104. class Meta:
  105. verbose_name = _("astronomical chart")
  106. verbose_name_plural = _("astronomical charts ")
  107. ordering = ("-year",)
  108. get_latest_by = "year"
  109. def __unicode__(self):
  110. return u"%s Astronomical Chart for the %s Hemisphere" % (
  111. self.year,
  112. self.hemisphere,
  113. )
  114. class Day(models.Model):
  115. date = models.DateField(_("date"))
  116. chart = models.ForeignKey(AstroChart)
  117. sunrise = models.TimeField(_("sunrise"), null=True, blank=True)
  118. sunset = models.TimeField(_("sunset"), null=True, blank=True)
  119. moonrise = models.TimeField(_("moonrise"), null=True, blank=True)
  120. moonset = models.TimeField(_("moonset"), null=True, blank=True)
  121. moonphase = models.IntegerField(
  122. _("moon phase"), choices=MOON_PHASES, null=True, blank=True
  123. )
  124. moonphase_time = models.TimeField(_("moon phase time"), null=True, blank=True)
  125. moonphase_fraction = models.FloatField(
  126. _("moon phase fraction"), null=True, blank=True
  127. )
  128. civil_twilight_am = models.TimeField(
  129. _("morning civil twilight"), null=True, blank=True
  130. )
  131. civil_twilight_pm = models.TimeField(
  132. _("evening civil twilight"), null=True, blank=True
  133. )
  134. nautical_twilight_am = models.TimeField(
  135. _("morning nautical twilight"), null=True, blank=True
  136. )
  137. nautical_twilight_pm = models.TimeField(
  138. _("evening nautical twilight"), null=True, blank=True
  139. )
  140. daily_avg_temp = models.DecimalField(
  141. _("daily average temperature"),
  142. null=True,
  143. blank=True,
  144. decimal_places=2,
  145. max_digits=5,
  146. )
  147. daily_high_temp = models.DecimalField(
  148. _("daily high temperature"),
  149. null=True,
  150. blank=True,
  151. decimal_places=2,
  152. max_digits=5,
  153. )
  154. daily_low_temp = models.DecimalField(
  155. _("daily low temperature"),
  156. null=True,
  157. blank=True,
  158. decimal_places=2,
  159. max_digits=5,
  160. )
  161. monthly_avg_temp = models.DecimalField(
  162. _("monthly average temperature"),
  163. null=True,
  164. blank=True,
  165. decimal_places=2,
  166. max_digits=5,
  167. )
  168. yearly_avg_temp = models.DecimalField(
  169. _("yearly average temperature"),
  170. null=True,
  171. blank=True,
  172. decimal_places=2,
  173. max_digits=5,
  174. )
  175. class Meta:
  176. verbose_name = _("day")
  177. verbose_name_plural = _("days")
  178. ordering = ("date",)
  179. get_latest_by = "date"
  180. def __unicode__(self):
  181. return u"%s" % (self.date)
  182. TIDE_STATES = (
  183. ("H", _("High")),
  184. ("L", _("Low")),
  185. )
  186. class TideChart(StandardMetadata):
  187. place = models.ForeignKey(Place)
  188. year = models.CharField(max_length=4)
  189. mean_range = models.FloatField()
  190. spring_range = models.FloatField()
  191. mean_tide_level = models.FloatField()
  192. public = models.BooleanField(_("public"), default=False)
  193. reference = models.ForeignKey(
  194. "self", blank=True, null=True, related_name="ref_chart"
  195. )
  196. high_tide_offset = models.FloatField(_("high tide offset"), blank=True, null=True)
  197. low_tide_offest = models.FloatField(_("low tide offset"), blank=True, null=True)
  198. high_time_offset = models.IntegerField(
  199. _("high time offset"), blank=True, null=True, max_length=2
  200. )
  201. low_time_offset = models.IntegerField(
  202. _("low time offset"), blank=True, null=True, max_length=2
  203. )
  204. class Meta:
  205. verbose_name = _("tide chart")
  206. verbose_name_plural = _("tide charts")
  207. ordering = ("place",)
  208. def __unicode__(self):
  209. return u"%s %s %s" % (self.place.title, "Tide Chart", self.year)
  210. def get_absolute_url(self):
  211. args = self.place.slug + "-" + self.year
  212. return reverse("al-tide-chart-detail", args=args)
  213. def save(self, *args, **kwargs):
  214. super(TideChart, self).save(*args, **kwargs)
  215. if self.reference:
  216. if not TideDay.objects.filter(tide_chart=self):
  217. # There are no days for this chart and it's referenced from another one
  218. # Let's generate the days
  219. self.generate_tidedays(self.reference.tideday_set.all())
  220. def generate_tidedays(self, tidedays):
  221. fill_tide = Tide.objects.create(time="12:00", level=0.0, state="L")
  222. overflow = False
  223. for ref_day in tidedays:
  224. new_day = TideDay.objects.create(day=ref_day.day, tide_chart=self)
  225. if overflow: # We overflowed from the previous day, so offset tides
  226. new_day.tide_1 = self.generate_tide(fill_tide)
  227. new_day.tide_2 = self.generate_tide(ref_day.tide_1)
  228. new_day.tide_3 = self.generate_tide(ref_day.tide_2)
  229. new_day.tide_4 = self.generate_tide(ref_day.tide_3)
  230. overflow = False # Reset the overflow flag
  231. else:
  232. new_day.tide_1 = self.generate_tide(ref_day.tide_1)
  233. new_day.tide_2 = self.generate_tide(ref_day.tide_2)
  234. new_day.tide_3 = self.generate_tide(ref_day.tide_3)
  235. try: # Does ref_day.tide_4 exist?
  236. if ref_day.tide_4.time.hour == 23:
  237. if ref_day.tide_4.state == "H":
  238. if (
  239. ref_day.tide_4.time.minute + self.high_time_offset
  240. ) > 59:
  241. # Adding the offest would overflow the day, so save
  242. # the tide to add to the start of the next day.
  243. fill_tide = ref_day.tide_4
  244. overflow = True
  245. else:
  246. # Otherwise just save the high tide
  247. new_day.tide_4 = self.generate_tide(ref_day.tide_4)
  248. else:
  249. # Do the same as the overflow scenario above, but for low tides
  250. if (ref_day.tide_4.time.minute + self.low_time_offset) > 59:
  251. fill_tide = ref_day.tide_4
  252. overflow = True
  253. else:
  254. new_day.tide_4 = self.generate_tide(ref_day.tide_4)
  255. else: # Tide 4 is not even at 11 and will not trigger an overflow
  256. new_day.tide_4 = self.generate_tide(ref_day.tide_4)
  257. except:
  258. pass
  259. new_day.save()
  260. return True
  261. def generate_tide(self, ref_tide):
  262. if ref_tide.state == "H":
  263. d = datetime(
  264. 1900, 1, 1, ref_tide.time.hour, ref_tide.time.minute
  265. ) + timedelta(minutes=self.high_time_offset)
  266. new_time = d.strftime("%H:%M")
  267. new_level = ref_tide.level * self.high_tide_offset
  268. new_state = "H"
  269. else:
  270. d = datetime(
  271. 1908, 1, 1, ref_tide.time.hour, ref_tide.time.minute
  272. ) + timedelta(minutes=self.low_time_offset)
  273. new_time = d.strftime("%H:%M")
  274. new_level = ref_tide.level * self.low_tide_offest
  275. new_state = "L"
  276. return Tide.objects.create(time=(new_time), level=(new_level), state=new_state)
  277. class Tide(models.Model):
  278. time = models.TimeField(_("time"))
  279. level = models.FloatField(_("level"), blank=True, null=True)
  280. state = models.CharField(max_length=1, choices=TIDE_STATES, blank=True)
  281. def __unicode__(self):
  282. return u"%s" % (self.time)
  283. def get_absolute_url(self):
  284. args = self.state + "-at-" + self.time
  285. return reverse("al-tide-detail", args=args)
  286. class TideDay(models.Model):
  287. day = models.ForeignKey(Day)
  288. tide_chart = models.ForeignKey(TideChart)
  289. tide_1 = models.ForeignKey(Tide, related_name="tide_1", blank=True, null=True)
  290. tide_2 = models.ForeignKey(Tide, related_name="tide_2", blank=True, null=True)
  291. tide_3 = models.ForeignKey(Tide, related_name="tide_3", blank=True, null=True)
  292. tide_4 = models.ForeignKey(Tide, related_name="tide_4", blank=True, null=True)
  293. class Meta:
  294. verbose_name = _("tide day")
  295. verbose_name_plural = _("tide days")
  296. ordering = ("day",)
  297. get_latest_by = "day"
  298. def __unicode__(self):
  299. return u"%s" % (self.day)
  300. class AstroChartUpload(models.Model):
  301. data_file = models.FileField(
  302. _("data file (.csv)"),
  303. upload_to="temp",
  304. help_text=_(
  305. "A comma separeted list of tide data for your particular location."
  306. ),
  307. )
  308. chart = models.ForeignKey(
  309. AstroChart, help_text=_("Select a chart to add the astronomical data to.")
  310. )
  311. dst = models.BooleanField(_("correct for DST"), default=False)
  312. class Meta:
  313. verbose_name = _("astronomical chart upload")
  314. verbose_name_plural = _("astronomical chart uploads")
  315. def save(self, *args, **kwargs):
  316. super(AstroChartUpload, self).save(*args, **kwargs)
  317. self.process_datafile()
  318. super(AstroChartUpload, self).delete()
  319. def process_datafile(self):
  320. try:
  321. data = csv.reader(open(self.data_file.path))
  322. except:
  323. raise Exception("There is a problem with your csv file.")
  324. for row in data:
  325. print row[0]
  326. day = Day.objects.create(date=row[0], chart=self.chart)
  327. if self.dst:
  328. if (
  329. self.chart.dst_start
  330. < datetime.strptime(day.date, "%Y-%m-%d").date()
  331. < self.chart.dst_end
  332. ):
  333. delta = timedelta(hours=1)
  334. else:
  335. delta = timedelta(hours=0)
  336. else:
  337. delta = timedelta(hours=0)
  338. # B/c python datetime blows:
  339. # Take string in csv file, convert to time, convert to datetime, add delta,
  340. # convert to timetuple and convert to string for insertion to db.
  341. day.sunrise = time.strftime(
  342. "%H:%M",
  343. (datetime(*time.strptime(row[1], "%H%M")[0:5]) + delta).timetuple(),
  344. )
  345. day.sunset = time.strftime(
  346. "%H:%M",
  347. (datetime(*time.strptime(row[2], "%H%M")[0:5]) + delta).timetuple(),
  348. )
  349. if row[3]:
  350. day.moonrise = time.strftime(
  351. "%H:%M",
  352. (datetime(*time.strptime(row[3], "%H%M")[0:5]) + delta).timetuple(),
  353. )
  354. if row[4]:
  355. day.moonset = time.strftime(
  356. "%H:%M",
  357. (datetime(*time.strptime(row[4], "%H%M")[0:5]) + delta).timetuple(),
  358. )
  359. day.civil_twilight_am = time.strftime(
  360. "%H:%M",
  361. (datetime(*time.strptime(row[5], "%H%M")[0:5]) + delta).timetuple(),
  362. )
  363. day.civil_twilight_pm = time.strftime(
  364. "%H:%M",
  365. (datetime(*time.strptime(row[6], "%H%M")[0:5]) + delta).timetuple(),
  366. )
  367. day.nautical_twilight_am = time.strftime(
  368. "%H:%M",
  369. (datetime(*time.strptime(row[7], "%H%M")[0:5]) + delta).timetuple(),
  370. )
  371. day.nautical_twilight_pm = time.strftime(
  372. "%H:%M",
  373. (datetime(*time.strptime(row[8], "%H%M")[0:5]) + delta).timetuple(),
  374. )
  375. if row[9]:
  376. day.moonphase = row[9]
  377. day.moonphase_time = time.strftime(
  378. "%H:%M",
  379. (
  380. datetime(*time.strptime(row[10], "%H%M")[0:5]) + delta
  381. ).timetuple(),
  382. )
  383. day.save()
  384. class TideChartUpload(models.Model):
  385. data_file = models.FileField(
  386. _("data file (.csv)"),
  387. upload_to="temp",
  388. help_text=_(
  389. "A comma separeted list of tide data for your particular location."
  390. ),
  391. )
  392. chart = models.ForeignKey(
  393. TideChart, help_text=_("Select a chart to add the tide data to.")
  394. )
  395. class Meta:
  396. verbose_name = _("tide chartupload")
  397. verbose_name_plural = _("tide chart uploads")
  398. def save(self, *args, **kwargs):
  399. super(TideChartUpload, self).save(*args, **kwargs)
  400. self.process_datafile()
  401. super(TideChartUpload, self).delete()
  402. def process_datafile(self):
  403. try:
  404. tides = BeautifulStoneSoup(open(self.data_file.path))
  405. except:
  406. raise Exception(
  407. "There is a problem with your XML file or you do not have BeautifulSoup installed."
  408. )
  409. try:
  410. days = Day.objects.filter(date__year=chart.year)
  411. for day in days:
  412. """
  413. XML for tide data from tidesandcurrents.noaa.gov:
  414. <item>
  415. <date>YYYY-MM-DD</date>
  416. <time>HH:MM A/P</time>
  417. <level_std>11.2 ft</level>
  418. <level_mtr>2.1 m</leve>
  419. <state>H/L</state>
  420. </item>
  421. """
  422. days_tides = tides.findAll(text=day.date.replace("/", "-"))
  423. tideday = TideDay.objects.create(day=day, tide_chart=self.chart)
  424. tide_1 = days_tides[0].parent.parent
  425. tide_2 = days_tides[1].parent.parent
  426. tide_3 = days_tides[2].parent.parent
  427. try:
  428. tide_4 = days_tides[3].parent.parent
  429. except:
  430. pass
  431. tideday.tide_1 = Tide.objects.create(
  432. time=tide_1.time.contents[0],
  433. level=tide_1.predictions_in_ft.contents[0],
  434. state=tide_1.highlow.conetnts[0],
  435. )
  436. tideday.tide_2 = Tide.objects.create(
  437. time=tide_2.time.contents[0],
  438. level=tide_2.predictions_in_ft.contents[0],
  439. state=tide_2.highlow.conetnts[0],
  440. )
  441. tideday.tide_3 = Tide.objects.create(
  442. time=tide_3.time.contents[0],
  443. level=tide_3.predictions_in_ft.contents[0],
  444. state=tide_3.highlow.conetnts[0],
  445. )
  446. if tide_4:
  447. tideday.tide_4 = Tide.objects.create(
  448. time=tide_4.time.contents[0],
  449. level=tide_4.predictions_in_ft.contents[0],
  450. state=tide_4.highlow.conetnts[0],
  451. )
  452. tideday.save()
  453. except:
  454. raise Exception(
  455. "Tide data must be loaded after astronomical data for any given date."
  456. )