def post(self): per_page = app.config['SEARCH_RESULTS_PER_PAGE'] args = search_parser.parse_args() args = dict([(key, value) for (key, value) in args.items() if key is not None]) if 'page' not in args: abort(400, message="must specify page") if not any(key in args for key in ['student_id', 'team_id', 'project_id']): abort(400, message="must have at least one query field") students = Student.objects( db.Q(team_id=args.get('team_id')) | db.Q(guc_id=args.get('guc_id'))) if 'project_id' in args: if len(students) > 0: subs = (Submission.objects( submitter__in=students, project=Project.objects(id=args['project_id'])).order_by( '-created_at').paginate(args['page'], per_page)) else: subs = (Submission.objects(project=Project.objects( id=args['project_id'])).order_by('-created_at').paginate( args['page'], per_page)) else: subs = Submission.objects( submitter__in=students).order_by('-created_at').paginate( args['page'], per_page) return mongo_paginate_to_dict(subs, 'submissions')
def test_submission_create(self): sub1 = Submission().create( **{ "title": "title", "email": "email", "card_id": "card_id", "card_url": "card_url" }) assert Submission().get_by_id(sub1.id)
def test_submission_card_url_not_nullable(self): with pytest.raises(IntegrityError): sub1 = Submission().create(**{ "title": "title", "email": "email", "card_id": "card_id" })
def seed_db(): # seed the database with fake data with app.app_context(): # create users u1 = User(name="Jami", email="*****@*****.**", password="******") u2 = User(name="Samyo", email="*****@*****.**", password="******") save_all([u1, u2]) # follow u1.follow(u2) u2.follow(u1) # create and tag challenges c1 = Challenge(question="Who invented Python?", author=u1) c1.tags.append(u2) c1.save() # challenge submissions s = Submission(user=u2, challenge=c1, answer="Guido Van Rossum") save_all([u1, u2, c1, s]) db.session.commit() print("Done")
def get(self, id): """ Lists all grades related to the user. """ user = User.objects.get_or_404(id=id) return [ sub.to_dict() for sub in Submission.objects( submitter=user).order_by('-created_at') ]
def extract_card_email(_id): """Forcibly extract the Trello card email address. The Trello card email address allows submitters to directly reply to comment notifications. Trello does NOT provide this information via the API. See: https://stackoverflow.com/questions/42247377/trello-api-e-mail-address-of-my-card-returns-null # noqa: E501 So we have to do good 'ole browser emulation Args: _id (int): the submission id """ session = requests.Session() # Load the login page to capture the dsc cookie session.get("https://trello.com/login") # Authenticate using email and password auth_response = session.post( "https://trello.com/1/authentication", data={ "factors[user]": current_app.config["TRELLO_USERNAME"], "factors[password]": current_app.config["TRELLO_PASSWORD"], "method": "password", }, ) # Perform authorization step session.post( "https://trello.com/1/authorization/session", data={ "authentication": auth_response.json()["code"], "dsc": session.cookies["dsc"] }, ) # Fetch the card JSON submission = Submission().get_by_id(_id) response = session.get(submission.card_url + ".json") # Extract the special email and save it to our database record submission.card_email = response.json()["email"] Submission().save(submission)
def test_submission_status_default(self): sub1 = Submission().create( **{ "title": "title", "email": "email", "card_id": "card_id", "card_url": "card_url" }) assert sub1.status == Status.NEW.value
def add_submission(challenge_id): data = request.get_json() challenge = Challenge.query.get(challenge_id) if challenge is None: return NotFound("Challenge not found") current_time = datetime.utcnow() if current_time >= challenge.expires_on: return {"message": "Challenge expired. Cannot accept answers now"}, 400 user = User.query.get(get_jwt_identity()) submission = Submission(challenge=challenge, user=user, answer=data["answer"]) submission.save() return submission.serialize(), 201
def submit(): """Submission form page. Renders the submission application form and handles form POSTs as well. Returns: render_template: if page is being called by GET, or form had errors redirect: when form is successfully ingested (this redirects and clears form) Todo: * add Title to Submission object (waiting on update to model) * need simple notification for successful form POST, currently no feedback """ form = SubmissionForm() if form.validate_on_submit(): client = SubmissionsTrelloClient() labels = client.labels card = client.new_submissions_list.add_card( name=form.data["title"], desc=f"""#DESCRIPTION {form.data['description']} #PITCH {form.data['pitch']}""", labels=[ labels[form.data["format"]], labels[form.data["audience"]] ], position="bottom", assign=[current_app.config["TRELLO_ASSIGNEE"]], ) submission = Submission().create( title=form.data["title"], email=form.data["email"], card_id=card.id, card_url=card.url, description=form.data["description"], pitch=form.data["pitch"], notes=form.data["notes"], ) # message Celery to create the webhook create_hook.apply_async(args=[submission.id, submission.card_id]) # message Celery to fetch the card email address extract_card_email.apply_async(args=[submission.id]) # reset form by redirecting back and apply url params return redirect( url_for("bp.submit", success=1, id=card.id, url=card.url)) return render_template("submit.html", form=form)
def get(self, course_name, name, page=1): try: course = Course.objects.get(name=course_name) except: abort(404, message="Course not found") try: project = Project.objects.get(name=name, course=course) except: abort(404, message="Project not found") per_page = api.app.config['SUMBISSIONS_PAGE_SIZE'] if isinstance(g.user, Student) and project.published: # Filter all submissions subs = Submission.objects(submitter=g.user, project=project).order_by('-created_at').paginate(page, per_page) return mongo_paginate_to_dict(subs, 'submissions') elif g.user in course.teachers: # No need to filter subs = Submission.objects(project=project).order_by('-created_at').paginate(page, per_page) return mongo_paginate_to_dict(subs, 'submissions') else: abort(403, message="You are not a student or course teacher.") # not a student and not a course teacher
def create_hook(_id, card): """Create Webhook for Trello Card. When a card is created after a submission, we need to fix it with a webhook back to our app. Args: _id (int): the submission id card(str): the trello id for the card Todo: * No Exception handling! * should this handle submission not found? """ client = SubmissionsTrelloClient() submission = Submission().get_by_id(_id) webhook = client.create_hook(current_app.config["TRELLO_HOOK"], card) Submission().update(submission, hook=webhook.id) # send confirmation email send_email.apply_async( args=[submission.id, Status(submission.status).name.lower()])
def junit_task(submission_id): submission = junit_actual(submission_id) if submission is not None: for sub in [ s for s in Submission.objects(submitter=submission.submitter, project=submission.project, processed=True).order_by( '-created_at')[10:] ]: app.logger.info( 'Deleting submission id {0}, submitter {1}, project {2}'. format(sub.id, sub.submitter.name, sub.project.name)) sub.delete() app.logger.info('Submission deleted')
def handle_submission_state_changed(data, action): send = False lists = {} # Create a lookup table by symbolic list name for local_list in TrelloList().all(): lists[local_list.list_symbolic_name] = local_list.list_id submission = Submission().first(card_id=data["model"]["id"]) if submission: # if card moved from NEW to IN-REVIEW if (action["data"]["listAfter"]["id"] == lists["REVIEW"] and action["data"]["listBefore"]["id"] == lists["NEW"] and submission.status == Status.NEW.value): Submission().update(submission, status=Status.INREVIEW.value) current_app.logger.info( f"Submission {submission.id} is now IN-REVIEW") send = True # if card moved from IN-REVIEW to SCHEDULED elif (action["data"]["listAfter"]["id"] == lists["SCHEDULED"] and action["data"]["listBefore"]["id"] == lists["REVIEW"] and submission.status == Status.INREVIEW.value): Submission().update(submission, status=Status.SCHEDULED.value) current_app.logger.info( f"Submission {submission.id} is now SCHEDULED") send = True # if the card has been updated, send an email if send: send_email.apply_async( args=[submission.id, Status(submission.status).name.lower()]) else: current_app.logger.error("Submission not found for Card: {}".format( data["model"]["id"]))
def test_submit_post(self, client, mocker): mocker.patch("application.routes.SubmissionsTrelloClient", new=MockTrelloClient) data = dict( email="*****@*****.**", title="foo", pitch="foo", format="IN-DEPTH", audience="INTERMEDIATE", description="foo", notes="foo", ) # https://stackoverflow.com/questions/37579411/testing-a-post-that-uses-flask-wtf-validate-on-submit resp = client.post("/submit", data=data, follow_redirects=True) # make sure it's successful assert resp.status_code == 200 # make sure the object was saved assert Submission().first(email="*****@*****.**")
def handle_comment_received(data, action): submission = Submission().first(card_id=data["model"]["id"]) if submission: current_app.logger.info( f"Comment received on Submission {submission.id}") template_params = { "comment": action["display"]["entities"]["comment"]["text"], "name": action["memberCreator"]["fullName"], } if action["memberCreator"]["id"] != current_app.config[ "TRELLO_ASSIGNEE"]: # Only send an email when a comment is left by someone other than the submitter # Organizers themselves should have notifications enabled for the whole board, so # they don't really need to get another email notification. This also prevents # a noisy email back to the submitter when they reply to a comment by email. send_email.apply_async( args=[submission.id, "comment", template_params]) else: current_app.logger.error("Submission not found for Card: {}".format( data["model"]["id"]))
def post(self, course_name, name, page=1): """Creates a new submission.""" course = Course.objects.get_or_404(name=course_name) project = Project.objects.get_or_404(name=name) if len(Submission.objects(project=project, submitter=g.user, processed=False)) > 4: abort(429, message="Too many pending submissions") if not g.user in course.students: abort(403, message="Must be a course student to submit") if not project in course.projects: abort(404, message="Project not found.") if not project.can_submit: abort(498, message="Due date has passed, tough luck!") if project.is_quiz: # Verify verification code args = submission_parser.parse_args() if g.user.verification_code != args['verification_code']: abort(400, message="Invalid verification code.") if len(request.files.values()) == 1: subm = Submission(submitter=g.user, project=project) for file in request.files.values(): if allowed_code_file(file.filename): grid_file = db.GridFSProxy() grid_file.put( file, filename=secure_filename(file.filename), content_type=file.mimetype) subm.code = grid_file else: abort(400, message="Only {0} files allowed".format(','.join(api.app.config['ALLOWED_CODE_EXTENSIONS']))) subm.save() project.submissions.append(subm) project.save() if api.app.config['DELETE_SUBMISSIONS']: junit_task.delay(str(subm.id)) else: junit_no_deletion.delay(str(subm.id)) return marshal(subm.to_dict(parent_course=course, parent_project=project), submission_fields), 201 else: abort(400, message="Can only submit one file.") # Bad request
def send_email(self, _id, template_name, template_params=None): """Sends an email Currently configured to send emails for updates to submission statuses Args: self: this task is bound to allow access to self _id (int): the submission id email(str): email address of the intended recipient template_name(str): the name of the template to use for this email. Must be located in templates/email directory. Todo: * use send_at to send emails at a updates at reasonable times * should this handle submission not found? * add attachment in Scheduled email. """ if not template_params: template_params = {} # logger = get_task_logger(__name__) # subject lines for emails based on submission status # not the best place for this, I admit. SUBJECTS = { "new": "Talk Submission Received", "inreview": "Talk Submission In-Review", "scheduled": "🎉 CONGRATS! Talk Submission Accepted 🎉", "comment": "Reply to Comments", } submission = Submission().get_by_id(_id) sg = SendGridAPIClient(apikey=current_app.config["SENDGRID_API_KEY"]) if not template_name: # No template specified, use the submission status to determine what we're sending template_name = Status(submission.status).name.lower() template = f"email/{template_name}.html" mail = Mail( Email(current_app.config["SENDGRID_DEFAULT_FROM"]), SUBJECTS[template_name], Email(submission.email), Content( "text/html", (render_template(template, title=SUBJECTS[template_name], submission=submission, **template_params)), ), ) mail.personalizations[0].add_cc( Email(current_app.config["SENDGRID_DEFAULT_FROM"])) if template_name == "comment" and submission.card_email: # Add the card itself as the reply-to address. When the user replies, it will create a # comment on the card via the Organizers account mail.reply_to = Email(submission.card_email) # mail.add_attachment(build_attachment()) # mail.send_at = 1443636842 sg.client.mail.send.post(request_body=mail.get())
def test_base_methods(self): # create calls save and new sub1 = Submission().create( **{ "title": "title", "email": "email", "card_id": "card_id", "card_url": "card_url", "card_email": "card_email", "description": "description", "notes": "notes", "pitch": "pitch", }) assert Submission().get_by_id(sub1.id) assert Submission().first(email="email") Submission().update(sub1, card_id="new_card_id") assert Submission().find(card_id="new_card_id") assert Submission().all() is not None assert sub1 in Submission().get_all(sub1.id) assert Submission().get_or_404(sub1.id) is not None assert sub1.to_dict() == { "title": sub1.title, "card_id": sub1.card_id, "card_url": sub1.card_url, "created_at": sub1.created_at, "updated_at": sub1.updated_at, "type": "Submission", "email": sub1.email, "hook": sub1.hook, "status": sub1.status, "card_email": sub1.card_email, "description": sub1.description, "notes": sub1.notes, "pitch": sub1.pitch, } with pytest.raises(ValueError): sub1._isinstance(str()) Submission().delete(sub1) assert Submission().get_by_id(sub1.id) is None