diff options
author | Alvin Li <liweitianux@gmail.com> | 2013-08-13 14:13:24 +0800 |
---|---|---|
committer | Alvin Li <liweitianux@gmail.com> | 2013-08-13 14:13:24 +0800 |
commit | 9636d4a6767f49384d5c386bc3f1142c88b90613 (patch) | |
tree | 3a70f6d9e4be1791d36c87cc7cbfd1d5aa2b39dd /97suifangqa/apps/indicator | |
parent | 9383d9a8a5988d071766c3d08a5c946e9c5b02ae (diff) | |
download | 97dev-9636d4a6767f49384d5c386bc3f1142c88b90613.tar.bz2 |
cloned from 'bitbucket', 2013/08/13
Diffstat (limited to '97suifangqa/apps/indicator')
-rw-r--r-- | 97suifangqa/apps/indicator/__init__.py | 0 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/fixtures/initial_data.json | 307 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/forms.py | 321 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/models.py | 1127 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/search_indexes.py | 53 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/templates/done.html | 9 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/templates/show_category.html | 32 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/templates/show_indicator.html | 48 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/templates/show_record.html | 52 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/templates/simple.html | 23 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/tools.py | 273 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/urls.py | 124 | ||||
-rw-r--r-- | 97suifangqa/apps/indicator/views.py | 416 |
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) + |