Skip to content
This repository was archived by the owner on Jan 8, 2021. It is now read-only.

weather panel #292

Merged
merged 36 commits into from
Oct 6, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
150adef
weather panel now based on lxml
dcrystalj Sep 15, 2012
f38dbcb
update on query data
dcrystalj Sep 22, 2012
5fc2f32
changed i to interval
dcrystalj Sep 22, 2012
2b52c57
unique added
dcrystalj Sep 22, 2012
8c2b78f
Merge branch 'master' into vreme
dcrystalj Sep 22, 2012
3ac3494
update
dcrystalj Sep 24, 2012
6e3b775
Merge branch 'master' into vreme
dcrystalj Sep 24, 2012
e6cff3f
Merge branch 'master' into vreme
dcrystalj Sep 24, 2012
5f36842
Merge remote-tracking branch 'mitar/master' into vreme
dcrystalj Sep 24, 2012
d615a91
update
dcrystalj Sep 24, 2012
3a37d5c
update
dcrystalj Sep 24, 2012
253369d
update
dcrystalj Sep 24, 2012
5fe1d91
update
dcrystalj Sep 24, 2012
9dcb33f
update
dcrystalj Sep 24, 2012
f1fee5a
css update
dcrystalj Sep 25, 2012
994e47a
updated design
dcrystalj Sep 26, 2012
e906f26
description to TODO, ordered imports
dcrystalj Sep 27, 2012
3231644
Merge branch 'master' into vreme
dcrystalj Sep 28, 2012
8ba3cf1
group task, limited tasks per second, horoscope string, link to met.no..
dcrystalj Sep 28, 2012
5dfe82b
uncomented
dcrystalj Sep 28, 2012
1794f56
fixed link
dcrystalj Sep 28, 2012
3fd7e5b
limited forecast, improved updateweather command, css fixed...
dcrystalj Sep 29, 2012
bbd9f4e
fixed weather forecast range
dcrystalj Sep 30, 2012
1186d34
missing dot
dcrystalj Oct 1, 2012
ded786b
merge
dcrystalj Oct 3, 2012
5a60d86
space fix
dcrystalj Oct 3, 2012
3a2affa
Merge branch 'master' into vreme
dcrystalj Oct 3, 2012
c875c3f
fixed cp
dcrystalj Oct 3, 2012
30a80bf
finaly merged settings
dcrystalj Oct 3, 2012
f76c61b
comment moved
dcrystalj Oct 5, 2012
4fdc96c
requirements.txt
dcrystalj Oct 5, 2012
5b5ba19
Merge remote-tracking branch 'mitar/master'
dcrystalj Oct 5, 2012
618ad60
Merge branch 'master' into vreme
dcrystalj Oct 5, 2012
5fceea4
fixed tasks to work properly
dcrystalj Oct 6, 2012
cbe4258
Merge remote-tracking branch 'mitar/master'
dcrystalj Oct 6, 2012
299d9e8
Merge branch 'master' into vreme
dcrystalj Oct 6, 2012
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ def handle(self, *args, **options):
verbosity = int(options['verbosity'])

if verbosity > 1:
self.stdout.write('Updating horoscopes...\n')
self.stdout.write("Updating horoscopes...\n")

tasks.update_horoscope.delay().wait()

if verbosity > 1:
self.stdout.write('Successfully updated all horoscopes.\n')
self.stdout.write("Successfully updated all horoscopes.\n")
Empty file.
Empty file.
Empty file.
30 changes: 30 additions & 0 deletions piplmesh/panels/weather/management/commands/updateweather.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import time

from django.core.management import base

from piplmesh.panels.weather import tasks

class Command(base.BaseCommand):
help = 'Force weather update.'

def handle(self, *args, **options):
"""
Forces Weather update.
"""

verbosity = int(options['verbosity'])

if verbosity > 1:
self.stdout.write("Updating weather...\n")

results = tasks.generate_weather_tasks()
prev_completed = None
while not results.ready():
completed = results.completed_count()
if prev_completed != completed:
self.stdout.write("Completed %d/%d.\n" % (completed, len(results)))
prev_completed = completed
time.sleep(1)

if verbosity > 1:
self.stdout.write("Successfully updated weather.\n")
61 changes: 61 additions & 0 deletions piplmesh/panels/weather/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from django.utils.translation import ugettext_lazy as _

import mongoengine

SYMBOLS = (
(1, _("Sun")),
(2, _("Light cloud")),
(3, _("Partly cloud")),
(4, _("Cloud")),
(5, _("Light rain and sun")),
(6, _("Light rain, thunder and sun")),
(7, _("Sleet and sun")),
(8, _("Snow and sun")),
(9, _("Light rain")),
(10, _("Rain")),
(11, _("Rain and thunder")),
(12, _("Sleet")),
(13, _("Snow")),
(14, _("Snow and thunder")),
(15, _("Fog")),
(16, _("Sun (used for winter darkness)")),
(17, _("Light cloud ( winter darkness )")),
(18, _("Light rain and sun")),
(19, _("Snow and sun ( used for winter darkness )")),
(20, _("Sleet, sun and thunder")),
(21, _("Snow, sun and thunder")),
(22, _("Light rain and thunder")),
(23, _("Sleet thunder")),
)

