aboutsummaryrefslogtreecommitdiffstats
path: root/97suifangqa/apps/indicator
diff options
context:
space:
mode:
authorAlvin Li <liweitianux@gmail.com>2013-08-13 14:13:24 +0800
committerAlvin Li <liweitianux@gmail.com>2013-08-13 14:13:24 +0800
commit9636d4a6767f49384d5c386bc3f1142c88b90613 (patch)
tree3a70f6d9e4be1791d36c87cc7cbfd1d5aa2b39dd /97suifangqa/apps/indicator
parent9383d9a8a5988d071766c3d08a5c946e9c5b02ae (diff)
download97dev-9636d4a6767f49384d5c386bc3f1142c88b90613.tar.bz2
cloned from 'bitbucket', 2013/08/13
Diffstat (limited to '97suifangqa/apps/indicator')
-rw-r--r--97suifangqa/apps/indicator/__init__.py0
-rw-r--r--97suifangqa/apps/indicator/fixtures/initial_data.json307
-rw-r--r--97suifangqa/apps/indicator/forms.py321
-rw-r--r--97suifangqa/apps/indicator/models.py1127
-rw-r--r--97suifangqa/apps/indicator/search_indexes.py53
-rw-r--r--97suifangqa/apps/indicator/templates/done.html9
-rw-r--r--97suifangqa/apps/indicator/templates/show_category.html32
-rw-r--r--97suifangqa/apps/indicator/templates/show_indicator.html48
-rw-r--r--97suifangqa/apps/indicator/templates/show_record.html52
-rw-r--r--97suifangqa/apps/indicator/templates/simple.html23
-rw-r--r--97suifangqa/apps/indicator/tools.py273
-rw-r--r--97suifangqa/apps/indicator/urls.py124
-rw-r--r--97suifangqa/apps/indicator/views.py416
13 files changed, 2785 insertions, 0 deletions
diff --git a/97suifangqa/apps/indicator/__init__.py b/97suifangqa/apps/indicator/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/97suifangqa/apps/indicator/__init__.py
diff --git a/97suifangqa/apps/indicator/fixtures/initial_data.json b/97suifangqa/apps/indicator/fixtures/initial_data.json
new file mode 100644
index 0000000..25d6c73
--- /dev/null
+++ b/97suifangqa/apps/indicator/fixtures/initial_data.json
@@ -0,0 +1,307 @@
+[
+ {
+ "pk": 1,
+ "model": "indicator.indicatorcategory",
+ "fields": {
+ "pinyin": "lei-bie-1",
+ "englishName": "category1",
+ "addByUser": 1,
+ "name": "\u7c7b\u522b1",
+ "description": "\u6307\u6807\u7c7b\u522b1\r\n\r\n\u4fee\u65391\uff08\u6d4b\u8bd5\uff09"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "indicator.indicatorcategory",
+ "fields": {
+ "pinyin": "lei-bie-2",
+ "englishName": "category2",
+ "addByUser": 1,
+ "name": "\u7c7b\u522b2",
+ "description": "\u7c7b\u522b2\r\n\r\nadd_edit_category() \u6dfb\u52a0"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "indicator.indicator",
+ "fields": {
+ "addByUser": 1,
+ "name": "\u6d4b\u8bd51",
+ "dataType": "FL",
+ "pinyin": "ce-shi-1",
+ "helpText": "\u5e2e\u52a9 help",
+ "englishName": "test1",
+ "categories": [
+ 1
+ ],
+ "description": "forms \u6d4b\u8bd51"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "indicator.indicator",
+ "fields": {
+ "addByUser": 1,
+ "name": "\u5b9a\u503c1",
+ "dataType": "FL",
+ "pinyin": "ding-zhi-1",
+ "helpText": "\u6d6e\u70b9\u5b9a\u503c",
+ "englishName": "float1",
+ "categories": [
+ 1
+ ],
+ "description": "float type"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "indicator.indicator",
+ "fields": {
+ "addByUser": 1,
+ "name": "\u8303\u56f41",
+ "dataType": "RG",
+ "pinyin": "fan-wei-1",
+ "helpText": "\u8303\u56f4\u578b",
+ "englishName": "range1",
+ "categories": [
+ 1
+ ],
+ "description": "range type\r\n\r\n\u8303\u56f4\u578b"
+ }
+ },
+ {
+ "pk": 1,
+ "model": "indicator.indicator",
+ "fields": {
+ "addByUser": 1,
+ "name": "\u6307\u68071",
+ "dataType": "FL",
+ "pinyin": "zhi-biao-1",
+ "helpText": "\u6d6e\u70b9\u578b",
+ "englishName": "indicator1",
+ "categories": [
+ 1
+ ],
+ "description": "\u6307\u68071\r\n\r\n\u6d6e\u70b9\u578b\u6570\u636e"
+ }
+ },
+ {
+ "pk": 1,
+ "model": "indicator.userindicator",
+ "fields": {
+ "followedIndicators": [
+ 4
+ ],
+ "user": 1
+ }
+ },
+ {
+ "pk": 1,
+ "model": "indicator.indicatorrecord",
+ "fields": {
+ "indicator": 1,
+ "notes": "\u6307\u68071\r\n\u7b2c1\u6761\u8bb0\u5f55",
+ "created_at": "2013-08-05T15:48:00.035",
+ "updated_at": "2013-08-05T15:50:00.326",
+ "value": "250",
+ "val_min": null,
+ "user": 1,
+ "date": "2013-08-05",
+ "val_max": null,
+ "unit": 1
+ }
+ },
+ {
+ "pk": 2,
+ "model": "indicator.indicatorrecord",
+ "fields": {
+ "indicator": 1,
+ "notes": "test\r\n\u8bb0\u5f55",
+ "created_at": "2013-08-09T10:53:15.927",
+ "updated_at": "2013-08-10T00:30:09.336",
+ "value": "50",
+ "val_min": null,
+ "user": 2,
+ "date": "2013-08-09",
+ "val_max": null,
+ "unit": 1
+ }
+ },
+ {
+ "pk": 1,
+ "model": "indicator.recordhistory",
+ "fields": {
+ "val_min_bak": null,
+ "created_at": "2013-08-05T16:07:01.832",
+ "indicatorRecord": 1,
+ "reason": "\u6d4b\u8bd5\r\nadmin\u754c\u9762\u76f4\u63a5\u4fee\u6539",
+ "unit_bak": 1,
+ "val_max_bak": null,
+ "value_bak": "250",
+ "date_bak": "2013-08-05",
+ "notes_bak": "\u6307\u68071\r\n\u7b2c1\u6761\u8bb0\u5f55"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "indicator.recordhistory",
+ "fields": {
+ "val_min_bak": null,
+ "created_at": "2013-08-10T11:40:23.170",
+ "indicatorRecord": 1,
+ "reason": "\u6d4b\u8bd5\u4fee\u6539",
+ "unit_bak": 1,
+ "val_max_bak": null,
+ "value_bak": "250",
+ "date_bak": "2013-08-05",
+ "notes_bak": "\u6307\u68071\r\n\u7b2c1\u6761\u8bb0\u5f55"
+ }
+ },
+ {
+ "pk": 1,
+ "model": "indicator.unit",
+ "fields": {
+ "indicator": 1,
+ "description": "",
+ "symbol": "unit11",
+ "addByUser": 1,
+ "standard": true,
+ "relation": "v",
+ "name": "\u5355\u4f4d11"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "indicator.unit",
+ "fields": {
+ "indicator": 1,
+ "description": "",
+ "symbol": "unit12",
+ "addByUser": 1,
+ "standard": false,
+ "relation": "log10(v) + 10",
+ "name": "\u5355\u4f4d12"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "indicator.unit",
+ "fields": {
+ "indicator": 2,
+ "description": "",
+ "symbol": "unit21",
+ "addByUser": 1,
+ "standard": true,
+ "relation": "v",
+ "name": "\u5355\u4f4d21"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "indicator.unit",
+ "fields": {
+ "indicator": 4,
+ "description": "\u7b80\u5355\u63cf\u8ff0",
+ "symbol": "unit41",
+ "addByUser": 1,
+ "standard": true,
+ "relation": "v",
+ "name": "\u5355\u4f4d41"
+ }
+ },
+ {
+ "pk": 1,
+ "model": "indicator.innateconfine",
+ "fields": {
+ "math_max": 800.0,
+ "indicator": 1,
+ "human_max": 500.0,
+ "description": "\u6307\u68071\r\n\u6570\u636e\u8303\u56f4",
+ "val_norm": "",
+ "addByUser": 1,
+ "human_min": 50.0,
+ "unit": 1,
+ "math_min": 0.0
+ }
+ },
+ {
+ "pk": 2,
+ "model": "indicator.innateconfine",
+ "fields": {
+ "math_max": 800.0,
+ "indicator": 2,
+ "human_max": 500.0,
+ "description": "\u6307\u6807\r\n\r\n\u6570\u636e\u7c7b\u578b\uff1a\u8303\u56f4\u578b\r\n\r\n\u6570\u636e\u8303\u56f4",
+ "val_norm": "",
+ "addByUser": 1,
+ "human_min": 50.0,
+ "unit": 3,
+ "math_min": 0.0
+ }
+ },
+ {
+ "pk": 1,
+ "model": "indicator.relatedindicator",
+ "fields": {
+ "indicator": 1,
+ "weight": 5.9,
+ "created_at": "2013-08-10T22:40:00.035",
+ "updated_at": "2013-08-10T22:40:00.326",
+ "blog": null,
+ "annotation": 2,
+ "objectType": "AN"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "indicator.relatedindicator",
+ "fields": {
+ "indicator": 1,
+ "weight": 8.0,
+ "created_at": "2013-08-11T00:56:08.080",
+ "updated_at": "2013-08-11T00:56:08.080",
+ "blog": null,
+ "annotation": 1,
+ "objectType": "AN"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "indicator.relatedindicator",
+ "fields": {
+ "indicator": 2,
+ "weight": 8.3,
+ "created_at": "2013-08-10T22:50:00.035",
+ "updated_at": "2013-08-10T22:50:00.326",
+ "blog": 3,
+ "annotation": null,
+ "objectType": "BL"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "indicator.relatedindicator",
+ "fields": {
+ "indicator": 1,
+ "weight": 4.0,
+ "created_at": "2013-08-11T00:56:49.463",
+ "updated_at": "2013-08-11T00:56:49.463",
+ "blog": 1,
+ "annotation": null,
+ "objectType": "BL"
+ }
+ },
+ {
+ "pk": 5,
+ "model": "indicator.relatedindicator",
+ "fields": {
+ "indicator": 1,
+ "weight": 6.0,
+ "created_at": "2013-08-11T00:57:23.067",
+ "updated_at": "2013-08-11T00:57:23.067",
+ "blog": 3,
+ "annotation": null,
+ "objectType": "BL"
+ }
+ }
+] \ No newline at end of file
diff --git a/97suifangqa/apps/indicator/forms.py b/97suifangqa/apps/indicator/forms.py
new file mode 100644
index 0000000..2e0b709
--- /dev/null
+++ b/97suifangqa/apps/indicator/forms.py
@@ -0,0 +1,321 @@
+# -*- coding: utf-8 -*-
+
+"""
+forms for apps/indicator
+"""
+
+from django import forms
+from django.utils.translation import ugettext as _
+
+from indicator import models as im
+
+import sympy
+from sympy.core.sympify import SympifyError
+
+
+class IndicatorCategoryForm(forms.ModelForm): # {{{
+ """
+ form for 'models.IndicatorCategory'
+ """
+ class Meta:
+ model = im.IndicatorCategory
+ exclude = ('addByUser',)
+# }}}
+
+
+class IndicatorForm(forms.ModelForm): # {{{
+ """
+ form for 'models.Indicator'
+ """
+ class Meta:
+ model = im.Indicator
+ exclude = ('addByUser',)
+# }}}
+
+
+class UnitForm(forms.ModelForm): # {{{
+ """
+ form for 'models.Unit'
+ """
+ class Meta:
+ model = im.Unit
+ exclude = ('addByUser',)
+
+ def __init__(self, *args, **kwargs):
+ super(UnitForm, self).__init__(*args, **kwargs)
+ # store 'instance_id', for edting instance
+ self.instance_id = self.instance.id
+
+ # 'clean_standard()' cannot raise Vali dationError correctly??
+ # TODO: clean each field and generate errors accordingly.
+
+ def clean(self):
+ cleaned_data = super(UnitForm, self).clean()
+ instance_id = self.instance_id
+ standard = cleaned_data['standard']
+ indicator = cleaned_data['indicator']
+ std_unit_list = indicator.get_unit(type="standard")
+ relation = cleaned_data.get('relation', u'')
+ if standard:
+ if std_unit_list and (instance_id != std_unit_list[0].id):
+ raise forms.ValidationError(_(u'标准单位已存在'),
+ code='standard')
+ cleaned_data['relation'] = u'v'
+ else:
+ try:
+ fsym = sympy.sympify(relation)
+ except SympifyError:
+ raise forms.ValidationError(_(u'"%(relation)s" 不是合法的表达式'),
+ code='relation_invalid',
+ params={'relation': relation})
+ # always return the full collection of cleaned data
+ return cleaned_data
+# }}}
+
+
+class InnateConfineForm(forms.ModelForm): # {{{
+ """
+ form for 'models.InnateConfine'
+ """
+ unit = forms.ModelChoiceField(label=u"标准单位",
+ queryset=im.Unit.objects.filter(standard=True))
+
+ class Meta:
+ model = im.InnateConfine
+ exclude = ('addByUser',)
+
+ def clean(self): # {{{
+ """
+ check the validity of data
+ """
+ cleaned_data = super(InnateConfineForm, self).clean()
+ indicator = cleaned_data['indicator']
+ unit = cleaned_data.get('unit')
+ val_norm = cleaned_data.get('val_norm')
+ human_max = cleaned_data.get('human_max')
+ human_min = cleaned_data.get('human_min')
+ math_max = cleaned_data.get('math_max')
+ math_min = cleaned_data.get('math_min')
+ # check data
+ if indicator.dataType in [indicator.FLOAT_TYPE,
+ indicator.RANGE_TYPE, indicator.FLOAT_RANGE_TYPE]:
+ # check unit
+ if not (unit and unit.standard):
+ raise forms.ValidationError(_(u'unit 未填写/不是标准单位'),
+ code='unit')
+ if (human_max is None) or (human_min is None):
+ raise forms.ValidationError(_(u'human_max/human_min 未填写'),
+ code='human_empty')
+ if (human_max <= human_min):
+ raise forms.ValidationError(_(u'human_max <= human_min'),
+ code='human_relation')
+ # check 'math_max' and 'math_min'
+ if (math_max is None) or (math_min is None):
+ raise forms.ValidationError(_(u'math_max/math_min 未填写'),
+ code='math_empty')
+ if (math_max <= math_min):
+ raise forms.ValidationError(_(u'math_max <= math_min'),
+ code='math_relation')
+ # compare 'human*' and 'math*'
+ if (human_max > math_max) or (human_min < math_min):
+ raise forms.ValidationError(_(u'Error: human_max>math_max / human_min<math_min'),
+ code='human_math_relation')
+ # check finished
+ elif indicator.dataType == indicator.INTEGER_TYPE:
+ # 整数型
+ try:
+ val_norm = int(val_norm)
+ except ValueError:
+ raise ValidationError(_(u'val_norm="%(val_norm)s" 不是整数型值'),
+ code='val_norm_int',
+ params={'val_norm': val_norm})
+ elif indicator.dataType == indicator.PM_TYPE:
+ # 阴阳(+/-)型
+ if (len(val_norm) == 1) and (val_norm in [u'+', u'-']):
+ pass
+ else:
+ raise forms.ValidationError(_(u'val_norm 只接受 "+"/"-"'),
+ code='val_norm_pm')
+ ## TODO: RADIO_TYPE, CHECKBOX_TYPE
+ elif indicator.dataType in [indicator.RADIO_TYPE,
+ indicator.CHECKBOX_TYPE]:
+ raise forms.ValidationError(_(u'RADIO_TYPE, CHECKBOX_TYPE 验证未实现'),
+ code='radio_checkbox')
+ else:
+ raise forms.ValidationError(_(u'数据不符合要求'),
+ code='data_type_invalid')
+ # all checks finished
+ return cleaned_data
+ # }}}
+# }}}
+
+
+class IndicatorRecordForm(forms.ModelForm): # {{{
+ """
+ form for 'models.IndicatorRecord'
+ """
+
+ class Meta:
+ model = im.IndicatorRecord
+ exclude = ('user',)
+
+ def clean(self): # {{{
+ cleaned_data = super(IndicatorRecordForm, self).clean()
+ # get data
+ indicator = cleaned_data['indicator']
+ unit = cleaned_data.get('unit')
+ _value = cleaned_data.get('value')
+ _val_max = cleaned_data.get('val_max')
+ _val_min = cleaned_data.get('val_min')
+ # check data # {{{
+ if indicator.dataType == indicator.INTEGER_TYPE:
+ # integer
+ try:
+ value = int(_value)
+ except ValueError:
+ raise forms.ValidationError(_(u'value 不是整数类型'),
+ code='value_integer')
+ elif indicator.dataType == indicator.FLOAT_TYPE:
+ # float
+ if not unit:
+ raise forms.ValidationError(_(u'unit 未填写'),
+ code='unit_empty')
+ try:
+ value = float(_value)
+ except ValueError:
+ raise forms.ValidationError(_(u'value 不是浮点数类型'),
+ code='value_float')
+ elif indicator.dataType == indicator.RANGE_TYPE:
+ # range
+ val_max = _val_max
+ val_min = _val_min
+ if not unit:
+ raise forms.ValidationError(_(u'unit 未填写'),
+ code='unit_empty')
+ if (val_max is None) or (val_min is None):
+ raise forms.ValidationError(_(u'val_max/val_min 未填写'),
+ code='val_empty')
+ if (val_max <= val_min):
+ raise forms.ValidationError(_(u'val_max <= val_min'),
+ code='val_relation')
+ elif indicator.dataType == indicator.FLOAT_RANGE_TYPE:
+ # float/range
+ if not unit:
+ raise forms.ValidationError(_(u'unit 未填写'),
+ code='unit_empty')
+ if value:
+ # float (first)
+ try:
+ value = float(_value)
+ except ValueError:
+ raise forms.ValidationError(_(u'value 不是浮点数类型'),
+ code='value_float')
+ elif (val_max is not None) or (val_min is not None):
+ # range
+ val_max = _val_max
+ val_min = _val_min
+ if (val_max <= val_min):
+ raise forms.ValidationError(_(u'val_max <= val_min'),
+ code='val_relation')
+ else:
+ raise forms.ValidationError(_(u'请填写 value 或者 "val_max + val_min"'),
+ code='value_val')
+ elif indicator.dataType == indicator.PM_TYPE:
+ # +/-
+ value = _value
+ if (len(value) == 1) and (value in [u'+', u'-']):
+ pass
+ else:
+ raise forms.ValidationError(_(u'value 只接受 "+"/"-"'),
+ code='value_pm')
+ elif indicator.dataType in [indicator.RADIO_TYPE,
+ indicator.CHECKBOX_TYPE]:
+ ## TODO: RADIO_TYPE, CHECKBOX_TYPE
+ raise forms.ValidationError(_(u'RADIO_TYPE, CHECKBOX_TYPE 验证未实现'),
+ code='radio_checkbox')
+ else:
+ raise forms.ValidationError(_(u'数据不符合要求'),
+ code='data_type_invalid')
+ # }}}
+ # check confine # {{{
+ # for [FLOAT_TYPE, RANGE_TYPE, FLOAT_RANGE_TYPE]
+ # [INTEGER_TYPE, PM_TYPE] already validated above
+ if indicator.dataType in [indicator.FLOAT_TYPE,
+ indicator.RANGE_TYPE, indicator.FLOAT_RANGE_TYPE]:
+ # check confine if specified for the indicator
+ if not indicator.check_confine():
+ raise forms.ValidationError(_(u'该指标未指定 InnateConfine'),
+ code='innateconfine')
+ # innateconfine ok
+ confine = indicator.innate_confine
+ human_max = confine.human_max
+ human_min = confine.human_min
+ math_max = confine.math_max
+ math_min = confine.math_min
+ # unit conversion
+ unit_rel = unit.relation
+ v = sympy.symbols('v')
+ rel_sym = sympy.sympify(unit_rel)
+ # data
+ value = _value
+ val_max = _val_max
+ val_min = _val_min
+ # value
+ if value:
+ try:
+ value = float(value)
+ except ValueError:
+ raise forms.ValidationError(_(u'value 不是浮点数类型'),
+ code='value_float')
+ # 'value' unit conversion
+ try:
+ value_std = float(rel_sym.evalf(subs={v: value}))
+ except ValueError:
+ raise forms.ValidationError(_(u'"%s" 求值错误,请检查只有变量"v"' % unit_rel),
+ code='value_evalf')
+ if (value_std < math_min) or (value_std > math_max):
+ raise forms.ValidationError(_(u'value(std) < math_min or value(std) > math_max'),
+ code='value_std_relation')
+ # val_max
+ if val_max is not None:
+ # unit conversion
+ try:
+ val_max_std = float(rel_sym.evalf(
+ subs={v: val_max}))
+ except ValueError:
+ raise forms.ValidationError(_(u'"%s" 求值错误,请检查只有变量"v"' % unit_rel),
+ code='val_max_evalf')
+ if (val_max_std <= math_min) or (
+ val_max_std > math_max):
+ raise forms.ValidationError(_(u'val_max(std) <= math_min or val_max(std) > math_max'),
+ code='val_max_std_relation')
+ # val_min
+ if val_min is not None:
+ try:
+ val_min_std = float(rel_sym.evalf(
+ subs={v: val_min}))
+ except ValueError:
+ raise forms.ValidationError(_(u'"%s" 求值错误,请检查只有变量"v"' % unit_rel),
+ code='val_min_evalf')
+ if (val_min_std < math_min) or (
+ val_min_std >= math_max):
+ raise forms.ValidationError(_(u'val_min(std) < math_min or val_min(std) >= math_max'),
+ code='val_min_std_relation')
+ # }}}
+ # return cleaned data
+ return cleaned_data
+ # }}}
+# }}}
+
+
+class RecordHistoryForm(forms.ModelForm): # {{{
+ """
+ form for 'models.RecordHistory'
+ """
+ class Meta:
+ model = im.RecordHistory
+ exclude = ('indicatorRecord',)
+# }}}
+
+
+# vim: set ts=4 sw=4 tw=0 fenc=utf-8 ft=python.django: #
diff --git a/97suifangqa/apps/indicator/models.py b/97suifangqa/apps/indicator/models.py
new file mode 100644
index 0000000..bd57d87
--- /dev/null
+++ b/97suifangqa/apps/indicator/models.py
@@ -0,0 +1,1127 @@
+# -*- coding: utf-8 -*-
+#
+# Weitian Li <liweitianux@foxmail.com>
+# updated: 2013/08/12
+#
+
+"""
+apps/indicator models
+"""
+
+from django.db import models
+from django.contrib import admin
+from django.contrib.auth.models import User
+# '@permalink' is no longer recommended
+from django.core.urlresolvers import reverse
+
+import re
+import datetime
+
+import sympy
+from sympy.core.sympify import SympifyError
+
+from utils.xpinyin import Pinyin
+
+
+class IndicatorCategory(models.Model): # {{{
+ """
+ 对 Indicator 进行分类,用于前端按分类显示和选择指标。
+ """
+ name = models.CharField(u"指标类别名称", max_length=100)
+ pinyin = models.CharField(u"拼音", max_length=200,
+ editable=False, blank=True)
+ englishName = models.CharField(u"Indicator Category Name",
+ max_length=200, blank=True)
+ description = models.TextField(u"指标类别描述", blank=True)
+ # 记录添加的用户,用户只能修改自己添加的对象
+ addByUser = models.ForeignKey(User, verbose_name=u"添加的用户",
+ related_name="indicator_categories")
+
+ class Meta:
+ verbose_name_plural = u"指标类别"
+ ordering = ['pinyin', 'id']
+
+ def __unicode__(self):
+ return u"< IndicatorCategory: #%s, %s addBy %s >"\
+ % (self.id, self.name, self.addByUser.username)
+
+ def show(self):
+ """
+ used in 'search/search.html'
+ to show search result
+ """
+ return self.__unicode__()
+
+ def get_absolute_url(self):
+ # need define url with name='show-category', 'pk' as parameter
+ return reverse('show-category',
+ kwargs={'pk': self.id})
+
+ # auto generate `pinyin'
+ def save(self, **kwargs):
+ p = Pinyin()
+ self.pinyin = p.get_pinyin(self.name)
+ super(IndicatorCategory, self).save(**kwargs)
+
+ def dump(self, **kwargs):
+ dump_data = {
+ 'id': self.id,
+ 'name': self.name,
+ 'pinyin': self.pinyin,
+ 'englishName': self.englishName,
+ 'description': self.description,
+ 'addByUser_id': self.addByUser.id,
+ }
+ return dump_data
+# }}}
+
+
+class Indicator(models.Model): # {{{
+ """
+ 指标模型
+ """
+ name = models.CharField(u"指标名称", max_length=100)
+ pinyin = models.CharField(u"拼音", max_length=200,
+ editable=False, blank=True)
+ englishName = models.CharField(u"Indicator Name",
+ max_length=200, blank=True)
+ description = models.TextField(u"指标描述", blank=True)
+ # Indicator 接受数据类型/格式等说明/示例
+ helpText = models.CharField(u"帮助", max_length=300, blank=True)
+ # 记录添加指标的用户,用户只能修改自己添加的指标
+ addByUser = models.ForeignKey(User, verbose_name=u"添加的用户",
+ related_name="indicators")
+ # Category
+ categories = models.ManyToManyField(IndicatorCategory,
+ verbose_name=u"所属类别", related_name="indicators")
+ # DATA_TYPES for indicator
+ INTEGER_TYPE = u'IN' # 整数型
+ FLOAT_TYPE = u'FL' # 浮点型
+ RANGE_TYPE = u'RG' # 范围型(eg. 250-500)
+ FLOAT_RANGE_TYPE = u'FR' # 浮点型/范围型,接受定值或范围
+ PM_TYPE = u'PM' # +/- 型
+ RADIO_TYPE = u'RD' # 单选型
+ CHECKBOX_TYPE = u'CB' # 多选多
+ DATA_TYPES = (
+ (INTEGER_TYPE, u"整数型"),
+ (FLOAT_TYPE, u"浮点定值型"),
+ (RANGE_TYPE, u"浮点范围型"),
+ (FLOAT_RANGE_TYPE, u"定值或范围型"),
+ (PM_TYPE, u"阴阳型(+/-)"),
+ #(RADIO_TYPE, u"单选型"),
+ #(CHECKBOX_TYPE, u"多选型"),
+ )
+ dataType = models.CharField(u"数据类型", max_length=2,
+ choices=DATA_TYPES)
+
+ class Meta:
+ verbose_name_plural = u"医学指标"
+ ordering = ['pinyin', 'id']
+
+ def __unicode__(self):
+ return u"< Indicator: #%s, %s, dataType %s addBy %s >"\
+ % (self.id, self.name, self.dataType,
+ self.addByUser.username)
+
+ def show(self):
+ """
+ used in 'search/search.html'
+ to show search result
+ """
+ return self.__unicode__()
+
+ def get_absolute_url(self):
+ return reverse('show-indicator',
+ kwargs={'pk': self.id})
+
+ # auto generate `pinyin'
+ def save(self, **kwargs):
+ p = Pinyin()
+ self.pinyin = p.get_pinyin(self.name)
+ super(Indicator, self).save(**kwargs)
+
+ def check_unit(self, **kwargs):
+ """
+ Check if the validity of the units specified for the indicator.
+ A indicator must have one 'standard unit'.
+ if indicator.dataType in [INTEGER_TYPE, PM_TYPE],
+ then units are not needed.
+ """
+ if self.dataType in [self.FLOAT_TYPE, self.RANGE_TYPE,
+ self.FLOAT_RANGE_TYPE]:
+ std_unit = self.units.filter(standard=True)
+ if std_unit:
+ return True
+ else:
+ print u"Indicator id=%s 未指定标准单位" % self.id
+ return False
+ else:
+ print u"dataType=%s 不需要单位" % self.dataType
+ return True
+
+ def _get_unit(self, type="standard"):
+ if type == "standard":
+ _units = self.units.filter(standard=True)
+ elif type == "other":
+ _units = self.units.filter(standard=False)
+ else:
+ _units = []
+ return list(_units)
+
+ def get_unit(self, type="standard"):
+ """
+ return a 'list' which contains the 'Unit's
+ related to the indicator
+ get_unit(type):
+ type = standard(default), other, all
+ this return the 'standard unit' by default
+ if 'type="all"', the 'standard unit' comes first
+ """
+ if type == "all":
+ # get all units
+ # standard unit first
+ _units = self._get_unit(type="standard")\
+ + self._get_unit(type="other")
+ return _units
+ else:
+ return self._get_unit(type)
+
+ def check_confine(self):
+ """
+ check the existence of the related InnateConfine
+ """
+ try:
+ c = self.innate_confine
+ return True
+ except InnateConfine.DoesNotExist:
+ print u'Indicator id=%s 未指定 InnateConfine' % self.id
+ raise ValueError(u'Indicator id=%s 未指定 InnateConfine'
+ % self.id)
+ return False
+
+ def get_confine(self):
+ """
+ dump the confine data from the related InnateConfine
+ """
+ try:
+ c = self.innate_confine
+ return c.dump()
+ except InnateConfine.DoesNotExist:
+ print u'Indicator id=%s 未指定 InnateConfine' % self.id
+ return {}
+
+ def is_ready(self):
+ """
+ check the status of this indicator,
+ if 'Unit's and 'InnateConfine' are correctly specified,
+ then the Indicator is ready to use. returned 'True'
+ """
+ return (self.check_unit() and self.check_confine())
+
+ def dump(self, **kwargs):
+ dump_data = {
+ 'id': self.id,
+ 'name': self.name,
+ 'pinyin': self.pinyin,
+ 'englishName': self.englishName,
+ 'description': self.description,
+ 'helpText': self.helpText,
+ 'addByUser_id': self.addByUser.id,
+ 'dataType': self.dataType,
+ 'categories_id': [c.id
+ for c in self.categories.all()],
+ 'units_id': [u.id
+ for u in self.get_unit(type="all")]
+ }
+ return dump_data
+# }}}
+
+
+class UserIndicator(models.Model): # {{{
+ """
+ 记录某用户关注了哪些指标
+ """
+ user = models.OneToOneField(User, verbose_name=u"用户",
+ related_name="user_indicator")
+ followedIndicators = models.ManyToManyField(Indicator,
+ verbose_name=u"关注的指标",
+ related_name="user_indicators",
+ null=True, blank=True)
+
+ class Meta:
+ verbose_name_plural = u"用户指标信息"
+
+ def __unicode__(self):
+ return u"< UserIndicator: for %s >" % self.user.username
+# }}}
+
+
+class IndicatorRecord(models.Model): # {{{
+ """
+ 指标记录
+ 对应某指标某一次的数据记录
+ """
+ indicator = models.ForeignKey(Indicator, verbose_name=u"化验指标",
+ related_name="indicator_records")
+ user = models.ForeignKey(User, verbose_name=u"用户",
+ related_name="indicator_records")
+ # date
+ created_at = models.DateTimeField(u"创建时间", auto_now_add=True)
+ updated_at = models.DateTimeField(u"更新时间",
+ auto_now_add=True, auto_now=True)
+ # data
+ date = models.DateField(u"化验日期")
+ # TODO: limit_choices_to
+ unit = models.ForeignKey("Unit", verbose_name=u"数据单位",
+ related_name="indicator_records", null=True, blank=True)
+ value = models.CharField(u"指标数据值", max_length=30,
+ blank=True)
+ val_max = models.FloatField(u"数据范围上限",
+ null=True, blank=True)
+ val_min = models.FloatField(u"数据范围下限",
+ null=True, blank=True)
+ notes = models.TextField(u"记录说明", blank=True)
+
+ class Meta:
+ verbose_name_plural = u"指标数据记录"
+ ordering = ['id', 'date', 'created_at']
+
+ def __unicode__(self):
+ return u"< IndicatorRecord: #%s; %s, %s, %s >" % (self.id,
+ self.user.username, self.indicator.name, self.date)
+
+ def get_absolute_url(self):
+ return reverse('show-record',
+ kwargs={'pk': self.id})
+
+ def save(self, **kwargs):
+ if self.is_valid() and self.check_confine:
+ super(IndicatorRecord, self).save(**kwargs)
+ else:
+ raise ValueError(u'您输入的数据不符合要求')
+
+ def is_valid(self, **kwargs): # {{{
+ """验证输入数据是否合法"""
+ if self.indicator.dataType == self.indicator.INTEGER_TYPE:
+ # 整数型
+ try:
+ value = int(self.value)
+ return True
+ except ValueError:
+ raise ValueError(u'您提交的指标数据类型不正确')
+ return False
+ elif self.indicator.dataType == self.indicator.FLOAT_TYPE:
+ # 浮点型
+ if not self.unit:
+ raise ValueError(u'未填写单位')
+ return False
+ try:
+ value = float(self.value)
+ return True
+ except ValueError:
+ raise ValueError(u'value 数据类型不正确')
+ return False
+ elif self.indicator.dataType == self.indicator.RANGE_TYPE:
+ # 范围型
+ if not self.unit:
+ raise ValueError(u'未填写单位')
+ return False
+ if (self.val_max is None) or (self.val_min is None):
+ raise ValueError(u'val_max 或 val_min 未填写')
+ return False
+ if (self.val_max <= self.val_min):
+ raise ValueError(u'val_max <= val_min')
+ return False
+ return True
+ elif self.indicator.dataType == self.indicator.FLOAT_RANGE_TYPE:
+ # 定值/范围型 (浮点定值优先)
+ if not self.unit:
+ raise ValueError(u'未填写单位')
+ return False
+ if self.value:
+ # 定值
+ try:
+ value = float(self.value)
+ return True
+ except ValueError:
+ raise ValueError(u'value 数据类型不正确')
+ return False
+ elif (self.val_max is not None) and (self.val_min is not None):
+ # 范围值
+ if (self.val_max <= self.val_min):
+ raise ValueError(u'val_max <= val_min')
+ return False
+ else:
+ return True
+ else:
+ raise ValueError(u'您提交的指标数据不符合要求')
+ return False
+ elif self.indicator.dataType == self.indicator.PM_TYPE:
+ # +/- 型,无单位要求
+ if (len(self.value) == 1) and (self.value in [u'+', u'-']):
+ return True
+ else:
+ raise ValueError(u'value 只接受 "+" 或 "-"')
+ return False
+ ## TODO: RADIO_TYPE, CHECKBOX_TYPE
+ elif self.indicator.dataType in [self.indicator.RADIO_TYPE,
+ self.indicator.CHECKBOX_TYPE]:
+ raise ValueError(u'RADIO_TYPE, CHECKBOX_TYPE 验证未实现')
+ return False
+ else:
+ raise ValueError(u'指标数据类型不合法')
+ return False
+ # }}}
+
+ def check_confine(self, **kwargs): # {{{
+ """
+ check if the record data within the related confine:
+ math_min <= value <= math_max
+
+ NOTE: convert record data to 'standard unit' before comparison
+ """
+ sind = self.indicator
+ # unit relation
+ unit_rel = self.unit.relation
+ v = sympy.symbols('v')
+ rel_sym = sympy.sympify(unit_rel)
+ # error message
+ errmsg = u"'%s' 求值错误,请检查只含有变量 'v'" % unit_rel
+ # check
+ if sind.dataType in [sind.FLOAT_TYPE, sind.RANGE_TYPE,
+ sind.FLOAT_RANGE_TYPE]:
+ if not sind.check_confine():
+ return False
+ # InnateConfine is ok
+ sic = sind.innate_confine
+ # value
+ if self.value:
+ try:
+ value = float(self.value)
+ except ValueError:
+ print u'ERROR: value="%s" cannot convert to float'\
+ % self.value
+ return False
+ # 'value' unit conversion
+ try:
+ value_std = float(rel_sym.evalf(subs={v: value}))
+ except ValueError:
+ print errmsg
+ raise ValueError(errmsg)
+ if (value_std < sic.math_min) or (
+ value_std > sic.math_max):
+ print u'ERROR: value(std) < math_min or value(std) > math_max'
+ return False
+ # val_max
+ if self.val_max is not None:
+ # unit conversion
+ try:
+ val_max_std = float(rel_sym.evalf(
+ subs={v: self.val_max}))
+ except ValueError:
+ print errmsg
+ raise ValueError(errmsg)
+ if (val_max_std <= sic.math_min) or (
+ val_max_std > sic.math_max):
+ print u'ERROR: val_max(std) <= math_min or val_max(std) > math_max'
+ return False
+ # val_min
+ if self.val_min is not None:
+ try:
+ val_min_std = float(rel_sym.evalf(
+ subs={v: self.val_min}))
+ except ValueError:
+ print errmsg
+ raise ValueError(errmsg)
+ if (val_min_std < sic.math_min) or (
+ val_min_std >= sic.math_max):
+ print u'ERROR: val_min(std) < math_min or val_min(std) >= math_max'
+ return False
+ # check finished
+ return True
+ else:
+ # INTEGER_TYPE or PM_TYPE
+ return True
+
+ # }}}
+
+ def get_data(self, **kwargs): # {{{
+ """
+ get the record data
+ in unit originally filled by the user
+ """
+ # check the indicator.dataType
+ sind = self.indicator
+ if sind.dataType in [sind.FLOAT_TYPE, sind.RANGE_TYPE,
+ sind.FLOAT_RANGE_TYPE]:
+ # self.value
+ if self.value:
+ value = float(self.value)
+ else:
+ value = None
+ # self.val_max
+ if self.val_max:
+ val_max = self.val_max
+ else:
+ val_max = None
+ # self.val_min
+ if self.val_min:
+ val_min = self.val_min
+ else:
+ val_min = None
+ # output data
+ data = {
+ 'date': self.date.isoformat(),
+ 'value': value,
+ 'val_max': val_max,
+ 'val_min': val_min,
+ 'unit': self.unit.dump(),
+ 'notes': self.notes,
+ 'record_histories_id': [rh.id
+ for rh in self.record_histories.all()],
+ }
+ else:
+ data = {
+ 'date': self.date.isoformat(),
+ 'value': self.value,
+ 'val_max': self.val_max,
+ 'val_min': self.val_min,
+ 'unit': {},
+ 'notes': self.notes,
+ 'record_histories_id': [rh.id
+ for rh in self.record_histories.all()],
+ }
+ return data
+ # }}}
+
+ def get_data_std(self, **kwargs): # {{{
+ """
+ get the record data in 'standard unit'
+ """
+ # check the indicator.dataType
+ sind = self.indicator
+ if sind.dataType in [sind.FLOAT_TYPE, sind.RANGE_TYPE,
+ sind.FLOAT_RANGE_TYPE]:
+ # check if self.unit standard
+ if self.unit.standard:
+ return self.get_data(**kwargs)
+ # check if specified 'standard unit' for this indicator
+ elif sind.check_unit():
+ # unit relation
+ std_unit = sind.get_unit(type="standard")[0]
+ unit_rel = self.unit.relation
+ v = sympy.symbols('v')
+ rel_sym = sympy.sympify(unit_rel)
+ # error message
+ errmsg = u"'%s' 求值错误,请检查只含有变量 'v'" % unit_rel
+ # self.value
+ if self.value:
+ value = float(self.value)
+ try:
+ value_std = float(rel_sym.evalf(
+ subs={v: value}))
+ except ValueError:
+ print errmsg
+ raise ValueError(errmsg)
+ else:
+ value_std = None
+ # self.val_max
+ if self.val_max:
+ val_max = self.val_max
+ try:
+ val_max_std = float(rel_sym.evalf(
+ subs={v: val_max}))
+ except ValueError:
+ print errmsg
+ raise ValueError(errmsg)
+ else:
+ val_max_std = None
+ # self.val_min
+ if self.val_min:
+ val_min = self.val_min
+ try:
+ val_min_std = float(rel_sym.evalf(
+ subs={v: val_min}))
+ except ValueError:
+ print errmsg
+ raise ValueError(errmsg)
+ else:
+ val_min_std = None
+ # output data
+ data_std = {
+ 'date': self.date.isoformat(),
+ 'value': value_std,
+ 'val_max': val_max_std,
+ 'val_min': val_min_std,
+ 'unit': std_unit.dump(),
+ 'notes': self.notes,
+ 'record_histories_id': [rh.id
+ for rh in self.record_histories.all()],
+ }
+ return data_std
+ else:
+ print u"id=%s Indicator 尚未指定标准单位" % sind.id
+ return {}
+ else:
+ return self.get_data(**kwargs)
+ # }}}
+
+ def is_normal(self, **kwargs): # {{{
+ """
+ compare the given data with the indicator confines.
+
+ if the data within the confines, then return 'True',
+ which suggests the indicator is normal.
+ if the data out of the confines, then return 'False'.
+
+ * return 'None' if there are other problems.
+ """
+ sind = self.indicator
+ # 先检查 Unit 和 InnateConfine 是否已经正确指定
+ if not sind.is_ready():
+ print u"ERROR: Indicator id=%s NOT ready yet" % sind.id
+ return None
+ sic = sind.innate_confine
+ # 获取以标准单位为单位的数据
+ data_std = self.get_data_std()
+ # 根据数据类型判断是否处于正常情况
+ if sind.dataType == sind.INTEGER_TYPE:
+ # 整数型
+ value = int(data_std['value'])
+ val_norm = int(sic.val_norm)
+ # XXX: modify accordingly
+ if value == val_norm:
+ return True
+ else:
+ return False
+ elif sind.dataType == sind.FLOAT_TYPE:
+ # 浮点型
+ value = data_std['value']
+ human_max = sic.human_max
+ human_min = sic.human_min
+ if (value <= human_max) and (value >= human_min):
+ return True
+ else:
+ return False
+ elif sind.dataType == sind.RANGE_TYPE:
+ # 范围型
+ val_max = data_std['val_max']
+ val_min = data_std['val_min']
+ human_max = sic.human_max
+ human_min = sic.human_min
+ if (val_max <= human_max) and (val_min >= human_min):
+ return True
+ else:
+ return False
+ elif sind.dataType == sind.FLOAT_RANGE_TYPE:
+ # 浮点型/范围型
+ if self.value:
+ value = float(data_std['value'])
+ human_max = sic.human_max
+ human_min = sic.human_min
+ if (value <= human_max) and (value >= human_min):
+ return True
+ else:
+ return False
+ elif self.val_max and self.val_min:
+ # 范围值
+ val_max = data_std['val_max']
+ val_min = data_std['val_min']
+ human_max = sic.human_max
+ human_min = sic.human_min
+ if (val_max <= human_max) and (val_min >= human_min):
+ return True
+ else:
+ return False
+ else:
+ print u'数据类型错误'
+ raise ValueError(u'数据类型错误')
+ return None
+ elif sind.dataType == sind.PM_TYPE:
+ # 阴阳(+/-)型
+ value = data_std['value']
+ val_norm = sic.val_norm
+ if value == val_norm:
+ return True
+ else:
+ return False
+ elif sind.dataType in [sind.RADIO_TYPE, sind.CHECKBOX_TYPE]:
+ print u'RADIO_TYPE, CHECKBOX_TYPE 验证未实现'
+ raise ValueError(u'RADIO_TYPE, CHECKBOX_TYPE 验证未实现')
+ return None
+ else:
+ print u'数据类型不合法'
+ raise ValueError(u'数据类型不合法')
+ return None
+ # }}}
+
+ def dump(self, **kwargs):
+ dump_data = {
+ 'id': self.id,
+ 'indicator_id': self.indicator.id,
+ 'user_id': self.user.id,
+ 'created_at': self.created_at.isoformat(),
+ 'updated_at': self.updated_at.isoformat(),
+ 'date': self.date.isoformat(),
+ 'unit_id': self.unit.id,
+ 'value': self.value,
+ 'val_max': self.val_max,
+ 'val_min': self.val_min,
+ 'notes': self.notes,
+ 'record_histories_id': [rh.id
+ for rh in self.record_histories.all()],
+ }
+ return dump_data
+# }}}
+
+
+class RecordHistory(models.Model): # {{{
+ """
+ 指标记录 IndicatorRecord 的历史数据和对应的修改原因
+ """
+ indicatorRecord = models.ForeignKey("IndicatorRecord",
+ verbose_name=u"指标数据记录",
+ related_name="record_histories")
+ # modification datetime
+ created_at = models.DateTimeField(u"创建时间", auto_now_add=True)
+ # modification reason
+ reason = models.TextField(u"修改原因")
+ # original data before modification
+ date_bak = models.DateField(u"原化验日期", blank=True,
+ editable=False)
+ unit_bak = models.ForeignKey("Unit", verbose_name=u"原数据单位",
+ related_name="record_histories",
+ null=True, blank=True, editable=False)
+ value_bak = models.CharField(u"原指标数据值", max_length=30,
+ blank=True, editable=False)
+ val_max_bak = models.FloatField(u"原数据范围上限",
+ null=True, blank=True, editable=False)
+ val_min_bak = models.FloatField(u"原数据范围下限",
+ null=True, blank=True, editable=False)
+ notes_bak = models.TextField(u"原记录说明", blank=True,
+ editable=False)
+
+ class Meta:
+ verbose_name_plural = u"记录修改历史"
+ ordering = ['indicatorRecord__id', 'created_at']
+
+ def __unicode__(self):
+ return u"< RecordHistory: for Record #%s, %s >"\
+ % (self.indicatorRecord.id, self.created_at)
+
+ def save(self, **kwargs):
+ sr = self.indicatorRecord
+ # get history data from *not-saved* IndicatorRecord
+ self.date_bak = sr.date
+ self.unit_bak = sr.unit
+ self.value_bak = sr.value
+ self.val_max_bak = sr.val_max
+ self.val_min_bak = sr.val_min
+ self.notes_bak = sr.notes
+ # save
+ super(RecordHistory, self).save(**kwargs)
+
+ def dump(self, **kwargs):
+ dump_data = {
+ 'id': self.id,
+ 'indicatorRecord_id': self.indicatorRecord.id,
+ 'created_at': self.created_at.isoformat(),
+ 'reason': self.reason,
+ 'date_bak': self.date_bak.isoformat(),
+ 'unit_bak_id': self.unit_bak.id,
+ 'value_bak': self.value_bak,
+ 'val_max_bak': self.val_max_bak,
+ 'val_min_bak': self.val_min_bak,
+ 'notes_bak': self.notes_bak,
+ }
+ return dump_data
+# }}}
+
+
+class Unit(models.Model): # {{{
+ """
+ 指标单位
+ 是否为标准单位,其他单位与标准单位的换算关系
+ """
+ # related to the `indicator'
+ indicator = models.ForeignKey(Indicator, verbose_name=u"指标",
+ related_name="units")
+ name = models.CharField(u"单位名称", max_length=50)
+ symbol = models.CharField(u"单位符号", max_length=50)
+ standard = models.BooleanField(u"是否标准单位", default=False)
+ # conversion relation
+ relation = models.CharField(u"与标准单位的映射",
+ help_text=u"value (std_unit) = f(v)",
+ max_length=100, blank=True)
+ description = models.TextField(u"单位描述", blank=True)
+ # 记录添加的用户,用户只能修改自己添加的对象
+ addByUser = models.ForeignKey(User, verbose_name=u"添加的用户",
+ related_name="units")
+
+ class Meta:
+ verbose_name_plural = u"单位"
+
+ def __unicode__(self):
+ if self.standard:
+ _std = ' (*)'
+ else:
+ _std = ''
+ return u"< Unit: %s%s for %s, addBy %s >" % (self.name,
+ _std, self.indicator.name, self.addByUser.username)
+
+ def is_valid(self):
+ if self.standard:
+ std_unit_list = self.indicator.get_unit(type="standard")
+ if std_unit_list:
+ std_unit = std_unit_list[0]
+ if self.id == std_unit.id:
+ return True
+ else:
+ print u"该指标已经指定了标准单位"
+ raise ValueError(u"该指标已经指定了标准单位")
+ return False
+ else:
+ return True
+ else:
+ if (not self.relation):
+ print u"单位映射关系未填写"
+ raise ValueError(u"单位映射关系未填写")
+ return False
+ else:
+ try:
+ fsym = sympy.sympify(self.relation)
+ return True
+ except SympifyError:
+ print u"'%s' 不是合法的算术表达式" % self.relation
+ raise ValueError(u"'%s' 不是合法的算术表达式"\
+ % self.relation)
+ return False
+
+ def save(self, **kwargs):
+ if self.standard:
+ self.relation = "v"
+ if self.is_valid():
+ super(Unit, self).save(**kwargs)
+
+ def dump(self, **kwargs):
+ dump_data = {
+ 'id': self.id,
+ 'indicator_id': self.indicator.id,
+ 'name': self.name,
+ 'symbol': self.symbol,
+ 'standard': self.standard,
+ 'relation': self.relation,
+ 'addByUser_id': self.addByUser.id,
+ }
+ return dump_data
+# }}}
+
+
+class InnateConfine(models.Model): # {{{
+ """
+ 指标数据范围
+ 数学可能值范围,人体正常值范围
+
+ 注意:
+ 如果数据类型需要单位,则必须使用"标准单位";
+ IndicatorRecord.is_normal() 方法需要如此;
+ 因为 标准单位 到 其他单位 的换算没有实现。
+ """
+ # indicator
+ indicator = models.OneToOneField("Indicator",
+ verbose_name=u"指标", related_name="innate_confine")
+ # unit
+ # TODO: limit_choices_to
+ unit = models.ForeignKey("Unit", related_name="innate_confines",
+ verbose_name=u"单位", null=True, blank=True)
+ # normal value (for INTEGER_TYPE, PM_TYPE)
+ val_norm = models.CharField(u"正常值", max_length=30, blank=True,
+ help_text=u'填写"整数型","阴阳(+/-)型数据"')
+ # normal range
+ human_max = models.FloatField(u"人体正常值上限",
+ null=True, blank=True)
+ human_min = models.FloatField(u"人体正常值下限",
+ null=True, blank=True)
+ # possbile range
+ math_max = models.FloatField(u"数学可能值上限",
+ null=True, blank=True)
+ math_min = models.FloatField(u"数学可能值下限",
+ null=True, blank=True)
+ # description or notes
+ description = models.TextField(u"描述", blank=True)
+ # 记录添加的用户,用户只能修改自己添加的对象
+ addByUser = models.ForeignKey(User, verbose_name=u"添加的用户",
+ related_name="innate_confines")
+
+ class Meta:
+ verbose_name_plural = u"固有数值范围"
+
+ def __unicode__(self):
+ return u"< InnateConfine: for %s, addBy %s >"\
+ % (self.indicator.name, self.addByUser.username)
+
+ def save(self, **kwargs):
+ """
+ check the data before save
+ """
+ if self.is_valid():
+ super(InnateConfine, self).save(**kwargs)
+ else:
+ print u"您填写的数据不符合要求,请检查"
+ return self
+
+ def is_valid(self): # {{{
+ """
+ check the validity of data
+ """
+ sind = self.indicator
+ if sind.dataType in [sind.FLOAT_TYPE, sind.RANGE_TYPE,
+ sind.FLOAT_RANGE_TYPE]:
+ # check unit
+ if not (self.unit and self.unit.standard):
+ raise ValueError(u'单位未填写/不是标准单位')
+ return False
+ if (self.human_max is None) or (self.human_min is None):
+ raise ValueError(u'Error: human_max 或 human_min 未填写')
+ return False
+ if not (self.human_max > self.human_min):
+ raise ValueError(u'Error: human_max <= human_min')
+ return False
+ # check 'math_max' and 'math_min'
+ if (self.math_max is None) or (self.math_min is None):
+ raise ValueError(u'Error: math_max 或 math_min 未填写')
+ return False
+ if not (self.math_max > self.math_min):
+ raise ValueError(u'Error: math_max <= math_min')
+ return False
+ # compare 'human*' and 'math*'
+ if (self.human_max > self.math_max) or (
+ self.human_min < self.math_min):
+ raise ValueError(u'Error: human_max>math_max / human_min<math_min')
+ return False
+ # check finished
+ return True
+ elif sind.dataType == sind.INTEGER_TYPE:
+ # 整数型
+ try:
+ val_norm = int(self.val_norm)
+ return True
+ except ValueError:
+ raise ValueError(u'val_norm="%s" 不是整数型值'
+ % self.val_norm)
+ return False
+ elif sind.dataType == sind.PM_TYPE:
+ # 阴阳(+/-)型
+ if (len(self.val_norm) == 1) and (
+ self.val_norm in [u'+', u'-']):
+ return True
+ else:
+ raise ValueError(u'value 只接受 "+" 或 "-"')
+ return False
+ ## TODO: RADIO_TYPE, CHECKBOX_TYPE
+ elif sind.dataType in [sind.RADIO_TYPE, sind.CHECKBOX_TYPE]:
+ raise ValueError(u'RADIO_TYPE, CHECKBOX_TYPE 验证未实现')
+ return False
+ else:
+ raise ValueError(u'数据不符合要求')
+ return False
+ # }}}
+
+ def dump(self, **kwargs):
+ dump_data = {
+ 'id': self.id,
+ 'indicator_id': self.indicator.id,
+ 'unit': self.unit.dump(),
+ 'val_norm': self.val_norm,
+ 'human_max': self.human_max,
+ 'human_min': self.human_min,
+ 'math_max': self.math_max,
+ 'math_min': self.math_min,
+ 'addByUser_id': self.addByUser.id,
+ }
+ return dump_data
+# }}}
+
+
+class StatisticalConfine(models.Model): # {{{
+
+ deviation_ceiling = models.FloatField(u"统计偏差范围上限", null=True, blank=True)
+ deviation_floor = models.FloatField(u"统计偏差范围限", null=True, blank=True)
+
+ class Meta:
+ verbose_name_plural = u"统计数值范围"
+
+ def __unicode__(self):
+ return "< StatisticalConfine: %s >" % self.id
+# }}}
+
+
+#class UnitLabSheet(models.Model): # {{{
+#
+# equipment = models.CharField(u"化验设备", max_length=100, null=True, blank= True)
+# #figure = models.OneToOneField("figure.Figure", verbose_name=u"图片", related_name="unitlabsheet")
+# unit_standard = models.ForeignKey("UnitStandard", verbose_name = u"单位标准", related_name="unit_lab_sheets", null=True, blank=True)
+#
+# class Meta:
+# verbose_name_plural = u"标准化验单"
+#
+# def __unicode__(self):
+# return "< UnitLabSheet: %s >" % self.id
+## }}}
+
+
+#class ReviseReason(models.Model): # {{{
+# """
+# 记录 IndicatorRecord 数据修改原因
+# 医学数据重要且要求准确,不可随意修改
+# ReviseReason 添加后不允许再修改?
+# """
+# # TODO: 中文支持
+# content = models.TextField(u"内容")
+# created_at = models.DateTimeField(u"创建时间",
+# editable=False, auto_now_add=True)
+# user = models.ForeignKey(User, verbose_name=u"用户")
+#
+# class Meta:
+# verbose_name_plural=u"指标记录修改原因"
+#
+# def __unicode__(self):
+# return u"< ReviseReason: %s, %s >" % (self.id,
+# self.user.username)
+#
+# def dump(self, **kwargs):
+# dump_data = {
+# 'id': self.id,
+# 'content': self.content,
+# 'created_at': self.created_at.isoformat(),
+# 'user_id': self.user.id,
+# }
+# return dump_data
+## }}}
+
+
+class RelatedIndicator(models.Model): # {{{
+ """
+ 记录 blog/annotation 与哪些 indicator 关联,
+ 以及关联的权重。
+
+ 用于为用户推荐可以关注的指标。
+ """
+ # indicator
+ indicator = models.ForeignKey("Indicator",
+ related_name="related_indicators",
+ verbose_name=u"待关联指标")
+ # type of related object
+ ANNOTATION_TYPE = 'AN'
+ BLOG_TYPE = 'BL'
+ OBJECT_TYPES = (
+ (ANNOTATION_TYPE, '文章注释'),
+ (BLOG_TYPE, '文章'),
+ )
+ objectType = models.CharField(u"待关联目标类型", max_length=2,
+ choices=OBJECT_TYPES)
+ # objects
+ annotation = models.ForeignKey("sciblog.BlogAnnotation",
+ related_name="related_indicators",
+ verbose_name=u"待关联文章注释", null=True, blank=True)
+ blog = models.ForeignKey("sciblog.SciBlog",
+ related_name="related_indicators",
+ verbose_name=u"待关联文章", null=True, blank=True)
+ # weight
+ weight = models.FloatField(u"权重", help_text=u"范围: 0-10")
+ # datetime
+ created_at = models.DateTimeField(u"创建时间", auto_now_add=True)
+ updated_at = models.DateTimeField(u"更新时间",
+ auto_now_add=True, auto_now=True)
+
+ class Meta:
+ verbose_name_plural = u"指标关联信息"
+ ordering = ['objectType']
+
+ def __unicode__(self):
+ if self.objectType == self.ANNOTATION_TYPE:
+ info = 'Annotation #%s' % self.annotation.id
+ elif self.objectType == self.BLOG_TYPE:
+ info = 'Blog #%s' % self.blog.id
+ else:
+ info = '%s' % self.objectType
+ return u"< RelatedIndicator: %s -> %s >"\
+ % (info, self.indicator.name)
+
+ def save(self, **kwargs):
+ if self.is_valid():
+ if self.objectType == self.ANNOTATION_TYPE:
+ self.blog = None
+ if self.objectType == self.BLOG_TYPE:
+ self.annotation = None
+ # save
+ super(RelatedIndicator, self).save(**kwargs)
+ else:
+ return self
+
+ def is_valid(self, **kwargs): # {{{
+ """
+ annotation/blog must be consistent with objectType
+ """
+ # check objectType
+ if self.objectType == self.ANNOTATION_TYPE:
+ if not self.annotation:
+ raise ValueError(u"Error: annotation 未填写")
+ return False
+ elif self.objectType == self.BLOG_TYPE:
+ if not self.blog:
+ raise ValueError(u"Error: blog 未填写")
+ return False
+ else:
+ raise ValueError(u"Error: objectType 不合法")
+ return False
+ # check weight range
+ if (self.weight < 0.0) or (self.weight > 10.0):
+ raise ValueError(u"Error: weight < 0.0 / weight > 10.0")
+ return False
+ # finished
+ return True
+ # }}}
+
+ def dump(self, **kwargs):
+ # annotation_id
+ if self.annotation:
+ annotation_id = self.annotation.id
+ else:
+ annotation_id = None
+ # blog_id
+ if self.blog:
+ blog_id = self.blog.id
+ else:
+ blog_id = None
+ # dump data
+ dump_data = {
+ 'id': self.id,
+ 'indicator_id': self.indicator.id,
+ 'objectType': self.objectType,
+ 'annotation_id': annotation_id,
+ 'blog_id': blog_id,
+ 'weight': self.weight,
+ 'created_at': self.created_at.isoformat(),
+ 'updated_at': self.updated_at.isoformat(),
+ }
+ return dump_data
+# }}}
+
+
+
+admin.site.register([
+ IndicatorCategory,
+ Indicator,
+ UserIndicator,
+ IndicatorRecord,
+ RecordHistory,
+ Unit,
+ InnateConfine,
+ StatisticalConfine,
+ #UnitLabSheet,
+ #ReviseReason,
+ RelatedIndicator,
+ ])
+
+# vim: set ts=4 sw=4 tw=0 fenc=utf-8 ft=python: #
diff --git a/97suifangqa/apps/indicator/search_indexes.py b/97suifangqa/apps/indicator/search_indexes.py
new file mode 100644
index 0000000..b7a8437
--- /dev/null
+++ b/97suifangqa/apps/indicator/search_indexes.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+
+from haystack import indexes
+
+from indicator import models as im
+
+
+
+# IndicatorCategoryIndex {{{
+class IndicatorCategoryIndex(indexes.SearchIndex, indexes.Indexable):
+ """
+ search index for 'Indicator'
+ """
+ text = indexes.CharField(document=True, use_template=True)
+ addByUser = indexes.CharField(model_attr='addByUser')
+
+ def get_model(self):
+ return im.IndicatorCategory
+
+ def index_queryset(self, using=None):
+ """
+ used when the entire index for model is updated
+ """
+ return self.get_model().objects.all()
+# }}}
+
+
+# IndicatorIndex {{{
+class IndicatorIndex(indexes.SearchIndex, indexes.Indexable):
+ """
+ search index for 'Indicator'
+ """
+ text = indexes.CharField(document=True, use_template=True)
+ addByUser = indexes.CharField(model_attr='addByUser')
+ dataType = indexes.CharField(model_attr='dataType')
+ categories = indexes.MultiValueField()
+
+ def get_model(self):
+ return im.Indicator
+
+ def prepare_categories(self, obj):
+ return [c.id for c in obj.categories.all()]
+
+ def index_queryset(self, using=None):
+ """
+ used when the entire index for model is updated
+ """
+ return self.get_model().objects.all()
+# }}}
+
+
+
+# vim: set ts=4 sw=4 tw=0 fenc=utf-8 ft=python:
diff --git a/97suifangqa/apps/indicator/templates/done.html b/97suifangqa/apps/indicator/templates/done.html
new file mode 100644
index 0000000..2a11ea8
--- /dev/null
+++ b/97suifangqa/apps/indicator/templates/done.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+ <title>DONE</title>
+</head>
+
+<body>
+ <h1>DONE</h1>
+</body>
+</html>
diff --git a/97suifangqa/apps/indicator/templates/show_category.html b/97suifangqa/apps/indicator/templates/show_category.html
new file mode 100644
index 0000000..1448bb3
--- /dev/null
+++ b/97suifangqa/apps/indicator/templates/show_category.html
@@ -0,0 +1,32 @@
+<html>
+<head>
+ <title>IndicatorCategory Details (id={{ object.id }})</title>
+</head>
+
+<body>
+ <h1>IndicatorCategory Details (id={{ object.id }})</h1>
+
+ <table>
+ <tr>
+ <td>name:</td>
+ <td>{{ object.name }}</td>
+ </tr>
+ <tr>
+ <td>pinyin:</td>
+ <td>{{ object.pinyin }}</td>
+ </tr>
+ <tr>
+ <td>englishName:</td>
+ <td>{{ object.englishName }}</td>
+ </tr>
+ <tr>
+ <td>description:</td>
+ <td>{{ object.description }}</td>
+ </tr>
+ <tr>
+ <td>addByUser_username:</td>
+ <td>{{ object.addByUser.username }}</td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/97suifangqa/apps/indicator/templates/show_indicator.html b/97suifangqa/apps/indicator/templates/show_indicator.html
new file mode 100644
index 0000000..0ecd027
--- /dev/null
+++ b/97suifangqa/apps/indicator/templates/show_indicator.html
@@ -0,0 +1,48 @@
+<html>
+<head>
+ <title>Indicator Details (id={{ object.id }})</title>
+</head>
+
+<body>
+ <h1>Indicator Details (id={{ object.id }})</h1>
+
+ <table>
+ <tr>
+ <td>name:</td>
+ <td>{{ object.name }}</td>
+ </tr>
+ <tr>
+ <td>pinyin:</td>
+ <td>{{ object.pinyin }}</td>
+ </tr>
+ <tr>
+ <td>englishName:</td>
+ <td>{{ object.englishName }}</td>
+ </tr>
+ <tr>
+ <td>description:</td>
+ <td>{{ object.description }}</td>
+ </tr>
+ <tr>
+ <td>helpText:</td>
+ <td>{{ object.helpText }}</td>
+ </tr>
+ <tr>
+ <td>addByUser_username:</td>
+ <td>{{ object.addByUser.username }}</td>
+ </tr>
+ <tr>
+ <td>categories_name:</td>
+ <td>
+ {% for c in object.categories.all %}
+ {{ c.name }};
+ {% endfor %}
+ </td>
+ </tr>
+ <tr>
+ <td>dataType:</td>
+ <td>{{ object.dataType }}</td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/97suifangqa/apps/indicator/templates/show_record.html b/97suifangqa/apps/indicator/templates/show_record.html
new file mode 100644
index 0000000..49c7918
--- /dev/null
+++ b/97suifangqa/apps/indicator/templates/show_record.html
@@ -0,0 +1,52 @@
+<html>
+<head>
+ <title>IndicatorRecord Details (id={{ object.id }})</title>
+</head>
+
+<body>
+ <h1>IndicatorRecord Details (id={{ object.id }})</h1>
+
+ <table>
+ <tr>
+ <td>indicator_name:</td>
+ <td>{{ object.indicator.name }}</td>
+ </tr>
+ <tr>
+ <td>user_username:</td>
+ <td>{{ object.user.username }}</td>
+ </tr>
+ <tr>
+ <td>created_at:</td>
+ <td>{{ object.created_at }}</td>
+ </tr>
+ <tr>
+ <td>updated_at:</td>
+ <td>{{ object.updated_at }}</td>
+ </tr>
+ <tr>
+ <td>date:</td>
+ <td>{{ object.date }}</td>
+ </tr>
+ <tr>
+ <td>unit_name:</td>
+ <td>{{ object.unit.name }}</td>
+ </tr>
+ <tr>
+ <td>value:</td>
+ <td>{{ object.value }}</td>
+ </tr>
+ <tr>
+ <td>val_max:</td>
+ <td>{{ object.val_max }}</td>
+ </tr>
+ <tr>
+ <td>val_min:</td>
+ <td>{{ object.val_min }}</td>
+ </tr>
+ <tr>
+ <td>notes:</td>
+ <td>{{ object.notes }}</td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/97suifangqa/apps/indicator/templates/simple.html b/97suifangqa/apps/indicator/templates/simple.html
new file mode 100644
index 0000000..7775ab7
--- /dev/null
+++ b/97suifangqa/apps/indicator/templates/simple.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+ <title>{{ action }} {{ object }}</title>
+</head>
+
+<body>
+ <h1>{{ action }} {{ object }}</h1>
+
+ {% if form.errors %}
+ <p style="color: red;">
+ Please correct the error{{ form.errors|pluralize }} below.
+ </p>
+ {% endif %}
+
+ <form action="" method="post">{% csrf_token %}
+ <table>
+ {{ form.as_table }}
+ </table>
+ <input type="submit" value="Submit" />
+ </form>
+</body>
+</html>
+
diff --git a/97suifangqa/apps/indicator/tools.py b/97suifangqa/apps/indicator/tools.py
new file mode 100644
index 0000000..663ec4f
--- /dev/null
+++ b/97suifangqa/apps/indicator/tools.py
@@ -0,0 +1,273 @@
+# -*- coding: utf-8 -*-
+
+"""
+utils for apps/indicator
+"""
+
+from django.contrib.auth.models import User
+
+from indicator import models as im
+from sciblog import models as sciblogm
+
+import datetime
+
+
+# get_indicator {{{
+def get_indicator(category_id="all", startswith="all"):
+ """
+ 根据指定的 category_id 和 startswith 获取 indicator
+ 返回一个 dict
+ Dict format:
+ dict = {
+ 'a': [ {'pinyin': 'aa', ...}, {'pinyin': 'ab', ...}, ... ],
+ 'b': [ {'pinyin': 'ba', ...}, {'pinyin': 'bb', ...}, ... ],
+ ...
+ }
+ """
+
+ _idict = {}
+ if category_id == 'all':
+ iqueryset = im.Indicator.objects.all()
+ else:
+ try:
+ cid = int(category_id)
+ cate = im.IndicatorCategory.objects.get(id=cid)
+ iqueryset = cate.indicators.all()
+ except ValueError:
+ raise ValueError(u'category_id 不是整数型')
+ return _idict
+ except im.IndicatorCategory.DoesNotExist:
+ raise ValueError(u'id=%s 的 IndicatorCategory 不存在'
+ % cid)
+ return _idict
+
+ if startswith == 'all':
+ starts = map(chr, range(ord('a'), ord('z')+1))
+ else:
+ starts = []
+ _str = startswith.lower()
+ for i in _str:
+ if i >= 'a' and i <= 'z':
+ starts.append(i)
+
+ for l in starts:
+ iq = iqueryset.filter(pinyin__istartswith=l).order_by('pinyin')
+ _idict[l] = [ i.dump() for i in iq ]
+ return _idict
+# }}}
+
+
+# get_followed_indicator {{{
+def get_followed_indicator(user_id, category_id="all", startswith="all"):
+ """
+ 获取已关注的指标
+ 返回 dict, 格式与 get_indicator() 一致
+ """
+
+ u = User.objects.get(id=user_id)
+ ui, created = im.UserIndicator.objects.get_or_create(user=u)
+ _idict = {}
+ iqueryset = ui.followedIndicators.all()
+ if not category_id == 'all':
+ try:
+ cid = int(category_id)
+ iqueryset = iqueryset.filter(categories__id=cid)
+ except ValueError:
+ raise ValueError(u'category_id 不是整数型')
+ return _idict
+
+ if startswith == 'all':
+ starts = map(chr, range(ord('a'), ord('z')+1))
+ else:
+ starts = []
+ _str = startswith.lower()
+ for i in _str:
+ if i >= 'a' and i <= 'z':
+ starts.append(i)
+
+ for l in starts:
+ iq = iqueryset.filter(pinyin__istartswith=l).order_by('pinyin')
+ _idict[l] = [ i.dump() for i in iq ]
+ return _idict
+# }}}
+
+
+# get_unfollowed_indicator {{{
+def get_unfollowed_indicator(user_id, category_id="all", startswith="all"):
+ """
+ 获取未关注的指标
+ 返回 dict, 格式与 get_indicator() 一致
+ """
+
+ u = User.objects.get(id=user_id)
+ ui, created = im.UserIndicator.objects.get_or_create(user=u)
+ _idict = {}
+ iqueryset = im.Indicator.objects.exclude(user_indicators=ui)
+ if not category_id == 'all':
+ try:
+ cid = int(category_id)
+ iqueryset = iqueryset.filter(categories__id=cid)
+ except ValueError:
+ raise ValueError(u'category_id 不是整数型')
+ return _idict
+
+ if startswith == 'all':
+ starts = map(chr, range(ord('a'), ord('z')+1))
+ else:
+ starts = []
+ _str = startswith.lower()
+ for i in _str:
+ if i >= 'a' and i <= 'z':
+ starts.append(i)
+
+ for l in starts:
+ iq = iqueryset.filter(pinyin__istartswith=l).order_by('pinyin')
+ _idict[l] = [ i.dump() for i in iq ]
+ return _idict
+# }}}
+
+
+# get_record {{{
+def get_record(user_id, indicator_id, begin="", end="", std=False):
+ """
+ get_record(user_id, indicator_id, begin="", end="", std=False)
+
+ return a dict with 'date' as key, and 'get_data()' as value.
+ args 'begin' and 'end' to specify the date range.
+ arg 'std=True' to get data in standard unit
+ if 'begin=""', then the earliest date is given;
+ if 'end=""', then the latest date is given.
+
+ return dict format:
+ rdata = {
+ 'date1': [d1r1.get_data(), d1r2.get_data(), ...],
+ 'date2': [d2r1.get_data(), d2r2.get_data(), ...],
+ ...
+ }
+ """
+ uid = int(user_id)
+ indid = int(indicator_id)
+ all_records = im.IndicatorRecord.objects.\
+ filter(user__id=uid, indicator__id=indid).\
+ order_by('date', 'created_at')
+ # check if 'all_records' empty
+ if not all_records:
+ return {}
+ # set 'begin' and 'end'
+ if begin == '':
+ begin = all_records[0].date
+ if end == '':
+ end = all_records.reverse()[0].date
+ # check the validity of given 'begin' and 'end'
+ if (isinstance(begin, datetime.date) and
+ isinstance(end, datetime.date)):
+ records = all_records.filter(date__range=(begin, end))
+ _rdata = {}
+ for r in records:
+ _d = r.date.isoformat()
+ # get data
+ if std:
+ _data = r.get_data_std()
+ else:
+ _data = r.get_data()
+ #
+ if _rdata.has_key(_d):
+ # the date key already exist
+ _rdata[_d] += [_data]
+ else:
+ # the date key not exist
+ _rdata[_d] = [_data]
+ # return
+ return _rdata
+ else:
+ raise ValueError(u"begin='%s' or end='%s' 不是合法的日期" %
+ (begin, end))
+ return {}
+# }}}
+
+
+# get_record_std {{{
+def get_record_std(**kwargs):
+ return get_record(std=True, **kwargs)
+# }}}
+
+
+# calc_indicator_weight {{{
+def calc_indicator_weight(user_id, indicator_id):
+ """
+ calculate the weight of given indicator
+ used by 'recommend_indicator'
+ """
+ ### XXX: weight_type: how to store the weights into database ###
+ weight_annotation = 4.0
+ weight_blog_catched = 3.0
+ weight_blog_collected = 2.0
+ weight_other = 1.0
+ ################################################################
+ # weight = weight_type * relatedindicator.weight
+ user = User.objects.get(id=user_id)
+ ri_qs = im.RelatedIndicator.objects.filter(indicator__id=indicator_id)
+ if not ri_qs:
+ # queryset empty
+ w = 0.0
+ return w
+ # queryset not empty
+ annotation_ri_qs = ri_qs.filter(annotation__collected_by=user)
+ blogcatch_ri_qs = ri_qs.filter(blog__catched_by=user)
+ blogcollect_ri_qs = ri_qs.filter(blog__collected_by=user)
+ weights = []
+ if annotation_ri_qs:
+ # related to annotations collected by user
+ for ri in annotation_ri_qs:
+ w = weight_annotation * ri.weight
+ weights.append(w)
+ elif blogcatch_ri_qs:
+ # related to blogs catched by user
+ for ri in blogcatch_ri_qs:
+ w = weight_blog_catched * ri.weight
+ weights.append(w)
+ elif blogcollect_ri_qs:
+ # related to blogs catched by user
+ for ri in blogcollect_ri_qs:
+ w = weight_blog_collected * ri.weight
+ weights.append(w)
+ else:
+ # other type, use 'ri_qs' here
+ for ri in ri_qs:
+ w = weight_other * ri.weight
+ weights.append(w)
+ # return final result
+ return max(weights)
+# }}}
+
+
+# recommend_indicator {{{
+def recommend_indicator(user_id, number):
+ """
+ recommend unfollowed indicator for user,
+ based on his/her readings and collections.
+
+ return a list with the id's of recommended indicators
+
+ TODO:
+ performance test
+ """
+ user_id = int(user_id)
+ number = int(number)
+ # get unfollowed indicators
+ u = User.objects.get(id=user_id)
+ ui, created = im.UserIndicator.objects.get_or_create(user=u)
+ uf_ind_qs = im.Indicator.objects.exclude(user_indicators=ui)
+ # calc weight for each unfollowed indicator
+ weights = []
+ for ind in uf_ind_qs:
+ w = calc_indicator_weight(user_id, ind.id)
+ weights.append({'id': ind.id, 'weight': w})
+ # sort 'weights' dict list by key 'weight'
+ weights_sorted = sorted(weights, key=lambda item: item['weight'])
+ weights_sorted.reverse()
+ # return results with largest weights
+ return weights_sorted[:number]
+# }}}
+
+
diff --git a/97suifangqa/apps/indicator/urls.py b/97suifangqa/apps/indicator/urls.py
new file mode 100644
index 0000000..0b5b12a
--- /dev/null
+++ b/97suifangqa/apps/indicator/urls.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+
+"""
+URL configuration for apps/indicator
+"""
+
+from django.conf.urls.defaults import *
+
+from django.views.generic import DetailView, ListView
+from django.views.generic.simple import direct_to_template
+
+from indicator import models as im
+
+
+
+## named URLs
+## for 'django.core.urlresolvers.reverse()' in 'get_absolute_url()'
+urlpatterns = patterns('indicator.views',
+ # IndicatorCategory, name='show-category'
+ url(r'^show/category/(?P<pk>\d+)/$',
+ DetailView.as_view(
+ model=im.IndicatorCategory,
+ template_name='show_category.html'),
+ name='show-category'),
+ # Indicator, name='show-indicator'
+ url(r'^show/indicator/(?P<pk>\d+)/$',
+ DetailView.as_view(
+ model=im.Indicator,
+ template_name='show_indicator.html'),
+ name='show-indicator'),
+ # IndicatorRecord, name='show-record'
+ # TODO: howto add '@login_required'
+ url(r'^show/record/(?P<pk>\d+)/$',
+ DetailView.as_view(
+ model=im.IndicatorRecord,
+ template_name='show_record.html'),
+ name='show-record'),
+)
+
+
+urlpatterns += patterns('indicator.views',
+ ## test
+ url(r'^test/$', 'test_view', name='test'),
+ ## get_indicator_view
+ url(r'^list/(?P<startswith>all)/$',
+ 'get_indicator_view', name='get_indicator_view'),
+ url(r'^list/(?P<startswith>[a-zA-Z]+)/$',
+ 'get_indicator_view', name='get_indicator_view'),
+ url(r'^category/(?P<category_id>all)/$',
+ 'get_indicator_view', name='get_indicator_view'),
+ url(r'^category/(?P<category_id>\d+)/$',
+ 'get_indicator_view', name='get_indicator_view'),
+ url(r'^category/(?P<category_id>all)/(?P<startswith>all)/$',
+ 'get_indicator_view', name='get_indicator_view'),
+ url(r'^category/(?P<category_id>\d+)/(?P<startswith>all)/$',
+ 'get_indicator_view', name='get_indicator_view'),
+ url(r'^category/(?P<category_id>all)/(?P<startswith>[a-zA-Z]+)/$',
+ 'get_indicator_view', name='get_indicator_view'),
+ url(r'^category/(?P<category_id>\d+)/(?P<startswith>[a-zA-Z]+)/$',
+ 'get_indicator_view', name='get_indicator_view'),
+ ## get_followed_indicator_view
+ url(r'^followed/(?P<startswith>all)/$',
+ 'get_followed_indicator_view', name='get_followed_indicator_view'),
+ url(r'^followed/(?P<startswith>[a-zA-Z]+)/$',
+ 'get_followed_indicator_view', name='get_followed_indicator_view'),
+ url(r'^followed/category/(?P<category_id>all)/$',
+ 'get_followed_indicator_view', name='get_followed_indicator_view'),
+ url(r'^followed/category/(?P<category_id>\d+)/$',
+ 'get_followed_indicator_view', name='get_followed_indicator_view'),
+ ## get_unfollowed_indicator_view
+ url(r'^unfollowed/(?P<startswith>all)/$',
+ 'get_unfollowed_indicator_view', name='get_unfollowed_indicator_view'),
+ url(r'^unfollowed/(?P<startswith>[a-zA-Z]+)/$',
+ 'get_unfollowed_indicator_view', name='get_unfollowed_indicator_view'),
+ url(r'^unfollowed/category/(?P<category_id>all)/$',
+ 'get_unfollowed_indicator_view', name='get_unfollowed_indicator_view'),
+ url(r'^unfollowed/category/(?P<category_id>\d+)/$',
+ 'get_unfollowed_indicator_view', name='get_unfollowed_indicator_view'),
+ ## get_record view
+ url(r'^record/(?P<indicator_id>\d+)/(?P<date_range>\d{8}-\d{8})/$',
+ 'get_record_view', name='get_record_view'),
+ url(r'^record/(?P<indicator_id>\d+)/(?P<date_range>\d{8}-\d{8})/std/$',
+ 'get_record_view', { 'std': True }),
+ ## recommend indicator
+ url(r'^recommend/indicator/(?P<number>\d+)/$',
+ 'recommend_indicator_view', name='recommend_indicator'),
+ ## add/edit category
+ url(r'^add/category/$', 'add_edit_category',
+ name='add_category'),
+ url(r'^edit/category/(?P<category_id>\d+)/$', 'add_edit_category',
+ name='edit_category'),
+ ## add/edit indicator
+ url(r'^add/indicator/$', 'add_edit_indicator',
+ name='add_indicator'),
+ url(r'^edit/indicator/(?P<indicator_id>\d+)/$', 'add_edit_indicator',
+ name='edit_indicator'),
+ ## add/edit unit
+ url(r'^add/unit/$', 'add_edit_unit',
+ name='add_unit'),
+ url(r'^edit/unit/(?P<unit_id>\d+)/$', 'add_edit_unit',
+ name='edit_unit'),
+ ## add/edit innateconfine
+ url(r'^add/confine/$', 'add_edit_confine',
+ name='add_confine'),
+ url(r'^edit/confine/(?P<confine_id>\d+)/$', 'add_edit_confine',
+ name='edit_confine'),
+ ## add/edit record
+ url(r'^add/record/$', 'add_edit_record',
+ name='add_record'),
+ url(r'^edit/record/(?P<record_id>\d+)/$', 'add_edit_record',
+ name='edit_record'),
+ ## add record history (modify history NOT allowed)
+ url(r'^add/recordhistory/$', 'add_recordhistory',
+ name='add_recordhistory'),
+ url(r'^add/recordhistory/(?P<record_id>\d+)/$', 'add_recordhistory',
+ name='add_recordhistory'),
+)
+
+
+urlpatterns += patterns('',
+ ## done
+ url(r'^done/$', direct_to_template, { 'template': 'done.html' }),
+)
+
diff --git a/97suifangqa/apps/indicator/views.py b/97suifangqa/apps/indicator/views.py
new file mode 100644
index 0000000..1175d62
--- /dev/null
+++ b/97suifangqa/apps/indicator/views.py
@@ -0,0 +1,416 @@
+# -*- coding: utf-8 -*-
+
+"""
+apps/indicator views
+
+"""
+
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden
+from django.shortcuts import render_to_response, get_object_or_404
+# CRSF
+from django.template import RequestContext
+
+from indicator import models as im
+from indicator.forms import *
+from indicator.tools import *
+
+import re
+import datetime
+
+
+def get_indicator_view(request, **kwargs):
+ idict = get_indicator(**kwargs)
+ return HttpResponse("%s" % idict)
+
+
+@login_required
+def get_followed_indicator_view(request, **kwargs):
+ idict = get_followed_indicator(request.user.id, **kwargs)
+ return HttpResponse("%s" % idict)
+
+
+@login_required
+def get_unfollowed_indicator_view(request, **kwargs):
+ idict = get_unfollowed_indicator(request.user.id, **kwargs)
+ return HttpResponse("%s" % idict)
+
+
+@login_required
+def recommend_indicator_view(request, **kwargs):
+ ilist = recommend_indicator(request.user.id, **kwargs)
+ return HttpResponse("%s" % ilist)
+
+
+# follow_indicator {{{
+@login_required
+def follow_indicator(request, indicator_id):
+ """
+ 用户关注指标
+ """
+ try:
+ indicator = im.Indicator.objects.get(pk=int(indicator_id))
+ ui, created = im.UserIndicator.objects.get_or_create(
+ user=request.user)
+ ui.followedIndicators.add(indicator)
+ return { 'success': True }
+ except:
+ return { 'success': False }
+# }}}
+
+
+# unfollow_indicator {{{
+@login_required
+def unfollow_indicator(request, indicator_id):
+ """
+ 用户取消关注指标
+ """
+ try:
+ indicator = im.Indicator.objects.get(pk=int(indicator_id))
+ ui, created = im.UserIndicator.objects.get_or_create(
+ user=request.user)
+ ui.followedIndicators.remove(indicator)
+ return { 'success': True }
+ except:
+ return { 'success': False }
+# }}}
+
+
+# get_record_view {{{
+@login_required
+def get_record_view(request, indicator_id, date_range, **kwargs):
+ """
+ get IndicatorRecord record
+ """
+ indicator_id = int(indicator_id)
+ # regex to match given 'date_range' (yyyymmdd-yyyymmdd)
+ p = re.compile(r'^(?P<b_y>\d{4})(?P<b_m>\d{2})(?P<b_d>\d{2})-(?P<e_y>\d{4})(?P<e_m>\d{2})(?P<e_d>\d{2})$')
+ m = p.match(date_range)
+ # begin date
+ begin_y = int(m.group('b_y'))
+ # remove '^0'; avoid the '0???' octal number
+ begin_m = int(re.sub(r'^0', '', m.group('b_m')))
+ begin_d = int(re.sub(r'^0', '', m.group('b_d')))
+ # end date
+ end_y = int(m.group('b_y'))
+ end_m = int(re.sub(r'^0', '', m.group('e_m')))
+ end_d = int(re.sub(r'^0', '', m.group('e_d')))
+ # date
+ begin = datetime.date(begin_y, begin_m, begin_d)
+ end = datetime.date(end_y, end_m, end_d)
+ data = get_record(request.user.id, indicator_id=indicator_id,
+ begin=begin, end=end, **kwargs)
+ return HttpResponse("%s" % data)
+# }}}
+
+
+
+###########################################################
+###### forms ######
+
+## add_edit_category # {{{
+@login_required
+def add_edit_category(request, category_id=None, template='simple.html'):
+ """
+ add/edit category: 'models.IndicatorCategory'
+ for 'staff' or 'normal user'
+ """
+ # get or create model instance
+ if category_id:
+ category_id = int(category_id)
+ category = get_object_or_404(im.IndicatorCategory,
+ id=category_id)
+ action = 'Edit'
+ # check the user
+ # 'staff' can edit all data;
+ # normal users can only edit their own.
+ if category.addByUser != request.user and (
+ not request.user.is_staff):
+ return HttpResponseForbidden()
+ else:
+ category = im.IndicatorCategory(addByUser=request.user)
+ action = 'Add'
+
+ if request.method == 'POST':
+ form = IndicatorCategoryForm(request.POST, instance=category)
+ if form.is_valid():
+ # form posted and valid
+ # save form to create/update the model instance
+ form.save()
+ # redirect url, avoid page reload/refresh
+ return HttpResponseRedirect('/indicator/done/')
+ else:
+ # form with data of the specified instance
+ form = IndicatorCategoryForm(instance=category)
+
+ return render_to_response(template, {
+ 'object': 'IndicatorCategory',
+ 'action': action,
+ 'form': form,
+ }, context_instance=RequestContext(request))
+# }}}
+
+
+# add_edit_indicator # {{{
+@login_required
+def add_edit_indicator(request, indicator_id=None, template='simple.html'):
+ """
+ add/edit indicator: 'models.Indicator'
+ for 'staff' or 'normal user'
+ """
+ if indicator_id:
+ indicator_id = int(indicator_id)
+ indicator = get_object_or_404(im.Indicator,
+ id=indicator_id)
+ action = 'Edit'
+ # check the user
+ # 'staff' can edit all data;
+ # normal users can only edit their own.
+ if indicator.addByUser != request.user and (
+ not request.user.is_staff):
+ return HttpResponseForbidden()
+ else:
+ indicator = im.Indicator(addByUser=request.user)
+ action = 'Add'
+
+ if request.method == 'POST':
+ form = IndicatorForm(request.POST, instance=indicator)
+ if form.is_valid():
+ # form posted and valid
+ form.save()
+ # redirect url, avoid page reload/refresh
+ return HttpResponseRedirect('/indicator/done/')
+ else:
+ # form with instance
+ form = IndicatorForm(instance=indicator)
+
+ return render_to_response(template, {
+ 'object': 'Indicator',
+ 'action': action,
+ 'form': form,
+ }, context_instance=RequestContext(request))
+# }}}
+
+
+## add_edit_unit {{{
+@login_required
+def add_edit_unit(request, unit_id=None, template='simple.html'):
+ """
+ add unit for indicator
+ """
+ if unit_id:
+ unit_id = int(unit_id)
+ unit = get_object_or_404(im.Unit, id=unit_id)
+ action = 'Edit'
+ # check the user
+ # 'staff' can edit all data;
+ # normal users can only edit their own.
+ if unit.addByUser != request.user and (
+ not request.user.is_staff):
+ return HttpResponseForbidden()
+ else:
+ unit = im.Unit(addByUser=request.user)
+ action = 'Add'
+
+ if request.method == 'POST':
+ form = UnitForm(request.POST, instance=unit)
+ if form.is_valid():
+ # form posted and valid
+ form.save()
+ # redirect url, avoid page reload/refresh
+ return HttpResponseRedirect('/indicator/done/')
+ else:
+ # form with instance
+ form = UnitForm(instance=unit)
+
+ return render_to_response(template, {
+ 'object': 'Unit',
+ 'action': action,
+ 'form': form,
+ }, context_instance=RequestContext(request))
+# }}}
+
+
+## add_edit_confine {{{
+@login_required
+def add_edit_confine(request, confine_id=None, template='simple.html'):
+ """
+ InnateConfine
+ add confines for indicator
+ """
+ if confine_id:
+ confine_id = int(confine_id)
+ confine = get_object_or_404(im.InnateConfine, id=confine_id)
+ action = 'Edit'
+ # check the user
+ # 'staff' can edit all data;
+ # normal users can only edit their own.
+ if confine.addByUser != request.user and (
+ not request.user.is_staff):
+ return HttpResponseForbidden()
+ else:
+ confine = im.InnateConfine(addByUser=request.user)
+ action = 'Add'
+
+ if request.method == 'POST':
+ form = InnateConfineForm(request.POST, instance=confine)
+ if form.is_valid():
+ # form posted and valid
+ form.save()
+ # redirect url, avoid page reload/refresh
+ return HttpResponseRedirect('/indicator/done/')
+ else:
+ # form with instance
+ form = InnateConfineForm(instance=confine)
+
+ return render_to_response(template, {
+ 'object': 'InnateConfine',
+ 'action': action,
+ 'form': form,
+ }, context_instance=RequestContext(request))
+# }}}
+
+
+## add_edit_record {{{
+@login_required
+def add_edit_record(request, record_id=None, template='simple.html'):
+ """
+ add/edit 'IndicatorRecord'
+
+ staff 能自由地修改所有的记录,并且无需填写"修改原因";
+ 普通用户只能修改自己的记录,而且必须填写"修改原因" -> RecordHistory
+
+ TODO:
+ * 当用户选择好"indicator"后,重新筛选"unit",只提供与"indicator"
+ 对应的"unit"供选择;
+ * 对"普通用户"增加限制,修改数据"记录"时必须同时提交"修改原因",
+ 对应模型"RecordHistory"。
+ """
+ if record_id:
+ record_id = int(record_id)
+ record = get_object_or_404(im.IndicatorRecord, id=record_id)
+ action = 'Edit'
+ # check the user
+ if request.user.is_staff:
+ # 'staff' can edit all data;
+ pass
+ elif request.user == record.user:
+ # user modify the record
+ return HttpResponse("Not finished yet ...")
+ #return modify_record(request, record_id)
+ else:
+ return HttpResponseForbidden()
+ else:
+ record = im.IndicatorRecord(user=request.user)
+ action = 'Add'
+
+ if request.method == 'POST':
+ form = IndicatorRecordForm(request.POST, instance=record)
+ if form.is_valid():
+ #raise ValueError
+ form.save()
+ # redirect url, avoid page reload/refresh
+ return HttpResponseRedirect('/indicator/done/')
+ else:
+ # form with instance
+ form = IndicatorRecordForm(instance=record)
+
+ return render_to_response(template, {
+ 'object': 'IndicatorRecord',
+ 'action': action,
+ 'form': form,
+ }, context_instance=RequestContext(request))
+# }}}
+
+
+## modify_record {{{
+@login_required
+def modify_record(request, record_id=None, template='simple.html'):
+ """
+ modify an existing IndicatorRecord
+
+ TODO:
+ a new 'RecordHistory' is added to record the modification reason
+ and backup the original data
+ """
+ if record_id:
+ record_id = int(record_id)
+ record = get_object_or_404(im.IndicatorRecord, id=record_id)
+ action = 'Edit'
+ # check the user
+ if request.user.is_staff:
+ # 'staff' can edit all data;
+ return add_edit_record(request, record_id)
+ elif request.user == record.user:
+ # user modify the record
+ action = 'Modify'
+ pass
+ else:
+ return HttpResponseForbidden()
+ else:
+ return add_edit_record(request)
+
+ if request.method == 'POST':
+ form = IndicatorRecordForm(request.POST, instance=record)
+ if form.is_valid():
+ # form posted and valid
+ # TODO
+ raise ValueError(u"该功能尚未完整实现")
+ form.save()
+ # redirect url, avoid page reload/refresh
+ return HttpResponseRedirect('/indicator/done/')
+ else:
+ # form with instance
+ form = IndicatorRecordForm(instance=record)
+
+ return render_to_response(template, {
+ 'object': 'IndicatorRecord',
+ 'action': action,
+ 'form': form,
+ }, context_instance=RequestContext(request))
+## }}}
+
+
+## add_recordhistory {{{
+@login_required
+def add_recordhistory(request, record_id, template='simple.html'):
+ """
+ add 'RecordHistory' for a record by given
+
+ 'staff' should use the 'admin' interface.
+ """
+ record_id = int(record_id)
+ record = get_object_or_404(im.IndicatorRecord, id=record_id)
+ recordhistory = im.RecordHistory(indicatorRecord=record)
+ action = 'Add'
+ # check the user
+ if request.user != record.user:
+ return HttpResponseForbidden()
+
+ if request.method == 'POST':
+ form = RecordHistoryForm(request.POST, instance=recordhistory)
+ if form.is_valid():
+ # form posted and valid
+ form.save()
+ # redirect url, avoid page reload/refresh
+ return HttpResponseRedirect('/indicator/done/')
+ else:
+ # form with instance
+ form = RecordHistoryForm(instance=recordhistory)
+
+ return render_to_response(template, {
+ 'object': 'RecordHistory',
+ 'action': action,
+ 'form': form,
+ }, context_instance=RequestContext(request))
+# }}}
+
+
+###########################################################
+
+### test_view ###
+def test_view(request, **kwargs):
+ html = '<html><body>%s</body></html>' % u"正文测试内容"
+ text = u"中文测试"
+ return HttpResponse("%s" % request)
+