def testLastTaskId(self): # Using "_" with no prior task activity should raise an exception self.assertRaises(YokadiException, self.cmd.getTaskFromId, "_") tui.addInputAnswers("y") self.cmd.do_t_add("x t1") task1 = Task.get(1) self.assertEqual(self.cmd.getTaskFromId("_"), task1) self.cmd.do_t_add("x t2") task2 = Task.get(2) self.assertEqual(self.cmd.getTaskFromId("_"), task2) self.cmd.do_t_mark_started("1") self.assertEqual(self.cmd.getTaskFromId("_"), task1)
def testRecurs(self): tui.addInputAnswers("y") self.cmd.do_t_add("x t1") task = Task.get(1) self.cmd.do_t_recurs("1 daily 10:00") desc = str(task.recurrence) self.cmd.do_t_recurs("1 weekly FR 23:00") self.cmd.do_t_recurs("1 none") self.cmd.do_t_recurs("1 weekly fr 23:00") self.cmd.do_t_recurs("1 weekly Fr 23:00") self.cmd.do_t_recurs("1 weekly Friday 23:00") self.cmd.do_t_recurs("1 monthly 3 13:00") self.cmd.do_t_recurs("1 monthly second friday 13:00") self.cmd.do_t_recurs("1 yearly 3/07 11:20") self.cmd.do_t_recurs("1 quarterly 14 11:20") self.cmd.do_t_recurs("1 quarterly first monday 23:20") self.assertNotEqual(desc, str(task.recurrence)) self.assertEqual(task.status, "new") self.cmd.do_t_mark_done("1") self.assertEqual(task.status, "new") for bad_input in ("", # No task "1", # No recurence "1 foo", # Unknown recurrence "1 daily", # No time "1 weekly", # No day "1 weekly monday", # No time "1 monthly", # No day "1 monthly 10", # No time "1 quarterly", # No day "1 quarterly 10", # No time "1 monthly foo 12:00", # Bad date ): self.assertRaises(YokadiException, self.cmd.do_t_recurs, bad_input)
def handle_file(code, task, file): task_obj = Task.get(name=task) data = get_data_for_code(code) if task_obj is None: return {"error": "Report type not found: " + str(task)}, 404 committee = data["group"]["codeName"] year = str(data["year"]) lp = str(data["study_period"]) name = task + "_" + committee + "_" + year + "_" + lp + ".pdf" path = "./uploads/" + year + "/lp" + lp + "/" + str( data["meeting_no"]) + "/" + str(committee) if not os.path.exists(path): os.makedirs(path) save_loc = path + "/" + name print("Saving file " + str(file) + " in " + path) file.save(save_loc) code_file = CodeFile.get(code=code, task=task) if code_file is None: CodeFile(code=code, task=task, file_location=save_loc) return {"overwrite": False} else: print("OVERWRITE!") code_file.date = datetime.datetime.now() return {"overwrite": True}
def do_t_reorder(self, line): """Reorder tasks of a project. It works by starting an editor with the task list: you can then change the order of the lines and save the list. The urgency field will be updated to match the order. t_reorder <project_name>""" project = Project.byName(projectName) taskList = Task.select(AND(Task.q.projectID == project.id, Task.q.status != 'done'), orderBy=-Task.q.urgency) lines = [ "%d,%s" % (x.id, x.title) for x in taskList] text = tui.editText("\n".join(lines)) ids = [] for line in text.split("\n"): line = line.strip() if not "," in line: continue id = int(line.split(",")[0]) ids.append(id) ids.reverse() for urgency, id in enumerate(ids): task = Task.get(id) task.urgency = urgency
def testMark(self): tui.addInputAnswers("y") self.cmd.do_t_add("x t1") task = Task.get(1) self.assertEqual(task.status, "new") self.cmd.do_t_mark_started("1") self.assertEqual(task.status, "started") self.cmd.do_t_mark_done("1") self.assertEqual(task.status, "done")
def testAddKeywords(self): tui.addInputAnswers("y") self.cmd.do_t_add("x t1") task = Task.get(1) tui.addInputAnswers("y", "y") self.cmd.do_t_add_keywords("1 @kw1 @kw2=12") kwDict = task.getKeywordDict() self.assertEqual(kwDict, dict(kw1=None, kw2=12))
def testAdd(self): tui.addInputAnswers("y", "2", "4", "123") self.cmd.do_bug_add("x t1") tasks = list(Task.select()) result = [x.title for x in tasks] expected = [u"t1"] self.assertEqual(result, expected) kwDict = Task.get(1).getKeywordDict() self.assertEqual(kwDict, dict(_severity=2, _likelihood=4, _bug=123))
def testAdd(self): tui.addInputAnswers("y") self.cmd.do_t_add("x t1") tui.addInputAnswers("y", "y") self.cmd.do_t_add("x @kw1 @kw2=12 t2") tasks = list(Task.select()) result = [x.title for x in tasks] expected = [u"t1", u"t2"] self.assertEqual(result, expected) kwDict = Task.get(2).getKeywordDict() self.assertEqual(kwDict, dict(kw1=None, kw2=12))
def testAddKeywords(self): tui.addInputAnswers("y") self.cmd.do_t_add("x t1") task = Task.get(1) tui.addInputAnswers("y", "y") self.cmd.do_t_add_keywords("1 @kw1 @kw2=12") kwDict = task.getKeywordDict() self.assertEqual(kwDict, dict(kw1=None, kw2=12)) for bad_input in ("", # No task "1", # No keyword "1 kw1"): # No @ before kw1 self.assertRaises(YokadiException, self.cmd.do_t_add_keywords, bad_input)
def get_or_create_task_for_user(user, date=None, ip=None): if not date: date = today() try: task_obj = Task.get(Task.user == user, Task.day == date) if task_obj.task not in get_tasks(user.level): task_obj.task = random_task_for_user(user) task_obj.save() except Task.DoesNotExist: if ip: task_name = random_task_for_ip(ip) else: task_name = random_task_for_user(user) task_obj = Task(user=user, day=date, task=task_name) task_obj.save() return task_obj
def changeset(): if 'osm_token' not in session: redirect(url_for('login')) cs_data = request.args.get('changeset') if not cs_data.strip(): return redirect(url_for('front')) user = get_user() # TODO: call submit_changeset instead try: changeset = parse_changeset_id(cs_data) cs_date, conforms = validate_changeset(user, changeset, None, openstreetmap) except ValueError as e: flash(str(e)) return redirect(url_for('front')) if not cs_date or cs_date != today(): flash('Date of the changeset is wrong') return redirect(url_for('front')) task = Task.get(Task.user == user, Task.day == cs_date) try: last_task = Task.select(Task.day).where( Task.user == user, Task.changeset.is_null(False)).order_by(Task.day.desc()).get() is_streak = last_task.day == cs_date - cs_date.resolution except Task.DoesNotExist: is_streak = False task.changeset = changeset task.correct = conforms if is_streak: user.streak += 1 else: user.streak = 1 user.score += int(math.log(user.streak + 1, 2)) if conforms: flash('An extra point for completing the task') user.score += 1 if user.level < len(config.LEVELS) + 1: if user.score >= config.LEVELS[user.level - 1]: user.level += 1 flash('Congratulations on gaining a level!') with database.atomic(): task.save() user.save() flash('Changeset noted, thank you!') return redirect(url_for('front'))
def testRecurs(self): tui.addInputAnswers("y") self.cmd.do_t_add("x t1") task = Task.get(1) self.cmd.do_t_recurs("1 daily 10:00") desc = str(task.recurrence) self.cmd.do_t_recurs("1 weekly FR 23:00") self.cmd.do_t_recurs("1 weekly fr 23:00") self.cmd.do_t_recurs("1 weekly Fr 23:00") self.cmd.do_t_recurs("1 weekly Friday 23:00") self.cmd.do_t_recurs("1 monthly 3 13:00") self.cmd.do_t_recurs("1 monthly second friday 13:00") self.cmd.do_t_recurs("1 yearly 3/07 11:20") self.cmd.do_t_recurs("1 quarterly 14 11:20") self.cmd.do_t_recurs("1 quarterly first monday 23:20") self.assertNotEqual(desc, str(task.recurrence)) self.assertEqual(task.status, "new") self.cmd.do_t_mark_done("1") self.assertEqual(task.status, "new")
def testAdd(self): tui.addInputAnswers("y", "2", "4", "123") self.cmd.do_bug_add("x t1") tui.addInputAnswers("n") self.cmd.do_bug_add("notExistingProject newBug") tasks = list(Task.select()) result = [x.title for x in tasks] expected = [u"t1"] self.assertEqual(result, expected) kwDict = Task.get(1).getKeywordDict() self.assertEqual(kwDict, dict(_severity=2, _likelihood=4, _bug=123)) for bad_input in ("", # No project "x", # No task name "x t1"): # Existing task self.assertRaises(YokadiException, self.cmd.do_bug_add, bad_input)
def getTaskFromId(line, parameterName="id"): """Returns a task given its id, or raise a YokadiException if it does not exist. @param line: taskId string @param parameterName: name of the parameter to mention in exception @return: Task instance or None if existingTask is False""" line = line.strip() if len(line) == 0: raise YokadiException("Missing <%s> parameter" % parameterName) # We do not use line.isdigit() because it returns True if line is '¹'! try: taskId = int(line) except ValueError: raise YokadiException("<%s> should be a number" % parameterName) try: return Task.get(taskId) except SQLObjectNotFound: raise YokadiException("Task %s does not exist. Use t_list to see all tasks" % taskId)
def submit_changeset(user, changeset, req=None): """Validates the changeset, records it and returns a series of messages.""" lang = load_language_from_user('', user)['validation'] try: changeset = parse_changeset_id(changeset) cs_date, conforms = validate_changeset(user, changeset, None, req) if not cs_date: raise ValidationError('wrong_date') last_task_day = get_last_task_day(user) if last_task_day and last_task_day >= cs_date: raise ValidationError('has_later_changeset') if cs_date < yesterday(): raise ValidationError('old_changeset') except ValidationError as e: return [e.to_lang(lang)], False task = Task.get(Task.user == user, Task.day == cs_date) task.changeset = changeset task.correct = conforms if last_task_day == cs_date - cs_date.resolution: user.streak += 1 else: user.streak = 1 user.score += int(math.log(user.streak + 1, 2)) msgs = [lang['changeset_noted'].format(user.streak)] if conforms: user.score += 1 msgs.append(lang['extra_point']) if user.level < len(config.LEVELS) + 1: if user.score >= config.LEVELS[user.level - 1]: user.level += 1 msgs.append(lang['gain_level']) with database.atomic(): task.save() user.save() return msgs, True
def testAdd(self): tui.addInputAnswers("y") self.cmd.do_t_add("x t1") tui.addInputAnswers("y", "y") self.cmd.do_t_add("x @kw1 @kw2=12 t2") tui.addInputAnswers("n") self.cmd.do_t_add("notExistingProject newTask") tasks = list(Task.select()) result = [x.title for x in tasks] expected = [u"t1", u"t2"] self.assertEqual(result, expected) kwDict = Task.get(2).getKeywordDict() self.assertEqual(kwDict, dict(kw1=None, kw2=12)) for bad_input in ("", # No project "x", # No task name "x t1"): # Existing task self.assertRaises(YokadiException, self.cmd.do_t_add, bad_input)
def get_task_by_name(task_name: str) -> Optional[Task]: return Task.get(name=task_name)
def validate_changeset(user, changeset, task_name=None, req=None): if not req: req = RequestsWrapper() resp = req.get('changeset/{}'.format(changeset)) if resp.status != 200: raise ValidationError('api_error') ch = resp.data[0] uid = int(ch.get('uid')) if uid != user.uid: raise ValidationError('not_yours') date_str = ch.get('created_at')[:10] date = datetime.datetime.strptime(date_str, '%Y-%m-%d').date() if date < yesterday(): raise ValidationError('old_changeset') try: if not task_name: task_obj = Task.get(Task.user == user, Task.day == date) task_name = task_obj.task except Task.DoesNotExist: get_or_create_task_for_user(user, date) return date, False task = load_task(task_name) if 'test' not in task: return date, True changes = None RE_NUMBER = re.compile(r'^\d+$') for t, tagtest in task['test'].items(): if t == 'changeset': if not validate_tags(ch, tagtest): return date, False else: if changes is None: resp = req.get('changeset/{}/download'.format(changeset)) if resp.status != 200: raise ValidationError('api_error') changes = resp.data if changes.tag != 'osmChange': raise ValidationError('api_strange') state_action = True actions = set() count = 1 obj_type = None for part in t.split('_'): if state_action: if part.startswith('create') or part.startswith('add'): actions.add('create') elif part.startswith('modif'): actions.add('modify') elif part.startswith('delete'): actions.add('delete') elif RE_NUMBER.match(part): count = int(part) state_action = False if actions: if part.startswith('node'): obj_type = 'node' elif part.startswith('way'): obj_type = 'way' elif part.startswith('rel'): obj_type = 'relation' elif part.startswith('area'): obj_type = 'area' elif part.startswith('obj'): obj_type = 'any' if obj_type: break if not obj_type: logging.error('Cannot parse a test header: %s', t) return date, True found_count = 0 for xaction in changes: if xaction.tag not in actions: continue for xobj in xaction: if obj_type in ('node', 'way', 'relation') and xobj.tag != obj_type: continue elif obj_type == 'area': if xobj.tag == 'way': if xobj.find('nd[1]').get('ref') != xobj.find( 'nd[last()]').get('ref'): continue elif xobj.tag == 'relation': xtype = xobj.find("tag[@k='type']") if xtype is None or xtype.get( 'v') != 'multipolygon': continue else: continue if validate_tags(xobj, tagtest): found_count += 1 if found_count < count: return date, False return date, True
osc = changes_to_osc(changes, changeset_id) resp = requests.post('{0}/api/0.6/changeset/{1}/upload'.format(API_ENDPOINT, changeset_id), osc, auth=oauth) if resp.status_code == 200: task.status = 'done' task.error = str(changeset_id) else: # We don't want to exit before closing the changeset task.status = 'error' task.error = 'Server rejected the changeset with code {0}: {1}'.format(resp.code, resp.text) task.save() finally: resp = requests.put('{0}/api/0.6/changeset/{1}/close'.format(API_ENDPOINT, changeset_id), auth=oauth) if __name__ == '__main__': if not lock(): sys.exit(0) database.connect() database.create_tables([Task], safe=True) try: task = Task.get(Task.pending) except Task.DoesNotExist: # Yay, no jobs for us. sys.exit(0) try: process(task) except Exception as e: update_status_exit_on_error(task, 'system error', str(e))
def setup_general_config(): ''' Is used to populate the database if it has been reset. :return: ''' long_string = ConfigType.get(type="long_string") long_string = ConfigType( type="long_string") if long_string is None else long_string string = ConfigType.get(type="string") string = ConfigType(type="string") if string is None else string number = ConfigType.get(type="number") number = ConfigType(type="number") if number is None else number config_list = [{ "key": "frontend_url", "value": "localhost:3001", "config_type": string, "description": "The url to this frontend page (used for links in the emails)" }, { "key": "archive_base_url", "value": "localhost:3001/api/archive/download", "config_type": string, "description": "The base url to download archives from (used in emails to the board)" }, { "key": "document_template_url", "value": "https://www.overleaf.com/read/ddjdhxnkxttj", "config_type": string, "description": "The template overleaf document for the different reports (used in the emails)" }, { "key": "gotify_url", "value": "http://*****:*****@chalmers.it", "config_type": string, "description": "The email to the secretary" }, { "key": "board_email", "value": "*****@*****.**", "config_type": string, "description": "The email to the board" }, { "key": "group_email_domain", "value": "@chalmers.it", "config_type": string, "description": "The domain to send emails to" }, { "key": "from_email_address", "value": "*****@*****.**", "config_type": string, "description": "The email to send from" }, { "key": "mail_to_groups_subject", "value": "Dokument till sektionsmöte den {day}/{month}", "config_type": string, "description": """ The subject for "regular" email sendout (that goes out to all active groups that have documents to turn in for the meeting). \n Description of the formatting values: \n - {group_name} = The display name of the group \n - {meeting_day} = The day of month for the meeting \n - {meeting_month} = The month (number) of the meeting \n - {deadline_time} = The deadline time (hh:mm) \n - {deadline_date} = The deadline date (dd/mm) \n - {task_list} = A list of the tasks that the group should upload \n - {frontend_url} = The url to the website \n - {group_code} = Their unique code \n - {template_url} = The document (overleaf) template url \n - {secretary_email} = The email to the secretary \n - {board_display_name} = The display name of the board \n - {board_email} = The email to the board """ }, { "key": "mail_to_groups_message", "value": "\nHej {group_name}!\n\nDen {meeting_day}/{meeting_month} är det dags för sektionsmöte och senast {deadline_time} den {deadline_date} behöver ni lämna in " "följande dokument: {task_list}\nDetta görs på sidan: {frontend_url}\nAnge koden: {group_code}\n\nMall för vissa " "dokument finns här: {template_url}\nGör en kopia av projektet (Menu -> Copy Project) och fyll i.\n\nOm ni har " "några frågor eller stöter på några problem kan kan ni kontakta mig på {secretary_email} eller hela {board_display_name} på {board_email} " ": ).", "config_type": long_string, "description": """ The body of the "regular" emails (the ones that are sent to all the active groups that should turn in documents for the meeting). \n Description of the formatting values: \n - {group_name} = The display name of the group \n - {meeting_day} = The day of month for the meeting \n - {meeting_month} = The month (number) of the meeting \n - {deadline_time} = The deadline time (hh:mm) \n - {deadline_date} = The deadline date (dd/mm) \n - {task_list} = A list of the tasks that the group should upload \n - {frontend_url} = The url to the website \n - {group_code} = Their unique code \n - {template_url} = The document (overleaf) template url \n - {secretary_email} = The email to the secretary \n - {board_display_name} = The display name of the board \n - {board_email} = The email to the board """ }, { "key": "mail_to_board_subject", "value": "Dokument för sektionsmöte {meeting_number} lp {meeting_lp}", "config_type": string, "description": """ The subject of the email that is sent to the board upon reaching the deadline. \n Description of the formatting values: \n - {board_name} = The display name of the board \n - {meeting_number} = The number of the meeting (usually 0) \n - {meeting_lp} = The study period of the meeting \n - {meeting_archive_url} = A link to the archive download \n - {secretary_email} = The email to the secretary """ }, { "key": "mail_to_board_message", "value": "\nHej {board_name}!\n\nDeadlinen för dokumentinsamling till sektionsmöte {meeting_number} i lp {meeting_lp} är nu nådd.\nFör " "nedladdning av dessa dokument klicka på denna länk: {meeting_archive_url}\n\nVid frågor, kontakta sekreteraren på {secretary_email}", "config_type": long_string, "description": """ The contents of the email that is sent out to the board upon reaching the deadline. \n Description of the formatting values: \n - {board_name} = The display name of the board \n - {meeting_number} = The number of the meeting (usually 0) \n - {meeting_lp} = The study period of the meeting \n - {meeting_archive_url} = A link to the archive download \n - {secretary_email} = The email to the secretary """ }, { "key": "mail_for_stories_subject", "value": "Dokument för sektionsmöte {meeting_number} lp {meeting_lp}", "config_type": string, "description": """ The subject of the email that is sent to the "story groups" (i.e. the groups that needs to turn in eberattelser / vberattelser. \n Description of the formatting values: \n - {group_name_year} = Display name of the group. \n - {meeting_day} = The day of month that the meeting will take place \n - {meeting_month} = The month (number) of the meeting \n - {deadline_time} = The deadline time \n - {deadline_date} = The deadline date \n - {task_list} = A list of the tasks that the group will have to turn in. \n - {frontend_url} = A url to the frontend (upload page) \n - {group_code} = Their unique code \n - {template_url} = A link the overleaf template for the documents. \n - {secretary_email} = The email to the secretary \n - {board_display_name} = The display name of the board \n - {board_email} = The email to the board \n - {meeting_number} = The number of the meeting that study period (usually 0) \n - {meeting_lp} = The study period """ }, { "key": "mail_for_stories", "value": "\nHej {group_name_year}!\n\nDen {meeting_day}/{meeting_month} är det dags för sektionsmöte och senast {deadline_time} den {deadline_date} behöver ni lämna in " "följande dokument: {task_list}\nDetta görs på sidan: {frontend_url}\nAnge koden: {group_code}\n\nMall för vissa " "dokument finns här: {template_url}\nGör en kopia av projektet (Menu -> Copy Project) och fyll i.\n " "Kontakta revisorerna på [email protected] för mer information om vad som behövs göras innan ni " "kan bli rekomenderade att bli ansvarsbefriade.\n\nOm ni har " "några frågor eller stöter på några problem kan kan ni kontakta mig på {secretary_email} eller hela {board_display_name} på {board_email} " ": ).", "config_type": long_string, "description": """ The body of the email that is sent to the "story groups" (i.e. the groups that needs to turn in eberattelser / vberattelser) \n Description of the formatting values: \n - {group_name_year} = Display name of the group. \n - {meeting_day} = The day of month that the meeting will take place \n - {meeting_month} = The month (number) of the meeting \n - {deadline_time} = The deadline time \n - {deadline_date} = The deadline date \n - {task_list} = A list of the tasks that the group will have to turn in. \n - {frontend_url} = A url to the frontend (upload page) \n - {group_code} = Their unique code \n - {template_url} = A link the overleaf template for the documents. \n - {secretary_email} = The email to the secretary \n - {board_display_name} = The display name of the board \n - {board_email} = The email to the board \n - {meeting_number} = The number of the meeting that study period (usually 0) \n - {meeting_lp} = The study period """ }, { "key": "board_display_name", "value": "styrIT", "config_type": string, "description": "The display name of the board" }, { "key": "minutes_after_deadline_to_mail", "value": "5", "config_type": number, "description": "The amount of minutes to wait extra after the deadline before sending the email to the board" }, { "key": "check_for_deadline_frequency", "value": "5", "config_type": number, "description": "The frequence (in minutes) to check if any deadlines have been reached" }, { "key": "possible_years_back_for_stories", "value": "5", "config_type": number, "description": "The number of years back that one should be able to select story groups for (usually 5 due to tax reasons)" }] for config in config_list: conf = Config.get(key=config["key"]) if conf is None: Config(key=config["key"], value=config["value"], config_type=config["config_type"], description=config["description"]) else: # Since the only way to change the description is here, # we always want the db version to be up to date with this list on application restart. conf.description = config["description"] # Setup groups and tasks groups = [{ "codeName": "armit", "displayName": "ArmIT" }, { "codeName": "digit", "displayName": "digIT" }, { "codeName": "fanbarerit", "displayName": "FanbärerIT" }, { "codeName": "fritid", "displayName": "frITid" }, { "codeName": "mrcit", "displayName": "MRCIT" }, { "codeName": "nollkit", "displayName": "NollKIT" }, { "codeName": "prit", "displayName": "P.R.I.T." }, { "codeName": "sexit", "displayName": "sexIT" }, { "codeName": "snit", "displayName": "snIT" }, { "codeName": "styrit", "displayName": "styrIT" }, { "codeName": "equalit", "displayName": "EqualIT" }, { "codeName": "flashit", "displayName": "FlashIT" }] tasks = [{ "codeName": "vplan", "displayName": "Verksamhetsplan / Operational plan" }, { "codeName": "budget", "displayName": "Budget" }, { "codeName": "vrapport", "displayName": "Verksamhetsrapport / Operational report" }, { "codeName": "vberattelse", "displayName": "Verksamhetsberättelse / Operational story" }, { "codeName": "eberattelse", "displayName": "Ekonomisk Berättelse / Economic story" }] for group in groups: if Group.get(name=group["codeName"]) is None: new_group = Group(name=group["codeName"], display_name=group["displayName"]) if GroupYear.get(group=new_group, year="active") is None: GroupYear(group=new_group, year="active", finished=False) for task in tasks: if Task.get(name=task["codeName"]) is None: Task(name=task["codeName"], display_name=task["displayName"]) commit() print("Finished loading database data from general config file.")