# TODO: Some fields have to be unique to make sure that there is only one entry for each time period for given location.
# https://github.com/wlanslovenija/PiplMesh/issues/210
class Weather(mongoengine.Document):
created = mongoengine.DateTimeField(required=True)
latitude = mongoengine.DecimalField(required=True)
longitude = mongoengine.DecimalField(required=True)
model_name = mongoengine.StringField(required=True)

meta = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line before this.

'allow_inheritance': True,
}

class Precipitation(Weather):
date_from = mongoengine.DateTimeField(required=True)
date_to = mongoengine.DateTimeField(required=True)
precipitation = mongoengine.DecimalField(required=True)
symbol = mongoengine.IntField(choices=SYMBOLS, required=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have here IntField, but above you have strings as keys, not numbers. Does this work?


class State(Weather):
at = mongoengine.DateTimeField(required=True)
temperature = mongoengine.DecimalField(required=True)
wind_direction = mongoengine.StringField(required=True)
wind_angle = mongoengine.DecimalField(required=True)
wind_speed = mongoengine.DecimalField(required=True)
humidity = mongoengine.DecimalField(required=True)
pressure = mongoengine.DecimalField(required=True)
cloudiness = mongoengine.DecimalField(required=True)
fog = mongoengine.DecimalField(required=True)
low_clouds = mongoengine.DecimalField(required=True)
medium_clouds = mongoengine.DecimalField(required=True)
high_clouds = mongoengine.DecimalField(required=True)
59 changes: 59 additions & 0 deletions piplmesh/panels/weather/panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import absolute_import

import datetime

from django.utils import timezone, translation
from django.utils.translation import ugettext_lazy as _

from piplmesh import nodes, panels

from . import models

WEATHER_OBSOLETE = 6 # 6 hours
FORECAST_HOUR = 14 # 14:00 o'clock or 2:00 PM
WEATHER_FORECAST_RANGE = 3 # days from being created

class WeatherPanel(panels.BasePanel):
def get_context(self, context):
context.update({
'header': _("Weather"),
})

latitude = self.request.node.latitude
longitude = self.request.node.longitude
# TODO: Check and possibly optimize
state = models.State.objects(latitude=latitude, longitude=longitude, at__lte=datetime.datetime.now(), at__gte=(datetime.datetime.now() - datetime.timedelta(hours=WEATHER_OBSOLETE))).order_by('+created').first()
if state is None:
context.update({
'error_data': True,
})
return context

if timezone.now() >= state.created + datetime.timedelta(hours=WEATHER_OBSOLETE):
context.update({
'error_obsolete': True,
})
return context

context.update({
'weather_objects': get_weather_content(latitude, longitude),
'created': state.created,
})
return context

# TODO: Check and possibly optimize
def get_weather_content(latitude, longitude):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit skeptical about this function. It looks strange. And the query above. But OK, leave it as it is. Or maybe add TODO saying to check and possibility optimize.

date = datetime.datetime.now()
for interval in range(0, WEATHER_FORECAST_RANGE):
state = models.State.objects(latitude=latitude, longitude=longitude, at__lte=date).order_by('-at').first()
precipitation = models.Precipitation.objects(latitude=latitude, longitude=longitude, date_from__lte=date, date_to__gte=date).order_by('-date_from').first()
weather_object = {
'at': state.at,
'temperature': state.temperature,
'symbol': precipitation.symbol,
}
date += datetime.timedelta(days=1)
date = date.replace(hour=FORECAST_HOUR, minute=0)
yield weather_object

panels.panels_pool.register(WeatherPanel)
41 changes: 41 additions & 0 deletions piplmesh/panels/weather/static/piplmesh/panel/weather/panel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.error {
font-size: x-small;
}

#weather {
font-size: 10px;
text-align: left;
}

#weather ul {
float: center;
padding: 0;
}

#weather li {
display: inline;
float: left;
width: 100%;
}

#weather span {
font-size: 7px;
}

#forecast ul {
border: 0;
display: table;
list-style: none;
margin: 0 auto;
text-align: center;
width: 250px;
}

#forecast li {
border: none;
display: table-cell;
margin-left: 5px;
padding: 0;
vertical-align: middle;
width: 77px;
}
90 changes: 90 additions & 0 deletions piplmesh/panels/weather/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from __future__ import absolute_import

import datetime

from lxml import etree, objectify

import celery
from celery import task
from celery.task import schedules

from piplmesh import nodes
from piplmesh.utils import decorators

from . import models, panel

CHECK_FOR_NEW_WEATHER = 30 # minutes

SOURCE_URL = 'http://api.met.no/'

def fetch_data(latitude, longitude):
"""
Get weather data for specific location
"""

weather_url = '%sweatherapi/locationforecast/1.8/?lat=%s;lon=%s' % (SOURCE_URL, latitude, longitude)
parser = etree.XMLParser(remove_blank_text=True)
lookup = objectify.ObjectifyElementClassLookup()
parser.setElementClassLookup(lookup)
weather = objectify.parse(weather_url, parser).getroot()
return weather

@task.periodic_task(run_every=schedules.crontab(minutes=CHECK_FOR_NEW_WEATHER))
@decorators.single_instance_task()
def generate_weather_tasks():
"""
Task which updates weather for all nodes.

Obsolete data is currently left in the database.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line after docstring.


weather_tasks = []
# Fetching data only once per possible duplicate locations
for latitude, longitude in {(node.latitude, node.longitude) for node in nodes.get_all_nodes()}:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment discussing what we are doing here could be useful, above this line. For example:

# Fetching data only once per possible duplicate locations

weather_tasks.append(update_weather.s(latitude, longitude))
return celery.group(weather_tasks)()

# 20 tasks per second. Limitation by the api http://api.yr.no/conditions_service.html
@task.task(rate_limit=20)
def update_weather(latitude, longitude):
"""
Task which updates weather for one location.
"""

weather_object = fetch_data(latitude, longitude)
for product in weather_object.product.iterchildren():
if datetime.datetime.strptime(product.attrib['to'], '%Y-%m-%dT%H:%M:%SZ') < datetime.datetime.strptime(weather_object.attrib['created'], '%Y-%m-%dT%H:%M:%SZ') + datetime.timedelta(days=panel.WEATHER_FORECAST_RANGE):
if product.attrib['from'] == product.attrib['to']:
models.State.objects(
created=datetime.datetime.strptime(weather_object.attrib['created'], '%Y-%m-%dT%H:%M:%SZ'),
latitude=latitude,
longitude=longitude,
model_name=weather_object.meta.model.attrib['name'],
at=datetime.datetime.strptime(product.attrib['from'], '%Y-%m-%dT%H:%M:%SZ')
).update(
set__temperature=product.location.temperature.attrib['value'],
set__wind_direction=product.location.windDirection.attrib['name'],
set__wind_angle=product.location.windDirection.attrib['deg'],
set__wind_speed=product.location.windSpeed.attrib['mps'],
set__humidity=product.location.humidity.attrib['value'],
set__pressure=product.location.pressure.attrib['value'],
set__cloudiness=product.location.cloudiness.attrib['percent'],
set__fog=product.location.fog.attrib['percent'],
set__low_clouds=product.location.lowClouds.attrib['percent'],
set__medium_clouds=product.location.mediumClouds.attrib['percent'],
set__high_clouds=product.location.highClouds.attrib['percent'],
upsert=True
)
else:
models.Precipitation.objects(
created=datetime.datetime.strptime(weather_object.attrib['created'], '%Y-%m-%dT%H:%M:%SZ'),
latitude=latitude,
longitude=longitude,
model_name=weather_object.meta.model.attrib['name'],
date_from=datetime.datetime.strptime(product.attrib['from'], '%Y-%m-%dT%H:%M:%SZ'),
date_to=datetime.datetime.strptime(product.attrib['to'], '%Y-%m-%dT%H:%M:%SZ')
).update(
set__precipitation=product.location.precipitation.attrib['value'],
set__symbol=product.location.symbol.attrib['number'],
upsert=True
)
27 changes: 27 additions & 0 deletions piplmesh/panels/weather/templates/panel/weather/panel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% extends "panel/panel.html" %}

{% load i18n static sekizai_tags %}

{% block content %}
{% addtoblock "css" %}<link href="{% static "piplmesh/panel/weather/panel.css" %}" rel="stylesheet" type="text/css" media="screen" />{% endaddtoblock %}
{% if error_data or error_obsolete %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do you define error_data? You might above if state is empty and state[0] does not exist. But you currently don't do it.

<span class="error">{% trans "Weather data is temporarily unavailable." %}</span>
{% else %}
<div id="weather">
<ul>
{% for weather in weather_objects %}
<li id="forecast">
<ul>
<li>{{ weather.at|date:"DATE_FORMAT" }}</li>
<li><img src="http://api.yr.no/weatherapi/weathericon/1.0/?symbol={{ weather.symbol }};content_type=image/png" alt="{% trans "weather icon" %}" /></li>
{% comment %}TODO temperature unit user-configurable{% endcomment %}
<li>{{ weather.temperature }}°C</li>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a TODO comment that we should make temperature unit user-configurable.

</ul>
</li>
{% endfor %}
</ul>
<p>{% blocktrans with date=created %}Forecast created on: {{ date }}{% endblocktrans %}</p>
<span>{% blocktrans %}Based on data from <a href="http://met.no">met.no</a>.{% endblocktrans %}</span>
</div>
{% endif %}
{% endblock %}
3 changes: 2 additions & 1 deletion piplmesh/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@
'piplmesh.utils',
'piplmesh.panels',
'piplmesh.panels.horoscope', # To load manage.py command

'piplmesh.panels.weather', # To load manage.py command

'django.contrib.messages',
'django.contrib.sessions',
'django.contrib.staticfiles',
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Django==1.4.1
Jinja2==2.6
PyYAML==3.10
Pygments==1.5
PyYAML==3.10
Sphinx==1.1.3
amqplib==1.0.2
anyjson==0.3.3
Expand Down