From 8ba8ae0495c0ef8192bd0a797f1687cde8d64526 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Sat, 24 Dec 2016 18:33:41 +0530 Subject: [PATCH 01/29] Merging Ben's PR --- auslib/admin/base.py | 24 +- auslib/admin/views/forms.py | 86 ++++- auslib/admin/views/permissions.py | 77 ++++- auslib/admin/views/releases.py | 79 ++++- auslib/admin/views/rules.py | 2 +- auslib/admin/views/scheduled_changes.py | 11 +- auslib/db.py | 77 +++-- auslib/global_state.py | 1 + ...elease_and_permission_scheduled_changes.py | 160 +++++++++ auslib/test/admin/views/test_permissions.py | 298 +++++++++++++++++ auslib/test/admin/views/test_releases.py | 315 +++++++++++++++++- auslib/test/admin/views/test_rules.py | 29 +- auslib/test/test_db.py | 31 +- auslib/web/base.py | 1 + docs/source/database.rst | 2 +- 15 files changed, 1117 insertions(+), 76 deletions(-) create mode 100644 auslib/migrate/versions/020_add_release_and_permission_scheduled_changes.py diff --git a/auslib/admin/base.py b/auslib/admin/base.py index 708013fa90..24f022d9ba 100644 --- a/auslib/admin/base.py +++ b/auslib/admin/base.py @@ -13,10 +13,16 @@ from auslib.admin.views.csrf import CSRFView from auslib.admin.views.permissions import UsersView, PermissionsView, \ - SpecificPermissionView, UserRolesView, UserRoleView + SpecificPermissionView, UserRolesView, UserRoleView, \ + PermissionScheduledChangesView, PermissionScheduledChangeView, \ + EnactPermissionScheduledChangeView, PermissionScheduledChangeHistoryView, \ + PermissionScheduledChangeSignoffsView from auslib.admin.views.releases import SingleLocaleView, \ SingleReleaseView, ReleaseHistoryView, \ - ReleasesAPIView, SingleReleaseColumnView, ReleaseReadOnlyView + ReleasesAPIView, SingleReleaseColumnView, ReleaseReadOnlyView, \ + ReleaseScheduledChangesView, ReleaseScheduledChangeView, \ + EnactReleaseScheduledChangeView, ReleaseScheduledChangeHistoryView, \ + ReleaseScheduledChangeSignoffsView from auslib.admin.views.rules import RulesAPIView, \ SingleRuleView, RuleHistoryAPIView, SingleRuleColumnView, \ RuleScheduledChangesView, RuleScheduledChangeView, \ @@ -59,6 +65,7 @@ def add_security_headers(response): response.headers['X-Content-Type-Options'] = 'nosniff' return response + Compress(app) @@ -92,3 +99,16 @@ def add_security_headers(response): app.add_url_rule("/scheduled_changes/rules//enact", view_func=EnactRuleScheduledChangeView.as_view("enact_scheduled_change_rules")) app.add_url_rule("/scheduled_changes/rules//signoffs", view_func=RuleScheduledChangeSignoffsView.as_view("scheduled_change_rules_signoffs")) app.add_url_rule("/scheduled_changes/rules//revisions", view_func=RuleScheduledChangeHistoryView.as_view("scheduled_change_rules_history")) +app.add_url_rule("/scheduled_changes/permissions", view_func=PermissionScheduledChangesView.as_view("scheduled_changes_permissions")) +app.add_url_rule("/scheduled_changes/permissions/", view_func=PermissionScheduledChangeView.as_view("scheduled_change_permissions")) +app.add_url_rule("/scheduled_changes/permissions//enact", view_func=EnactPermissionScheduledChangeView.as_view("enact_scheduled_change_permissions")) +app.add_url_rule("/scheduled_changes/permissions//signoffs", + view_func=PermissionScheduledChangeSignoffsView.as_view("scheduled_change_permissions_signoffs")) +app.add_url_rule("/scheduled_changes/permissions//revisions", + view_func=PermissionScheduledChangeHistoryView.as_view("scheduled_change_permissions_history")) +app.add_url_rule("/scheduled_changes/releases", view_func=ReleaseScheduledChangesView.as_view("scheduled_changes_releases")) +app.add_url_rule("/scheduled_changes/releases/", view_func=ReleaseScheduledChangeView.as_view("scheduled_change_releases")) +app.add_url_rule("/scheduled_changes/releases//enact", view_func=EnactReleaseScheduledChangeView.as_view("enact_scheduled_change_releases")) +app.add_url_rule("/scheduled_changes/releases//signoffs", view_func=ReleaseScheduledChangeSignoffsView.as_view("scheduled_change_release_signoffs")) +app.add_url_rule("/scheduled_changes/releases//revisions", + view_func=ReleaseScheduledChangeHistoryView.as_view("scheduled_change_releases_history")) diff --git a/auslib/admin/views/forms.py b/auslib/admin/views/forms.py index 9344af43b0..c756a6316f 100644 --- a/auslib/admin/views/forms.py +++ b/auslib/admin/views/forms.py @@ -140,11 +140,14 @@ class DbEditableForm(Form): data_version = IntegerField('data_version', validators=[InputRequired()], widget=HiddenInput()) -class ScheduledChangeForm(Form): +class ScheduledChangeTimeForm(Form): + when = IntegerField("When", validators=[Optional(), not_in_the_past()]) + + +class ScheduledChangeUptakeForm(Form): telemetry_product = NullableStringField("Telemetry Product") telemetry_channel = NullableStringField("Telemetry Channel") telemetry_uptake = NullableStringField("Telemetry Uptake") - when = IntegerField("When", validators=[Optional(), not_in_the_past()]) class NewPermissionForm(Form): @@ -155,6 +158,33 @@ class ExistingPermissionForm(DbEditableForm): options = JSONStringField(None, 'Options') +class ScheduledChangeNewPermissionForm(ScheduledChangeTimeForm, NewPermissionForm): + permission = StringField('Permission', validators=[Length(0, 50), InputRequired()]) + username = StringField('Username', validators=[Length(0, 100), InputRequired()]) + change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) + + +class ScheduledChangeExistingPermissionForm(ScheduledChangeTimeForm, ExistingPermissionForm): + permission = StringField('Permission', validators=[Length(0, 50), InputRequired()]) + username = StringField('Username', validators=[Length(0, 100), InputRequired()]) + change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) + + +class ScheduledChangeDeletePermissionForm(ScheduledChangeTimeForm): + change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete', 'delete')]) + permission = StringField('Permission', validators=[Length(0, 50), InputRequired()]) + username = StringField('Username', validators=[Length(0, 100), InputRequired()]) + data_version = IntegerField('data_version', validators=[InputRequired()], widget=HiddenInput()) + + +class EditScheduledChangeNewPermissionForm(ScheduledChangeTimeForm, NewPermissionForm): + sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) + + +class EditScheduledChangeExistingPermissionForm(ScheduledChangeTimeForm, ExistingPermissionForm): + sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) + + class PartialReleaseForm(Form): # Because we do implicit release creation in the Releases views, we can't # have data_version be InputRequired(). The views are responsible for checking @@ -212,11 +242,11 @@ class EditRuleForm(DbEditableForm): headerArchitecture = NullableStringField('Header Architecture', validators=[Optional(), Length(0, 10)]) -class ScheduledChangeNewRuleForm(ScheduledChangeForm, RuleForm): +class ScheduledChangeNewRuleForm(ScheduledChangeTimeForm, ScheduledChangeUptakeForm, RuleForm): change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) -class ScheduledChangeExistingRuleForm(ScheduledChangeForm, EditRuleForm): +class ScheduledChangeExistingRuleForm(ScheduledChangeTimeForm, ScheduledChangeUptakeForm, EditRuleForm): # EditRuleForm doesn't have rule_id in it because rules are edited through # URLs that contain them. Scheduled changes, on the other hand, are edited # through URLs that contain scheduled change IDs, so we need to include @@ -225,27 +255,27 @@ class ScheduledChangeExistingRuleForm(ScheduledChangeForm, EditRuleForm): rule_id = IntegerField('Rule ID', validators=[InputRequired()]) -class ScheduledChangeDeleteRuleForm(ScheduledChangeForm): +class ScheduledChangeDeleteRuleForm(ScheduledChangeTimeForm, ScheduledChangeUptakeForm): """ ScheduledChangeDeletionForm includes all the PK columns ,ScheduledChangeForm columns and data version """ change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete', 'delete')]) rule_id = IntegerField('Rule ID', validators=[InputRequired()]) - data_version = IntegerField('data_version', widget=HiddenInput()) + data_version = IntegerField('data_version', validators=[InputRequired()], widget=HiddenInput()) -class EditScheduledChangeNewRuleForm(ScheduledChangeForm, RuleForm): +class EditScheduledChangeNewRuleForm(ScheduledChangeTimeForm, ScheduledChangeUptakeForm, RuleForm): sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) # Unlike when scheduling a new change to an existing rule, rule_id is not # required (or even allowed) when modifying a scheduled change for an # existing rule. Allowing it to be modified would be confusing. -class EditScheduledChangeExistingRuleForm(ScheduledChangeForm, EditRuleForm): +class EditScheduledChangeExistingRuleForm(ScheduledChangeTimeForm, ScheduledChangeUptakeForm, EditRuleForm): sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) -class EditScheduledChangeDeleteRuleForm(ScheduledChangeForm): +class EditScheduledChangeDeleteRuleForm(ScheduledChangeTimeForm, ScheduledChangeUptakeForm): sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) @@ -265,3 +295,41 @@ class ReadOnlyForm(Form): product = StringField('Product', validators=[InputRequired()]) read_only = BooleanField('read_only') data_version = IntegerField('data_version', widget=HiddenInput()) + + +class ScheduledChangeNewReleaseForm(ScheduledChangeTimeForm): + name = StringField('Name', validators=[InputRequired()]) + product = StringField('Product', validators=[InputRequired()]) + data = JSONStringField({}, 'Data', validators=[InputRequired()], widget=FileInput()) + data_version = IntegerField('data_version', widget=HiddenInput()) + change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) + + +class ScheduledChangeExistingReleaseForm(ScheduledChangeTimeForm): + name = StringField('Name', validators=[InputRequired()]) + product = StringField('Product', validators=[Optional()]) + data = JSONStringField({}, 'Data', validators=[Optional()], widget=FileInput()) + data_version = IntegerField('data_version', widget=HiddenInput()) + change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) + + +class ScheduledChangeDeleteReleaseForm(ScheduledChangeTimeForm): + change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete', 'delete')]) + name = StringField('Name', validators=[InputRequired()]) + data_version = IntegerField('data_version', validators=[InputRequired()], widget=HiddenInput()) + + +class EditScheduledChangeNewReleaseForm(ScheduledChangeTimeForm): + name = StringField('Name', validators=[InputRequired()]) + product = StringField('Product', validators=[InputRequired()]) + data = JSONStringField({}, 'Data', validators=[InputRequired()], widget=FileInput()) + data_version = IntegerField('data_version', widget=HiddenInput()) + sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) + + +class EditScheduledChangeExistingReleaseForm(ScheduledChangeTimeForm): + name = StringField('Name', validators=[InputRequired()]) + product = StringField('Product', validators=[Optional()]) + data = JSONStringField({}, 'Data', validators=[Optional()], widget=FileInput()) + data_version = IntegerField('data_version', widget=HiddenInput()) + sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) diff --git a/auslib/admin/views/permissions.py b/auslib/admin/views/permissions.py index bc01fd18b6..f98158664c 100644 --- a/auslib/admin/views/permissions.py +++ b/auslib/admin/views/permissions.py @@ -4,7 +4,13 @@ from auslib.global_state import dbo from auslib.admin.views.base import requirelogin, AdminView -from auslib.admin.views.forms import NewPermissionForm, ExistingPermissionForm, DbEditableForm +from auslib.admin.views.forms import NewPermissionForm, ExistingPermissionForm, DbEditableForm, \ + ScheduledChangeNewPermissionForm, ScheduledChangeExistingPermissionForm, \ + EditScheduledChangeNewPermissionForm, EditScheduledChangeExistingPermissionForm, \ + ScheduledChangeDeletePermissionForm +from auslib.admin.views.scheduled_changes import ScheduledChangesView, \ + ScheduledChangeView, EnactScheduledChangeView, ScheduledChangeHistoryView, \ + SignoffsView __all__ = ["UsersView", "PermissionsView", "SpecificPermissionView"] @@ -97,6 +103,75 @@ def _delete(self, username, permission, changed_by, transaction): return Response(status=400, response=e.args) +class PermissionScheduledChangesView(ScheduledChangesView): + def __init__(self): + super(PermissionScheduledChangesView, self).__init__("permissions", dbo.permissions) + + @requirelogin + def _post(self, transaction, changed_by): + change_type = request.json.get("change_type") + + if change_type == "update": + form = ScheduledChangeExistingPermissionForm() + elif change_type == "insert": + form = ScheduledChangeNewPermissionForm() + elif change_type == "delete": + form = ScheduledChangeDeletePermissionForm() + else: + return Response(status=400, response="Invalid or missing change_type") + + return super(PermissionScheduledChangesView, self)._post(form, transaction, changed_by) + + +class PermissionScheduledChangeView(ScheduledChangeView): + def __init__(self): + super(PermissionScheduledChangeView, self).__init__("permissions", dbo.permissions) + + @requirelogin + def _post(self, sc_id, transaction, changed_by): + if request.json and request.json.get("data_version"): + form = EditScheduledChangeExistingPermissionForm() + else: + form = EditScheduledChangeNewPermissionForm() + + return super(PermissionScheduledChangeView, self)._post(sc_id, form, transaction, changed_by) + + @requirelogin + def _delete(self, sc_id, transaction, changed_by): + return super(PermissionScheduledChangeView, self)._delete(sc_id, transaction, changed_by) + + +class EnactPermissionScheduledChangeView(EnactScheduledChangeView): + def __init__(self): + super(EnactPermissionScheduledChangeView, self).__init__("permissions", dbo.permissions) + + @requirelogin + def _post(self, sc_id, transaction, changed_by): + return super(EnactPermissionScheduledChangeView, self)._post(sc_id, transaction, changed_by) + + +class PermissionScheduledChangeSignoffsView(SignoffsView): + def __init__(self): + super(PermissionScheduledChangeSignoffsView, self).__init__("permissions", dbo.permissions) + + @requirelogin + def _post(self, sc_id, transaction, changed_by): + return super(PermissionScheduledChangeSignoffsView, self)._post(sc_id, transaction, changed_by) + + @requirelogin + def _delete(self, sc_id, transaction, changed_by): + return super(PermissionScheduledChangeSignoffsView, self)._delete(sc_id, transaction, changed_by) + + +class PermissionScheduledChangeHistoryView(ScheduledChangeHistoryView): + def __init__(self): + super(PermissionScheduledChangeHistoryView, self).__init__("permissions", dbo.permissions) + + @requirelogin + def _post(self, sc_id, transaction, changed_by): + return super(PermissionScheduledChangeHistoryView, self)._post(sc_id, transaction, changed_by) + + class UserRolesView(AdminView): """/users/:username/roles""" diff --git a/auslib/admin/views/releases.py b/auslib/admin/views/releases.py index e889b0ef55..011312dcee 100644 --- a/auslib/admin/views/releases.py +++ b/auslib/admin/views/releases.py @@ -11,7 +11,12 @@ requirelogin, AdminView, HistoryAdminView ) from auslib.admin.views.csrf import get_csrf_headers -from auslib.admin.views.forms import PartialReleaseForm, CompleteReleaseForm, DbEditableForm, ReadOnlyForm +from auslib.admin.views.forms import PartialReleaseForm, CompleteReleaseForm, DbEditableForm, ReadOnlyForm, \ + ScheduledChangeNewReleaseForm, ScheduledChangeExistingReleaseForm, ScheduledChangeDeleteReleaseForm, \ + EditScheduledChangeNewReleaseForm, EditScheduledChangeExistingReleaseForm +from auslib.admin.views.scheduled_changes import ScheduledChangesView, \ + ScheduledChangeView, EnactScheduledChangeView, ScheduledChangeHistoryView, \ + SignoffsView __all__ = ["SingleReleaseView", "SingleLocaleView"] @@ -520,3 +525,75 @@ def get(self, column): column: column_values, } return jsonify(ret) + + +class ReleaseScheduledChangesView(ScheduledChangesView): + def __init__(self): + super(ReleaseScheduledChangesView, self).__init__("releases", dbo.releases) + + @requirelogin + def _post(self, transaction, changed_by): + change_type = request.json.get("change_type") + + if change_type == "update": + form = ScheduledChangeExistingReleaseForm() + form.data.data = createBlob(form.data.data) + elif change_type == "insert": + form = ScheduledChangeNewReleaseForm() + form.data.data = createBlob(form.data.data) + elif change_type == "delete": + form = ScheduledChangeDeleteReleaseForm() + else: + return Response(status=400, response="Invalid or missing change_type") + + return super(ReleaseScheduledChangesView, self)._post(form, transaction, changed_by) + + +class ReleaseScheduledChangeView(ScheduledChangeView): + def __init__(self): + super(ReleaseScheduledChangeView, self).__init__("releases", dbo.releases) + + @requirelogin + def _post(self, sc_id, transaction, changed_by): + if request.json and request.json.get("data_version"): + form = EditScheduledChangeExistingReleaseForm() + else: + form = EditScheduledChangeNewReleaseForm() + + form.data.data = createBlob(form.data.data) + return super(ReleaseScheduledChangeView, self)._post(sc_id, form, transaction, changed_by) + + @requirelogin + def _delete(self, sc_id, transaction, changed_by): + return super(ReleaseScheduledChangeView, self)._delete(sc_id, transaction, changed_by) + + +class EnactReleaseScheduledChangeView(EnactScheduledChangeView): + def __init__(self): + super(EnactReleaseScheduledChangeView, self).__init__("releases", dbo.releases) + + @requirelogin + def _post(self, sc_id, transaction, changed_by): + return super(EnactReleaseScheduledChangeView, self)._post(sc_id, transaction, changed_by) + + +class ReleaseScheduledChangeSignoffsView(SignoffsView): + def __init__(self): + super(ReleaseScheduledChangeSignoffsView, self).__init__("releases", dbo.releases) + + @requirelogin + def _post(self, sc_id, transaction, changed_by): + return super(ReleaseScheduledChangeSignoffsView, self)._post(sc_id, transaction, changed_by) + + @requirelogin + def _delete(self, sc_id, transaction, changed_by): + return super(ReleaseScheduledChangeSignoffsView, self)._delete(sc_id, transaction, changed_by) + + +class ReleaseScheduledChangeHistoryView(ScheduledChangeHistoryView): + def __init__(self): + super(ReleaseScheduledChangeHistoryView, self).__init__("releases", dbo.releases) + + @requirelogin + def _post(self, sc_id, transaction, changed_by): + return super(ReleaseScheduledChangeHistoryView, self)._post(sc_id, transaction, changed_by) diff --git a/auslib/admin/views/rules.py b/auslib/admin/views/rules.py index b8567fbce4..d506c39a8c 100644 --- a/auslib/admin/views/rules.py +++ b/auslib/admin/views/rules.py @@ -329,7 +329,7 @@ def _post(self, transaction, changed_by): form = ScheduledChangeDeleteRuleForm() else: - return Response(status=400, response="Change Type invalid or not entered") + return Response(status=400, response="Invalid or missing change_type") return super(RuleScheduledChangesView, self)._post(form, transaction, changed_by) diff --git a/auslib/admin/views/scheduled_changes.py b/auslib/admin/views/scheduled_changes.py index 1fab46a723..c57ca75895 100644 --- a/auslib/admin/views/scheduled_changes.py +++ b/auslib/admin/views/scheduled_changes.py @@ -47,7 +47,12 @@ def _post(self, form, transaction, changed_by): return Response(status=400, response=json.dumps(form.errors)) try: - sc_id = self.sc_table.insert(changed_by, transaction, **form.data) + # Forms can normally be accessed as a dict through form.data, + # but because some of the Forms we end up using have a Field + # called "data", this gets overridden, so we need to construct + # a dict ourselves. + columns = {k: v.data for k, v in form._fields.iteritems()} + sc_id = self.sc_table.insert(changed_by, transaction, **columns) except ValueError as e: self.log.warning("Bad input: %s", e) return Response(status=400, response=json.dumps({"exception": e.args})) @@ -74,7 +79,7 @@ def _post(self, sc_id, form, transaction, changed_by): # We need to be able to support changing AND removing things # and because of how Flask's request object and WTForm's defaults work # this gets a little hairy. - for k, v in form.data.iteritems(): + for k, v in form._fields.iteritems(): # sc_data_version is a "special" column, in that it's not part of the # primary data, and shouldn't be updatable by the user. if k == "sc_data_version": @@ -87,7 +92,7 @@ def _post(self, sc_id, form, transaction, changed_by): # will have already converted it to None, so we can treat it the # same as a modification here. if (request.json and k in request.json): - what[k] = v + what[k] = v.data try: self.sc_table.update({"sc_id": sc_id}, what, changed_by, form.sc_data_version.data, transaction) diff --git a/auslib/db.py b/auslib/db.py index f00750420b..b9b24e788d 100644 --- a/auslib/db.py +++ b/auslib/db.py @@ -111,10 +111,14 @@ def BlobColumn(impl=Text): class cls(sqlalchemy.types.TypeDecorator): def process_bind_param(self, value, dialect): - return value.getJSON() + if value: + value = value.getJSON() + return value def process_result_value(self, value, dialect): - return createBlob(value) + if value: + value = createBlob(value) + return value cls.impl = impl return cls @@ -961,10 +965,9 @@ def _dataVersionsAreSynced(self, sc_id, transaction): return True def validate(self, base_columns, condition_columns, changed_by, sc_id=None, transaction=None): - # We need to do additional checks for any changes that are modifying an - # existing row. These lists will have PK clauses in them at the end of - # the following loop, but only if the change contains a PK. This makes - # it easy to do the extra checks conditionally afterwards. + # Depending on the change type, we may do some additional checks + # against the base table PK columns. It's cleaner to build up these + # early than do it later. base_table_where = [] sc_table_where = [] @@ -979,12 +982,7 @@ def validate(self, base_columns, condition_columns, changed_by, sc_id=None, tran base_column = getattr(self.baseTable, pk) if pk in base_columns: sc_table_where.append(getattr(self, "base_%s" % pk) == base_columns[pk]) - # If a non-null data_version was provided it implies that the - # base table row should already exist. This will be checked for - # after we finish basic checks on the individual parts of the - # PK. - if "data_version" in base_columns and base_columns["data_version"]: - base_table_where.append(getattr(self.baseTable, pk) == base_columns[pk]) + base_table_where.append(getattr(self.baseTable, pk) == base_columns[pk]) # Non-Integer columns can have autoincrement set to True for some reason. # Any non-integer columns in the primary key are always required (because # autoincrement actually isn't a thing for them), and any Integer columns @@ -992,22 +990,32 @@ def validate(self, base_columns, condition_columns, changed_by, sc_id=None, tran elif not isinstance(base_column.type, (sqlalchemy.types.Integer,)) or not base_column.autoincrement: raise ValueError("Missing primary key column '%s' which is not autoincrement", pk) - # If anything ended up in base_table_where, it means that the baseTable - # row should already exist. In these cases, we need to check to make sure - # that the scheduled change has the same data version as the base table, - # to ensure that a change is not being scheduled from an out of date version - # of the base table row. - if base_table_where: + if base_columns["change_type"] == "delete": + for pk in self.base_primary_key: + if pk not in base_columns: + raise ValueError("Missing primary key column %s. PK values needed for deletion" % (pk)) + if base_columns[pk] is None: + raise ValueError("%s value found to be None. PK value can not be None for deletion" % (pk)) + elif base_columns["change_type"] == "update": + # For updates, we need to make sure that the baseTable row already + # exists, and that the data version provided matches the current + # version to ensure that someone isn't trying to schedule a change + # against out-of-date data. current_data_version = self.baseTable.select(columns=(self.baseTable.data_version,), where=base_table_where, transaction=transaction) if not current_data_version: raise ValueError("Cannot create scheduled change with data_version for non-existent row") if current_data_version and current_data_version[0]["data_version"] != base_columns.get("data_version"): raise OutdatedDataError("Wrong data_version given for base table, cannot create scheduled change.") - - # If the change has a PK in it and the change isn't already scheduled - # (meaning we're validating an update to it), we must ensure that no - # existing change with that PK is active before allowing it. + elif base_columns["change_type"] == "insert" and base_table_where: + # If the base table row shouldn't already exist, we need to make sure they don't + # to avoid getting an IntegrityError when the change is enacted. + if self.baseTable.select(columns=(self.baseTable.data_version,), where=base_table_where, transaction=transaction): + raise ValueError("Cannot schedule change for duplicate PK") + + # If we're validating a new scheduled change (sc_id is None), we need + # to make sure that no other scheduled change already exists if a + # primary key for the base table was provided (sc_table_where is not empty). if not sc_id and sc_table_where: sc_table_where.append(self.complete == False) # noqa because we need to use == for sqlalchemy operator overloading to work if len(self.select(columns=[self.sc_id], where=sc_table_where)) > 0: @@ -1089,6 +1097,7 @@ def update(self, where, what, changed_by, old_data_version, transaction=None, dr base_what = self._prefixColumns(base_what) base_what["scheduled_by"] = changed_by + super(ScheduledChangeTable, self).update(where, base_what, changed_by, old_data_version, transaction, dryrun=dryrun) for sc_id in affected_ids: @@ -1510,7 +1519,7 @@ def __init__(self, db, metadata, dialect): else: dataType = Text self.table.append_column(Column('data', BlobColumn(dataType), nullable=False)) - AUSTable.__init__(self, db, dialect) + AUSTable.__init__(self, db, dialect, scheduled_changes=True, scheduled_changes_kwargs={"conditions": ["time"]}) def setDomainWhitelist(self, domainWhitelist): self.domainWhitelist = domainWhitelist @@ -1785,20 +1794,21 @@ def localeExists(self, name, platform, locale, transaction=None): def delete(self, where, changed_by, old_data_version, transaction=None, dryrun=False): names = [] - mapping_count = dbo.rules.t.count().where(dbo.rules.mapping == where["name"]).execute().fetchone()[0] - - whitelist_count = dbo.rules.t.count().where(dbo.rules.whitelist == where["name"]).execute().fetchone()[0] - - fallbackMapping_count = dbo.rules.t.count().where(dbo.rules.fallbackMapping == where["name"]).execute().fetchone()[0] - - if mapping_count > 0 or whitelist_count > 0 or fallbackMapping_count > 0: - msg = "%s has rules pointing to it. Hence it cannot be deleted." % (self.name) - raise ValueError(msg) for toDelete in self.select(where=where, columns=[self.name, self.product], transaction=transaction): names.append(toDelete["name"]) self._proceedIfNotReadOnly(toDelete["name"], transaction=transaction) if not self.db.hasPermission(changed_by, "release", "delete", toDelete["product"], transaction): raise PermissionDeniedError("%s is not allowed to delete releases for product %s" % (changed_by, toDelete["product"])) + + for name in names: + mapping_count = dbo.rules.t.count().where(dbo.rules.mapping == name).execute().fetchone()[0] + whitelist_count = dbo.rules.t.count().where(dbo.rules.whitelist == name).execute().fetchone()[0] + fallbackMapping_count = dbo.rules.t.count().where(dbo.rules.fallbackMapping == name).execute().fetchone()[0] + + if mapping_count > 0 or whitelist_count > 0 or fallbackMapping_count > 0: + msg = "%s has rules pointing to it. Hence it cannot be deleted." % (self.name) + raise ValueError(msg) + super(Releases, self).delete(where=where, changed_by=changed_by, old_data_version=old_data_version, transaction=transaction, dryrun=dryrun) if not dryrun: for name in names: @@ -1854,7 +1864,7 @@ def __init__(self, db, metadata, dialect): Column('options', JSONColumn) ) self.user_roles = UserRoles(db, metadata, dialect) - AUSTable.__init__(self, db, dialect) + AUSTable.__init__(self, db, dialect, scheduled_changes=True, scheduled_changes_kwargs={"conditions": ["time"]}) def assertPermissionExists(self, permission): if permission not in self.allPermissions.keys(): @@ -2131,6 +2141,7 @@ def bleet(table, type_, changed_by, query, transaction): table.log.debug("Sending change notification mail for %s to %s", table.t.name, to_addr) return bleet + # A helper that sets sql_mode. This should only be used with MySQL, and # lets us put the database in a stricter mode that will disallow things like # automatic data truncation. diff --git a/auslib/global_state.py b/auslib/global_state.py index d332e2b077..056fef3175 100644 --- a/auslib/global_state.py +++ b/auslib/global_state.py @@ -22,6 +22,7 @@ def __getattr__(self, name): raise RuntimeError("No database configured") return getattr(self.db, name) + dbo = DbWrapper() # Similar to the above, we have a complication around having two separate diff --git a/auslib/migrate/versions/020_add_release_and_permission_scheduled_changes.py b/auslib/migrate/versions/020_add_release_and_permission_scheduled_changes.py new file mode 100644 index 0000000000..53254069ae --- /dev/null +++ b/auslib/migrate/versions/020_add_release_and_permission_scheduled_changes.py @@ -0,0 +1,160 @@ +from sqlalchemy import Table, Column, Integer, String, MetaData, \ + BigInteger, Boolean, Text + + +def upgrade(migrate_engine): + metadata = MetaData(bind=migrate_engine) + if migrate_engine.name == "mysql": + from sqlalchemy.dialects.mysql import LONGTEXT + bigintType = BigInteger + dataType = LONGTEXT + elif migrate_engine.name == "sqlite": + bigintType = Integer + dataType = Text + + releases_scheduled_changes = Table( # noqa - to hush pyflakes about this not being used + "releases_scheduled_changes", metadata, + Column("sc_id", Integer, primary_key=True, autoincrement=True), + Column("scheduled_by", String(100), nullable=False), + Column("complete", Boolean, default=False), + Column("change_type", String(50), nullable=False), + Column("data_version", Integer), + Column("base_name", String(100), nullable=False), + Column("base_product", String(15)), + Column("base_data", dataType), + Column("base_read_only", Boolean, default=False), + Column("base_data_version", Integer), + ) + + releases_scheduled_changes_history = Table( # noqa + "releases_scheduled_changes_history", metadata, + Column("change_id", Integer, primary_key=True, autoincrement=True), + Column("changed_by", String(100), nullable=False), + Column("timestamp", bigintType, nullable=False), + Column("sc_id", Integer, nullable=False), + Column("scheduled_by", String(100)), + Column("complete", Boolean, default=False), + Column("change_type", String(50), nullable=True), + Column("data_version", Integer), + Column("base_name", String(100)), + Column("base_product", String(15)), + Column("base_data", dataType), + Column("base_read_only", Boolean, default=False), + Column("base_data_version", Integer) + ) + + releases_scheduled_changes_conditions = Table( # noqa + "releases_scheduled_changes_conditions", metadata, + Column("sc_id", Integer, primary_key=True, autoincrement=True), + Column("when", bigintType, nullable=False), + Column("data_version", Integer), + ) + + releases_scheduled_changes_conditions_history = Table( # noqa + "releases_scheduled_changes_conditions_history", metadata, + Column("change_id", Integer, primary_key=True, autoincrement=True), + Column("changed_by", String(100), nullable=False), + Column("timestamp", bigintType, nullable=False), + Column("sc_id", Integer, nullable=False), + Column("when", bigintType), + Column("data_version", Integer), + ) + + releases_scheduled_changes_signoffs = Table( # noqa + "releases_scheduled_changes_signoffs", metadata, + Column("sc_id", Integer, primary_key=True, autoincrement=False), + Column("username", String(100), primary_key=True), + Column("role", String(50), nullable=False), + ) + + releases_scheduled_changes_signoffs_history = Table( # noqa + "releases_scheduled_changes_signoffs_history", metadata, + Column("change_id", Integer, primary_key=True, autoincrement=True), + Column("changed_by", String(100), nullable=False), + Column("timestamp", bigintType, nullable=False), + Column("sc_id", Integer, nullable=False, autoincrement=False), + Column("username", String(100), nullable=False), + Column("role", String(50)), + ) + + permissions_scheduled_changes = Table( # noqa + "permissions_scheduled_changes", metadata, + Column("sc_id", Integer, primary_key=True, autoincrement=True), + Column("scheduled_by", String(100), nullable=False), + Column("complete", Boolean, default=False), + Column("change_type", String(50), nullable=False), + Column("data_version", Integer), + Column('base_permission', String(50), nullable=False), + Column('base_username', String(100), nullable=False), + Column('base_options', Text), + Column("base_data_version", Integer), + ) + + permissions_scheduled_changes_history = Table( # noqa + "permissions_scheduled_changes_history", metadata, + Column("change_id", Integer, primary_key=True, autoincrement=True), + Column("changed_by", String(100), nullable=False), + Column("timestamp", bigintType, nullable=False), + Column("sc_id", Integer, nullable=False), + Column("scheduled_by", String(100)), + Column("change_type", String(50), nullable=True), + Column("complete", Boolean, default=False), + Column("data_version", Integer), + Column('base_permission', String(50)), + Column('base_username', String(100)), + Column('base_options', Text), + Column("base_data_version", Integer) + ) + + permissions_scheduled_changes_conditions = Table( # noqa + "permissions_scheduled_changes_conditions", metadata, + Column("sc_id", Integer, primary_key=True, autoincrement=True), + Column("when", bigintType, nullable=False), + Column("data_version", Integer), + ) + + permissions_scheduled_changes_conditions_history = Table( # noqa + "permissions_scheduled_changes_conditions_history", metadata, + Column("change_id", Integer, primary_key=True, autoincrement=True), + Column("changed_by", String(100), nullable=False), + Column("timestamp", bigintType, nullable=False), + Column("sc_id", Integer, nullable=False), + Column("when", bigintType), + Column("data_version", Integer), + ) + + permissions_scheduled_changes_signoffs = Table( # noqa + "permissions_scheduled_changes_signoffs", metadata, + Column("sc_id", Integer, primary_key=True, autoincrement=False), + Column("username", String(100), primary_key=True), + Column("role", String(50), nullable=False), + ) + + permissions_scheduled_changes_signoffs_history = Table( # noqa + "permissions_scheduled_changes_signoffs_history", metadata, + Column("change_id", Integer, primary_key=True, autoincrement=True), + Column("changed_by", String(100), nullable=False), + Column("timestamp", bigintType, nullable=False), + Column("sc_id", Integer, nullable=False, autoincrement=False), + Column("username", String(100), nullable=False), + Column("role", String(50)), + ) + + metadata.create_all() + + +def downgrade(migrate_engine): + metadata = MetaData(bind=migrate_engine) + + Table("releases_scheduled_changes", metadata, autoload=True).drop() + Table("releases_scheduled_changes_history", metadata, autoload=True).drop() + Table("releases_scheduled_changes_conditions", metadata, autoload=True).drop() + Table("releases_scheduled_changes_conditions_history", metadata, autoload=True).drop() + Table("releases_scheduled_changes_signoffs", metadata, autoload=True).drop() + Table("releases_scheduled_changes_signoffs_history", metadata, autoload=True).drop() + Table("permissions_scheduled_changes", metadata, autoload=True).drop() + Table("permissions_scheduled_changes_history", metadata, autoload=True).drop() + Table("permissions_scheduled_changes_conditions", metadata, autoload=True).drop() + Table("permissions_scheduled_changes_conditions_history", metadata, autoload=True).drop() + Table("permissions_scheduled_changes_signoffs", metadata, autoload=True).drop() + Table("permissions_scheduled_changes_signoffs_history", metadata, autoload=True).drop() diff --git a/auslib/test/admin/views/test_permissions.py b/auslib/test/admin/views/test_permissions.py index 625e0deff3..ed07e82203 100644 --- a/auslib/test/admin/views/test_permissions.py +++ b/auslib/test/admin/views/test_permissions.py @@ -1,3 +1,4 @@ +import mock import simplejson as json from auslib.global_state import dbo @@ -169,6 +170,303 @@ def testPermissionDeleteWithoutPermission(self): self.assertStatusCode(ret, 403) +class TestPermissionsScheduledChanges(ViewTest): + maxDiff = 10000 + + def setUp(self): + super(TestPermissionsScheduledChanges, self).setUp() + dbo.permissions.scheduled_changes.t.insert().execute( + sc_id=1, scheduled_by="bill", change_type="insert", data_version=1, base_permission="rule", base_username="janet", + base_options={"products": ["foo"]}, + ) + dbo.permissions.scheduled_changes.history.t.insert().execute(change_id=1, changed_by="bill", timestamp=20, sc_id=1) + dbo.permissions.scheduled_changes.history.t.insert().execute( + change_id=2, changed_by="bill", timestamp=21, sc_id=1, scheduled_by="bill", change_type="insert", data_version=1, + base_permission="rule", base_username="janet", base_options={"products": ["foo"]}, + ) + dbo.permissions.scheduled_changes.signoffs.t.insert().execute(sc_id=1, username="bill", role="releng") + dbo.permissions.scheduled_changes.conditions.t.insert().execute(sc_id=1, when=10000000, data_version=1) + dbo.permissions.scheduled_changes.conditions.history.t.insert().execute(change_id=1, changed_by="bill", timestamp=20, sc_id=1) + dbo.permissions.scheduled_changes.conditions.history.t.insert().execute( + change_id=2, changed_by="bill", timestamp=21, sc_id=1, when=10000000, data_version=1 + ) + + dbo.permissions.scheduled_changes.t.insert().execute( + sc_id=2, scheduled_by="bill", change_type="update", data_version=1, base_permission="release_locale", base_username="ashanti", + base_options=None, base_data_version=1, + ) + dbo.permissions.scheduled_changes.history.t.insert().execute(change_id=3, changed_by="bill", timestamp=40, sc_id=2) + dbo.permissions.scheduled_changes.history.t.insert().execute( + change_id=4, changed_by="bill", timestamp=41, sc_id=2, scheduled_by="bill", change_type="update", data_version=1, + base_permission="release_locale", base_username="ashanti", base_options=None, base_data_version=1 + ) + dbo.permissions.scheduled_changes.conditions.t.insert().execute(sc_id=2, when=20000000, data_version=1) + dbo.permissions.scheduled_changes.conditions.history.t.insert().execute(change_id=3, changed_by="bill", timestamp=40, sc_id=2) + dbo.permissions.scheduled_changes.conditions.history.t.insert().execute( + change_id=4, changed_by="bill", timestamp=41, sc_id=2, when=20000000, data_version=1 + ) + + dbo.permissions.scheduled_changes.t.insert().execute( + sc_id=3, scheduled_by="bill", change_type="insert", data_version=2, base_permission="permission", base_username="bob", complete=True + ) + dbo.permissions.scheduled_changes.history.t.insert().execute(change_id=5, changed_by="bill", timestamp=60, sc_id=3) + dbo.permissions.scheduled_changes.history.t.insert().execute( + change_id=6, changed_by="bill", timestamp=61, sc_id=3, scheduled_by="bill", change_type="insert", data_version=1, + base_permission="permission", base_username="bob", complete=False, + ) + dbo.permissions.scheduled_changes.history.t.insert().execute( + change_id=7, changed_by="bill", timestamp=100, sc_id=3, scheduled_by="bill", change_type="insert", data_version=2, + base_permission="permission", base_username="bob", complete=True, + ) + dbo.permissions.scheduled_changes.conditions.t.insert().execute(sc_id=3, when=30000000, data_version=2) + dbo.permissions.scheduled_changes.conditions.history.t.insert().execute(change_id=5, changed_by="bill", timestamp=60, sc_id=3) + dbo.permissions.scheduled_changes.conditions.history.t.insert().execute( + change_id=6, changed_by="bill", timestamp=61, sc_id=3, when=30000000, data_version=1 + ) + dbo.permissions.scheduled_changes.conditions.history.t.insert().execute( + change_id=7, changed_by="bill", timestamp=100, sc_id=3, when=30000000, data_version=2 + ) + + def testGetScheduledChanges(self): + ret = self._get("/scheduled_changes/permissions") + expected = { + "count": 2, + "scheduled_changes": [ + { + "sc_id": 1, "when": 10000000, "scheduled_by": "bill", "change_type": "insert", "complete": False, "sc_data_version": 1, + "permission": "rule", "username": "janet", "options": {"products": ["foo"]}, "data_version": None, + "signoffs": {"bill": "releng"}, + }, + { + "sc_id": 2, "when": 20000000, "scheduled_by": "bill", "change_type": "update", "complete": False, "sc_data_version": 1, + "permission": "release_locale", "username": "ashanti", "options": None, "data_version": 1, "signoffs": {}, + }, + ], + } + self.assertEquals(json.loads(ret.data), expected) + + def testGetScheduledChangesWithCompleted(self): + ret = self._get("/scheduled_changes/permissions", qs={"all": 1}) + expected = { + "count": 3, + "scheduled_changes": [ + { + "sc_id": 1, "when": 10000000, "scheduled_by": "bill", "change_type": "insert", "complete": False, "sc_data_version": 1, + "permission": "rule", "username": "janet", "options": {"products": ["foo"]}, "data_version": None, + "signoffs": {"bill": "releng"}, + }, + { + "sc_id": 2, "when": 20000000, "scheduled_by": "bill", "change_type": "update", "complete": False, "sc_data_version": 1, + "permission": "release_locale", "username": "ashanti", "options": None, "data_version": 1, "signoffs": {}, + }, + { + "sc_id": 3, "when": 30000000, "scheduled_by": "bill", "change_type": "insert", "complete": True, "sc_data_version": 2, + "permission": "permission", "username": "bob", "options": None, "data_version": None, "signoffs": {}, + }, + ], + } + self.assertEquals(json.loads(ret.data), expected) + + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testAddScheduledChangeExistingPermission(self): + data = { + "when": 400000000, "permission": "rule", "username": "bob", "options": None, "data_version": 1, "change_type": "update", + } + ret = self._post("/scheduled_changes/permissions", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"sc_id": 4}) + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 4, "scheduled_by": "bill", "change_type": "update", "complete": False, "data_version": 1, + "base_permission": "rule", "base_username": "bob", "base_options": None, "base_data_version": 1, + } + self.assertEquals(db_data, expected) + cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 4, "data_version": 1, "when": 400000000} + self.assertEquals(dict(cond[0]), cond_expected) + + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testAddScheduledChangeNewPermission(self): + data = { + "when": 400000000, "permission": "release", "username": "jill", "options": '{"products": ["a"]}', "change_type": "insert", + } + ret = self._post("/scheduled_changes/permissions", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"sc_id": 4}) + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + db_data["base_options"] = db_data["base_options"] + expected = { + "sc_id": 4, "scheduled_by": "bill", "change_type": "insert", "complete": False, "data_version": 1, + "base_permission": "release", "base_username": "jill", "base_options": {"products": ["a"]}, "base_data_version": None, + } + self.assertEquals(db_data, expected) + cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 4, "data_version": 1, "when": 400000000} + self.assertEquals(dict(cond[0]), cond_expected) + + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testAddScheduledChangeDeletePermission(self): + data = { + "when": 400000000, "permission": "build", "username": "ashanti", "change_type": "delete", "data_version": 1, + } + ret = self._post("/scheduled_changes/permissions", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"sc_id": 4}) + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 4, "scheduled_by": "bill", "change_type": "delete", "complete": False, "data_version": 1, + "base_permission": "build", "base_username": "ashanti", "base_options": None, "base_data_version": 1, + } + self.assertEquals(db_data, expected) + cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 4, "data_version": 1, "when": 400000000} + self.assertEquals(dict(cond[0]), cond_expected) + + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testUpdateScheduledChangeExistingPermission(self): + data = { + "options": '{"products": ["Thunderbird"]}', "data_version": 1, "sc_data_version": 1, "when": 200000000, + } + ret = self._post("/scheduled_changes/permissions/2", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"new_data_version": 2}) + + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 2).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + db_data["base_options"] = db_data["base_options"] + expected = { + "sc_id": 2, "complete": False, "data_version": 2, "scheduled_by": "bill", "change_type": "update", "base_permission": "release_locale", + "base_username": "ashanti", "base_options": {"products": ["Thunderbird"]}, "base_data_version": 1, + } + self.assertEquals(db_data, expected) + cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 2).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 2, "data_version": 2, "when": 200000000} + self.assertEquals(dict(cond[0]), cond_expected) + + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testUpdateScheduledChangeNewPermission(self): + data = { + "options": '{"products": ["Firefox"]}', "sc_data_version": 1, "when": 450000000, + } + ret = self._post("/scheduled_changes/permissions/1", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"new_data_version": 2}) + + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 1).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + db_data["base_options"] = db_data["base_options"] + expected = { + "sc_id": 1, "complete": False, "data_version": 2, "scheduled_by": "bill", "change_type": "insert", "base_permission": "rule", + "base_username": "janet", "base_options": {"products": ["Firefox"]}, "base_data_version": None, + } + self.assertEquals(db_data, expected) + cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 1).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 1, "data_version": 2, "when": 450000000} + self.assertEquals(dict(cond[0]), cond_expected) + + def testDeleteScheduledChange(self): + ret = self._delete("/scheduled_changes/permissions/1", qs={"data_version": 1}) + self.assertEquals(ret.status_code, 200, ret.data) + got = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 1).execute().fetchall() + self.assertEquals(got, []) + cond_got = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 1).execute().fetchall() + self.assertEquals(cond_got, []) + + def testEnactScheduledChangeExistingPermission(self): + ret = self._post("/scheduled_changes/permissions/2/enact") + self.assertEquals(ret.status_code, 200, ret.data) + + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 2).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 2, "complete": True, "data_version": 2, "scheduled_by": "bill", "change_type": "update", "base_permission": "release_locale", + "base_username": "ashanti", "base_options": None, "base_data_version": 1, + } + self.assertEquals(db_data, expected) + + base_row = dbo.permissions.t.select().where(dbo.permissions.username == "ashanti")\ + .where(dbo.permissions.permission == "release_locale")\ + .execute().fetchall()[0] + base_expected = { + "permission": "release_locale", "username": "ashanti", "options": None, "data_version": 2 + } + self.assertEquals(dict(base_row), base_expected) + + def testEnactScheduledChangeNewPermission(self): + ret = self._post("/scheduled_changes/permissions/1/enact") + self.assertEquals(ret.status_code, 200, ret.data) + + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 1).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + db_data["base_options"] = db_data["base_options"] + expected = { + "sc_id": 1, "complete": True, "data_version": 2, "scheduled_by": "bill", "change_type": "insert", "base_permission": "rule", + "base_username": "janet", "base_options": {"products": ["foo"]}, "base_data_version": None, + } + self.assertEquals(db_data, expected) + + base_row = dict(dbo.permissions.t.select().where(dbo.permissions.username == "janet") + .where(dbo.permissions.permission == "rule") + .execute().fetchall()[0]) + base_expected = { + "permission": "rule", "username": "janet", "options": {"products": ["foo"]}, "data_version": 1 + } + self.assertEquals(dict(base_row), base_expected) + + def testGetScheduledChangeHistoryRevisions(self): + ret = self._get("/scheduled_changes/permissions/3/revisions") + self.assertEquals(ret.status_code, 200, ret.data) + expected = { + "count": 2, + "revisions": [ + { + "change_id": 7, "changed_by": "bill", "timestamp": 100, "sc_id": 3, "scheduled_by": "bill", "change_type": "insert", + "data_version": None, "permission": "permission", "username": "bob", "options": None, "when": 30000000, "complete": True, + "sc_data_version": 2, + }, + { + "change_id": 6, "changed_by": "bill", "timestamp": 61, "sc_id": 3, "scheduled_by": "bill", "change_type": "insert", + "data_version": None, "permission": "permission", "username": "bob", "options": None, "when": 30000000, "complete": False, + "sc_data_version": 1, + }, + ], + } + self.assertEquals(json.loads(ret.data), expected) + + def testSignoffWithPermission(self): + ret = self._post("/scheduled_changes/permissions/2/signoffs", data=dict(role="qa"), username="bill") + self.assertEquals(ret.status_code, 200, ret.data) + r = dbo.permissions.scheduled_changes.signoffs.t.select().where(dbo.permissions.scheduled_changes.signoffs.sc_id == 2).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + self.assertEquals(db_data, {"sc_id": 2, "username": "bill", "role": "qa"}) + + def testSignoffWithoutPermission(self): + ret = self._post("/scheduled_changes/permissions/2/signoffs", data=dict(role="relman"), username="bill") + self.assertEquals(ret.status_code, 403, ret.data) + + def testRevokeSignoff(self): + ret = self._delete("/scheduled_changes/permissions/1/signoffs", username="bill") + self.assertEquals(ret.status_code, 200, ret.data) + r = dbo.permissions.scheduled_changes.signoffs.t.select().where(dbo.permissions.scheduled_changes.signoffs.sc_id == 1).execute().fetchall() + self.assertEquals(len(r), 0) + + class TestUserRolesAPI_JSON(ViewTest): def testGetRoles(self): diff --git a/auslib/test/admin/views/test_releases.py b/auslib/test/admin/views/test_releases.py index f8d8591225..c840c08fbe 100644 --- a/auslib/test/admin/views/test_releases.py +++ b/auslib/test/admin/views/test_releases.py @@ -1,3 +1,4 @@ +import mock import simplejson as json from sqlalchemy import select @@ -1016,18 +1017,308 @@ def testReleasesPost(self): """)) -def byteify(input): - if isinstance(input, dict): - res = {} - for key, value in input.items(): - res[byteify(key)] = byteify(value) - return res - elif isinstance(input, list): - return [byteify(element) for element in input] - elif isinstance(input, unicode): - return input.encode('utf-8') - else: - return input +class TestReleasesScheduledChanges(ViewTest): + maxDiff = 10000 + + def setUp(self): + super(TestReleasesScheduledChanges, self).setUp() + dbo.releases.scheduled_changes.t.insert().execute( + sc_id=1, scheduled_by="bill", change_type="insert", data_version=1, base_name="m", base_product="m", + base_data=createBlob(dict(name="m", hashFunction="sha512", schema_version=1)) + ) + dbo.releases.scheduled_changes.history.t.insert().execute(change_id=1, changed_by="bill", timestamp=50, sc_id=1) + dbo.releases.scheduled_changes.history.t.insert().execute( + change_id=2, changed_by="bill", timestamp=51, sc_id=1, scheduled_by="bill", change_type="insert", data_version=1, base_name="m", + base_product="m", base_data=createBlob(dict(name="m", hashFunction="sha512", schema_version=1)) + ) + dbo.releases.scheduled_changes.signoffs.t.insert().execute(sc_id=1, username="bill", role="releng") + dbo.releases.scheduled_changes.conditions.t.insert().execute(sc_id=1, when=4000000000, data_version=1) + dbo.releases.scheduled_changes.conditions.history.t.insert().execute(change_id=1, changed_by="bill", timestamp=50, sc_id=1) + dbo.releases.scheduled_changes.conditions.history.t.insert().execute( + change_id=2, changed_by="bill", timestamp=51, sc_id=1, when=4000000000, data_version=1 + ) + + dbo.releases.scheduled_changes.t.insert().execute( + sc_id=2, scheduled_by="bill", change_type="update", data_version=1, base_name="c", base_product="c", + base_data=createBlob(dict(name="c", hashFunction="sha512", schema_version=1, extv="2.0")), base_data_version=1 + ) + dbo.releases.scheduled_changes.history.t.insert().execute(change_id=3, changed_by="bill", timestamp=70, sc_id=2) + dbo.releases.scheduled_changes.history.t.insert().execute( + change_id=4, changed_by="bill", timestamp=71, sc_id=2, scheduled_by="bill", change_type="update", data_version=1, base_name="c", + base_product="c", base_data=createBlob(dict(name="c", hashFunction="sha512", schema_version=1, extv="2.0")), base_data_version=1 + ) + dbo.releases.scheduled_changes.conditions.t.insert().execute(sc_id=2, when=6000000000, data_version=1) + dbo.releases.scheduled_changes.conditions.history.t.insert().execute(change_id=3, changed_by="bill", timestamp=70, sc_id=2) + dbo.releases.scheduled_changes.conditions.history.t.insert().execute( + change_id=4, changed_by="bill", timestamp=71, sc_id=2, when=6000000000, data_version=1 + ) + + dbo.releases.scheduled_changes.t.insert().execute( + sc_id=3, complete=True, scheduled_by="bill", change_type="update", data_version=2, base_name="b", base_product="b", + base_data=createBlob(dict(name="b", hashFunction="sha512", schema_version=1)), base_data_version=1 + ) + dbo.releases.scheduled_changes.history.t.insert().execute(change_id=5, changed_by="bill", timestamp=6, sc_id=3) + dbo.releases.scheduled_changes.history.t.insert().execute( + change_id=6, changed_by="bill", timestamp=7, sc_id=3, complete=False, scheduled_by="bill", change_type="update", data_version=1, base_name="b", + base_product="b", base_data=createBlob(dict(name="b", hashFunction="sha512", schema_version=1)), base_data_version=1 + ) + dbo.releases.scheduled_changes.history.t.insert().execute( + change_id=7, changed_by="bill", timestamp=25, sc_id=3, complete=True, change_type="update", scheduled_by="bill", data_version=2, base_name="b", + base_product="b", base_data=createBlob(dict(name="b", hashFunction="sha512", schema_version=1)), base_data_version=1 + ) + dbo.releases.scheduled_changes.conditions.t.insert().execute(sc_id=3, when=10000000, data_version=2) + dbo.releases.scheduled_changes.conditions.history.t.insert().execute(change_id=5, changed_by="bill", timestamp=6, sc_id=3) + dbo.releases.scheduled_changes.conditions.history.t.insert().execute( + change_id=6, changed_by="bill", timestamp=7, sc_id=3, when=10000000, data_version=1 + ) + dbo.releases.scheduled_changes.conditions.history.t.insert().execute( + change_id=7, changed_by="bill", timestamp=25, sc_id=3, when=10000000, data_version=2 + ) + + def testGetScheduledChanges(self): + ret = self._get("/scheduled_changes/releases") + expected = { + "count": 2, + "scheduled_changes": [ + { + "sc_id": 1, "when": 4000000000, "scheduled_by": "bill", "change_type": "insert", "complete": False, "sc_data_version": 1, + "name": "m", "product": "m", "data": {"name": "m", "hashFunction": "sha512", "schema_version": 1}, "read_only": False, + "data_version": None, "signoffs": {"bill": "releng"}, + }, + { + "sc_id": 2, "when": 6000000000, "scheduled_by": "bill", "change_type": "update", "complete": False, "sc_data_version": 1, + "name": "c", "product": "c", "data": {"name": "c", "hashFunction": "sha512", "schema_version": 1, "extv": "2.0"}, + "read_only": False, "data_version": 1, "signoffs": {}, + }, + ] + } + self.assertEquals(json.loads(ret.data), expected) + + def testGetScheduledChangesWithCompleted(self): + ret = self._get("/scheduled_changes/releases", qs={"all": 1}) + expected = { + "count": 3, + "scheduled_changes": [ + { + "sc_id": 1, "when": 4000000000, "scheduled_by": "bill", "change_type": "insert", "complete": False, "sc_data_version": 1, + "name": "m", "product": "m", "data": {"name": "m", "hashFunction": "sha512", "schema_version": 1}, "read_only": False, + "data_version": None, "signoffs": {"bill": "releng"}, + }, + { + "sc_id": 2, "when": 6000000000, "scheduled_by": "bill", "change_type": "update", "complete": False, "sc_data_version": 1, + "name": "c", "product": "c", "data": {"name": "c", "hashFunction": "sha512", "schema_version": 1, "extv": "2.0"}, + "read_only": False, "data_version": 1, "signoffs": {}, + }, + { + "sc_id": 3, "when": 10000000, "scheduled_by": "bill", "change_type": "update", "complete": True, "sc_data_version": 2, + "name": "b", "product": "b", "data": {"name": "b", "hashFunction": "sha512", "schema_version": 1}, "read_only": False, + "data_version": 1, "signoffs": {}, + }, + ] + } + self.assertEquals(json.loads(ret.data), expected) + + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testAddScheduledChangeExistingRelease(self): + data = { + "when": 2300000000, "name": "ab", "data": '{"name": "ab", "hashFunction": "sha256", "schema_version": 1}', + "product": "ab", "data_version": 1, "change_type": "update" + } + ret = self._post("/scheduled_changes/releases", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"sc_id": 4}) + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 4, "scheduled_by": "bill", "change_type": "update", "complete": False, "data_version": 1, "base_product": "ab", "base_read_only": False, + "base_name": "ab", "base_data": {"name": "ab", "hashFunction": "sha256", "schema_version": 1}, "base_data_version": 1 + } + self.assertEquals(db_data, expected) + cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 4, "data_version": 1, "when": 2300000000} + self.assertEquals(dict(cond[0]), cond_expected) + + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testAddScheduledChangeDeleteRelease(self): + data = { + "when": 4200000000, "name": "d", "data_version": 1, "change_type": "delete", + } + ret = self._post("/scheduled_changes/releases", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"sc_id": 4}) + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 4, "scheduled_by": "bill", "change_type": "delete", "complete": False, "data_version": 1, "base_product": None, "base_read_only": False, + "base_name": "d", "base_data": None, "base_data_version": 1, + } + self.assertEquals(db_data, expected) + cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 4, "data_version": 1, "when": 4200000000} + self.assertEquals(dict(cond[0]), cond_expected) + + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testAddScheduledChangeNewRelease(self): + data = { + "when": 5200000000, "name": "q", "data": '{"name": "q", "hashFunction": "sha512", "schema_version": 1}', + "product": "q", "change_type": "insert", + } + ret = self._post("/scheduled_changes/releases", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"sc_id": 4}) + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 4, "scheduled_by": "bill", "change_type": "insert", "complete": False, "data_version": 1, "base_product": "q", "base_read_only": False, + "base_name": "q", "base_data": {"name": "q", "hashFunction": "sha512", "schema_version": 1}, "base_data_version": None, + } + self.assertEquals(db_data, expected) + cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 4, "data_version": 1, "when": 5200000000} + self.assertEquals(dict(cond[0]), cond_expected) + + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testUpdateScheduledChangeExistingRelease(self): + data = { + "data": '{"name": "c", "hashFunction": "sha512", "extv": "3.0", "schema_version": 1}', "name": "c", "product": "c", + "data_version": 1, "sc_data_version": 1, "when": 78900000000, + } + ret = self._post("/scheduled_changes/releases/2", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"new_data_version": 2}) + + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 2).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 2, "complete": False, "change_type": "update", "data_version": 2, "scheduled_by": "bill", "base_name": "c", "base_product": "c", + "base_read_only": False, "base_data": {"name": "c", "hashFunction": "sha512", "extv": "3.0", "schema_version": 1}, + "base_data_version": 1, + } + self.assertEquals(db_data, expected) + cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 2).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 2, "data_version": 2, "when": 78900000000} + self.assertEquals(dict(cond[0]), cond_expected) + + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testUpdateScheduledChangeNewRelease(self): + data = { + "data": '{"name": "m", "hashFunction": "sha512", "appv": "4.0", "schema_version": 1}', "name": "m", "product": "m", + "sc_data_version": 1, + } + ret = self._post("/scheduled_changes/releases/1", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"new_data_version": 2}) + + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 1).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 1, "complete": False, "change_type": "insert", "data_version": 2, "scheduled_by": "bill", "base_name": "m", "base_product": "m", + "base_read_only": False, "base_data": {"name": "m", "hashFunction": "sha512", "appv": "4.0", "schema_version": 1}, + "base_data_version": None, + } + self.assertEquals(db_data, expected) + cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 1).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 1, "data_version": 2, "when": 4000000000} + self.assertEquals(dict(cond[0]), cond_expected) + + def testDeleteScheduledChange(self): + ret = self._delete("/scheduled_changes/releases/2", qs={"data_version": 1}) + self.assertEquals(ret.status_code, 200, ret.data) + got = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 2).execute().fetchall() + self.assertEquals(got, []) + cond_got = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 2).execute().fetchall() + self.assertEquals(cond_got, []) + + def testEnactScheduledChangeExistingRelease(self): + ret = self._post("/scheduled_changes/releases/2/enact") + self.assertEquals(ret.status_code, 200, ret.data) + + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 2).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 2, "complete": True, "data_version": 2, "scheduled_by": "bill", "change_type": "update", "base_name": "c", "base_product": "c", + "base_read_only": False, "base_data": {"name": "c", "hashFunction": "sha512", "schema_version": 1, "extv": "2.0"}, + "base_data_version": 1, + } + self.assertEquals(db_data, expected) + + base_row = dict(dbo.releases.t.select().where(dbo.releases.name == "c").execute().fetchall()[0]) + base_expected = { + "name": "c", "product": "c", "read_only": False, + "data": {"name": "c", "hashFunction": "sha512", "schema_version": 1, "extv": "2.0"}, "data_version": 2, + } + self.assertEquals(base_row, base_expected) + + def testEnactScheduledChangeNewRelease(self): + ret = self._post("/scheduled_changes/releases/1/enact") + self.assertEquals(ret.status_code, 200, ret.data) + + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 1).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 1, "complete": True, "data_version": 2, "scheduled_by": "bill", "change_type": "insert", "base_name": "m", "base_product": "m", + "base_read_only": False, "base_data": {"name": "m", "hashFunction": "sha512", "schema_version": 1}, + "base_data_version": None, + } + self.assertEquals(db_data, expected) + + base_row = dict(dbo.releases.t.select().where(dbo.releases.name == "m").execute().fetchall()[0]) + base_expected = { + "name": "m", "product": "m", "read_only": False, + "data": {"name": "m", "hashFunction": "sha512", "schema_version": 1}, "data_version": 1, + } + self.assertEquals(base_row, base_expected) + + def testGetScheduledChangeHistoryRevisions(self): + ret = self._get("/scheduled_changes/releases/3/revisions") + self.assertEquals(ret.status_code, 200, ret.data) + ret = json.loads(ret.data) + expected = { + "count": 2, + "revisions": [ + { + "change_id": 7, "changed_by": "bill", "timestamp": 25, "sc_id": 3, "scheduled_by": "bill", "change_type": "update", "data_version": 1, + "name": "b", "product": "b", "data": {"name": "b", "hashFunction": "sha512", "schema_version": 1}, "read_only": False, + "complete": True, "when": 10000000, "sc_data_version": 2, + }, + { + "change_id": 6, "changed_by": "bill", "timestamp": 7, "sc_id": 3, "scheduled_by": "bill", "change_type": "update", "data_version": 1, + "name": "b", "product": "b", "data": {"name": "b", "hashFunction": "sha512", "schema_version": 1}, "read_only": False, + "complete": False, "when": 10000000, "sc_data_version": 1, + }, + ], + } + self.assertEquals(ret, expected) + + def testSignoffWithRelease(self): + ret = self._post("/scheduled_changes/releases/2/signoffs", data=dict(role="qa"), username="bill") + self.assertEquals(ret.status_code, 200, ret.data) + r = dbo.releases.scheduled_changes.signoffs.t.select().where(dbo.releases.scheduled_changes.signoffs.sc_id == 1).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + self.assertEquals(db_data, {"sc_id": 1, "username": "bill", "role": "releng"}) + + def testSignoffWithoutRelease(self): + ret = self._post("/scheduled_changes/releases/2/signoffs", data=dict(role="relman"), username="bill") + self.assertEquals(ret.status_code, 403, ret.data) + + def testRevokeSignoff(self): + ret = self._delete("/scheduled_changes/releases/1/signoffs", username="bill") + self.assertEquals(ret.status_code, 200, ret.data) + r = dbo.releases.scheduled_changes.signoffs.t.select().where(dbo.releases.scheduled_changes.signoffs.sc_id == 1).execute().fetchall() + self.assertEquals(len(r), 0) class TestReleaseHistoryView(ViewTest): diff --git a/auslib/test/admin/views/test_rules.py b/auslib/test/admin/views/test_rules.py index 775235692f..7be218e671 100644 --- a/auslib/test/admin/views/test_rules.py +++ b/auslib/test/admin/views/test_rules.py @@ -760,10 +760,10 @@ def testGetScheduledChanges(self): "data_version": 1, "alias": None, "product": None, "channel": None, "buildID": None, "locale": None, "osVersion": None, "distribution": None, "fallbackMapping": None, "distVersion": None, "headerArchitecture": None, "comment": None, "whitelist": None, "systemCapabilities": None, "telemetry_product": None, "telemetry_channel": None, "telemetry_uptake": None, + "change_type": "update", "signoffs": { "bill": "releng", }, - "change_type": "update", }, { "sc_id": 2, "when": 1500000, "scheduled_by": "bill", "complete": False, "sc_data_version": 1, "rule_id": None, "priority": 50, @@ -771,8 +771,7 @@ def testGetScheduledChanges(self): "buildTarget": None, "alias": None, "channel": None, "buildID": None, "locale": None, "osVersion": None, "distribution": None, "fallbackMapping": None, "distVersion": None, "headerArchitecture": None, "comment": None, "whitelist": None, "data_version": None, "systemCapabilities": None, "telemetry_product": None, "telemetry_channel": None, "telemetry_uptake": None, - "signoffs": {}, - "change_type": "insert", + "change_type": "insert", "signoffs": {}, }, { "sc_id": 3, "when": 2900000, "scheduled_by": "bill", "complete": False, "sc_data_version": 2, "rule_id": None, "priority": 150, @@ -780,8 +779,7 @@ def testGetScheduledChanges(self): "buildTarget": None, "alias": None, "channel": None, "buildID": None, "locale": None, "osVersion": None, "distribution": None, "fallbackMapping": None, "distVersion": None, "headerArchitecture": None, "comment": None, "whitelist": None, "data_version": None, "systemCapabilities": None, "telemetry_product": None, "telemetry_channel": None, "telemetry_uptake": None, - "signoffs": {}, - "change_type": "insert", + "change_type": "insert", "signoffs": {}, }, ], } @@ -798,10 +796,10 @@ def testGetScheduledChangesWithCompleted(self): "data_version": 1, "alias": None, "product": None, "channel": None, "buildID": None, "locale": None, "osVersion": None, "distribution": None, "fallbackMapping": None, "distVersion": None, "headerArchitecture": None, "comment": None, "whitelist": None, "systemCapabilities": None, "telemetry_product": None, "telemetry_channel": None, "telemetry_uptake": None, + "change_type": "update", "signoffs": { "bill": "releng", }, - "change_type": "update", }, { "sc_id": 2, "when": 1500000, "scheduled_by": "bill", "complete": False, "sc_data_version": 1, "rule_id": None, "priority": 50, @@ -809,8 +807,7 @@ def testGetScheduledChangesWithCompleted(self): "buildTarget": None, "alias": None, "channel": None, "buildID": None, "locale": None, "osVersion": None, "distribution": None, "fallbackMapping": None, "distVersion": None, "headerArchitecture": None, "comment": None, "whitelist": None, "data_version": None, "systemCapabilities": None, "telemetry_product": None, "telemetry_channel": None, "telemetry_uptake": None, - "signoffs": {}, - "change_type": "insert", + "change_type": "insert", "signoffs": {}, }, { "sc_id": 3, "when": 2900000, "scheduled_by": "bill", "complete": False, "sc_data_version": 2, "rule_id": None, "priority": 150, @@ -818,8 +815,7 @@ def testGetScheduledChangesWithCompleted(self): "buildTarget": None, "alias": None, "channel": None, "buildID": None, "locale": None, "osVersion": None, "distribution": None, "fallbackMapping": None, "distVersion": None, "headerArchitecture": None, "comment": None, "whitelist": None, "data_version": None, "systemCapabilities": None, "telemetry_product": None, "telemetry_channel": None, "telemetry_uptake": None, - "signoffs": {}, - "change_type": "insert", + "change_type": "insert", "signoffs": {}, }, { "sc_id": 4, "when": 500000, "scheduled_by": "bill", "complete": True, "sc_data_version": 2, "rule_id": 5, "priority": 80, @@ -827,8 +823,7 @@ def testGetScheduledChangesWithCompleted(self): "data_version": 1, "alias": None, "product": None, "channel": None, "buildID": None, "locale": None, "osVersion": None, "distribution": None, "fallbackMapping": None, "distVersion": None, "headerArchitecture": None, "comment": None, "whitelist": None, "systemCapabilities": None, "telemetry_product": None, "telemetry_channel": None, "telemetry_uptake": None, - "signoffs": {}, - "change_type": "update", + "change_type": "update", "signoffs": {}, }, ], } @@ -1136,6 +1131,10 @@ def testRevertScheduledChangeChangeIdDoesntMatchScId(self): def testSignoffWithPermission(self): ret = self._post("/scheduled_changes/rules/2/signoffs", data=dict(role="qa"), username="bill") self.assertEquals(ret.status_code, 200, ret.data) + r = dbo.rules.scheduled_changes.signoffs.t.select().where(dbo.rules.scheduled_changes.signoffs.sc_id == 2).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + self.assertEquals(db_data, {"sc_id": 2, "username": "bill", "role": "qa"}) def testSignoffWithoutPermission(self): ret = self._post("/scheduled_changes/rules/2/signoffs", data=dict(role="relman"), username="bill") @@ -1144,6 +1143,10 @@ def testSignoffWithoutPermission(self): def testSignoffASecondTimeWithSameRole(self): ret = self._post("/scheduled_changes/rules/1/signoffs", data=dict(role="releng"), username="bill") self.assertEquals(ret.status_code, 200, ret.data) + r = dbo.rules.scheduled_changes.signoffs.t.select().where(dbo.rules.scheduled_changes.signoffs.sc_id == 1).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + self.assertEquals(db_data, {"sc_id": 1, "username": "bill", "role": "releng"}) def testSignoffWithSecondRole(self): ret = self._post("/scheduled_changes/rules/1/signoffs", data=dict(role="qa"), username="bill") @@ -1152,6 +1155,8 @@ def testSignoffWithSecondRole(self): def testRevokeSignoff(self): ret = self._delete("/scheduled_changes/rules/1/signoffs", username="bill") self.assertEquals(ret.status_code, 200, ret.data) + r = dbo.rules.scheduled_changes.signoffs.t.select().where(dbo.rules.scheduled_changes.signoffs.sc_id == 1).execute().fetchall() + self.assertEquals(len(r), 0) def testRevokeOtherUsersSignoff(self): ret = self._delete("/scheduled_changes/rules/1/signoffs", username="bob") diff --git a/auslib/test/test_db.py b/auslib/test/test_db.py index ae2b930b76..8ca7a528d2 100644 --- a/auslib/test/test_db.py +++ b/auslib/test/test_db.py @@ -878,6 +878,11 @@ def noop(*args, **kwargs): what = {"foo": "newthing1", "when": 888000, "change_type": "insert"} self.assertRaises(MismatchedDataVersionError, self.sc_table.insert, changed_by="bob", **what) + @mock.patch("time.time", mock.MagicMock(return_value=200)) + def testInsertCreateExistingPK(self): + what = {"fooid": 3, "foo": "mine is better", "when": 99999999, "change_type": "insert"} + self.assertRaisesRegexp(ValueError, "Cannot schedule change for duplicate PK", self.sc_table.insert, changed_by="bob", **what) + @mock.patch("time.time", mock.MagicMock(return_value=200)) def testDeleteScheduledChangeWithoutPKColumns(self): class TestTable2(AUSTable): @@ -1311,7 +1316,7 @@ def testValidateConditionsTelemetryRaisesError(self): @mock.patch("time.time", mock.MagicMock(return_value=200)) def testInsertWithEnabledCondition(self): - what = {"fooid": 11, "foo": "i", "bar": "jjj", "data_version": 1, "when": 909000, "change_type": "insert"} + what = {"fooid": 11, "foo": "i", "bar": "jjj", "data_version": 1, "when": 909000, "change_type": "update"} self.sc_table.insert(changed_by="bob", **what) row = self.sc_table.t.select().where(self.sc_table.sc_id == 2).execute().fetchall()[0] cond_row = self.sc_table.conditions.t.select().where(self.sc_table.conditions.sc_id == 2).execute().fetchall()[0] @@ -1500,6 +1505,14 @@ def setUp(self): update_type="z", data_version=1) self.db.permissions.t.insert().execute(permission="admin", username="bill", data_version=1) + def testAllTablesCreated(self): + self.assertTrue(self.db.rules) + self.assertTrue(self.db.rules.history) + self.assertTrue(self.db.rules.scheduled_changes) + self.assertTrue(self.db.rules.scheduled_changes.history) + self.assertTrue(self.db.rules.scheduled_changes.conditions) + self.assertTrue(self.db.rules.scheduled_changes.conditions.history) + def testGetOrderedRules(self): rules = self._stripNullColumns(self.paths.getOrderedRules()) expected = [ @@ -1917,6 +1930,14 @@ def setUp(self): def tearDown(self): dbo.reset() + def testAllTablesCreated(self): + self.assertTrue(dbo.releases) + self.assertTrue(dbo.releases.history) + self.assertTrue(dbo.releases.scheduled_changes) + self.assertTrue(dbo.releases.scheduled_changes.history) + self.assertTrue(dbo.releases.scheduled_changes.conditions) + self.assertTrue(dbo.releases.scheduled_changes.conditions.history) + def testGetReleases(self): self.assertEquals(len(self.releases.getReleases()), 4) @@ -3055,6 +3076,14 @@ def setUp(self): self.user_roles.t.insert().execute(username="bob", role="dev", data_version=1) self.user_roles.t.insert().execute(username="cathy", role="releng", data_version=1) + def testAllTablesCreated(self): + self.assertTrue(self.db.permissions) + self.assertTrue(self.db.permissions.history) + self.assertTrue(self.db.permissions.scheduled_changes) + self.assertTrue(self.db.permissions.scheduled_changes.history) + self.assertTrue(self.db.permissions.scheduled_changes.conditions) + self.assertTrue(self.db.permissions.scheduled_changes.conditions.history) + def testPermissionsHasCorrectTablesAndColumns(self): columns = [c.name for c in self.permissions.t.get_children()] expected = ["username", "permission", "options", "data_version"] diff --git a/auslib/web/base.py b/auslib/web/base.py index 609b660f68..83d0986ee2 100644 --- a/auslib/web/base.py +++ b/auslib/web/base.py @@ -23,6 +23,7 @@ def heartbeat_database_function(dbo): # cause notable load, but will verify that the database works. return dbo.rules.countRules() + create_dockerflow_endpoints(app, heartbeat_database_function) diff --git a/docs/source/database.rst b/docs/source/database.rst index e5465ebe79..b60dab8bce 100644 --- a/docs/source/database.rst +++ b/docs/source/database.rst @@ -232,7 +232,7 @@ This allows us to look back in time when debugging issues, attribute changes to Scheduled Changes ----------------- -Some tables (only Rules currently) support having changes to them scheduled in advance. Tables with Scheduled Changes enabled will have additional related tables to store the necessary information about them. +Some tables (Rules, Releases, and Permissions) support having changes to them scheduled in advance. Tables with Scheduled Changes enabled will have additional related tables to store the necessary information about them. The primary Scheduled Changes table stores the desired new version of the object and the user who scheduled it. The Conditions table stores information about when to enact the Scheduled Change. Finally, the Signoffs table stores information about who (if anybody) has signed off on the Scheduled Change. All of these tables have their own History tables too. From 06e7fadbc0caf93aee1761c2295e9615e6328c74 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Mon, 2 Jan 2017 18:31:02 +0530 Subject: [PATCH 02/29] Initial commit --- .../release_scheduled_changes_controller.js | 157 ++++++++++++++++++ ...elease_scheduled_changes_new_controller.js | 108 ++++++++++++ ui/app/js/router.js | 6 + ui/app/js/services/releases_service.js | 41 ++++- ui/app/pages/index.us | 7 +- .../release_scheduled_change_modal.html | 68 ++++++++ .../templates/release_scheduled_changes.html | 129 ++++++++++++++ 7 files changed, 514 insertions(+), 2 deletions(-) create mode 100644 ui/app/js/controllers/release_scheduled_changes_controller.js create mode 100644 ui/app/js/controllers/release_scheduled_changes_new_controller.js create mode 100644 ui/app/templates/release_scheduled_change_modal.html create mode 100644 ui/app/templates/release_scheduled_changes.html diff --git a/ui/app/js/controllers/release_scheduled_changes_controller.js b/ui/app/js/controllers/release_scheduled_changes_controller.js new file mode 100644 index 0000000000..40ff0c681e --- /dev/null +++ b/ui/app/js/controllers/release_scheduled_changes_controller.js @@ -0,0 +1,157 @@ +angular.module("app").controller("ReleaseScheduledChangesController", +function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $route, Releases) { + + $scope.loading = true; + $scope.failed = false; + + Releases.getScheduledChanges() + .success(function(response) { + // "when" is a unix timestamp, but it's much easier to work with Date objects, + // so we convert it to that before rendering. + $scope.scheduled_changes = response.scheduled_changes.map(function(sc) { + if (sc.when !== null) { + sc.when = new Date(sc.when); + } + return sc; + }); + }) + .error(function() { + console.error(arguments); + $scope.failed = true; + }) + .finally(function() { + $scope.loading = false; + }); + + $scope.$watch("ordering_str", function(value) { + $scope.ordering = value.value.split(","); + }); + + $scope.ordering_options = [ + { + text: "When", + value: "when" + }, + { + text: "Product, Channel", + value: "product,channel" + }, + ]; + + $scope.ordering_str = $scope.ordering_options[0]; + + $scope.currentPage = 1; + $scope.pageSize = 10; + + $scope.state_filter = [ + { + text: "Active", + value: "active", + }, + { + text: "Completed", + value: "complete", + }, + ]; + $scope.state_str = $scope.state_filter[0]; + + $scope.filterBySelect = function(sc) { + if ($scope.state_str.value === "complete" && sc.complete) { + return true; + } + else if ($scope.state_str.value === "active" && !sc.complete) { + return true; + } + return false; + }; + + $scope.formatMoment = function(when) { + date = moment(when); + // This is copied from app/js/directives/moment_directive.js + // We can't use that for this page, because it doesn't re-render when + // values change. + return ''; + }; + + $scope.openNewScheduledReleaseChangeModal = function() { + + var modalInstance = $modal.open({ + templateUrl: 'release_scheduled_change_modal.html', + controller: 'NewReleaseScheduledChangeCtrl', + size: 'lg', + backdrop: 'static', + resolve: { + scheduled_changes: function() { + return $scope.scheduled_changes; + }, + sc: function() { + // blank new default rule + return { + name: '', + product: '', + change_type: 'insert', + }; + } + } + }); + }; + + $scope.openUpdateModal = function(sc) { + var modalInstance = $modal.open({ + templateUrl: "rule_scheduled_change_modal.html", + controller: "EditRuleScheduledChangeCtrl", + size: 'lg', + backdrop: 'static', + resolve: { + sc: function() { + sc.when = new Date(sc.when); + return sc; + } + } + }); + }; + + $scope.openDeleteModal = function(sc) { + var modalInstance = $modal.open({ + templateUrl: "rule_scheduled_change_delete_modal.html", + controller: "DeleteRuleScheduledChangeCtrl", + backdrop: 'static', + resolve: { + sc: function() { + return sc; + }, + scheduled_changes: function() { + return $scope.scheduled_changes; + } + } + }); + }; + + $scope.openReleaseDataModal = function(mapping) { + Releases.getRelease(mapping) + .success(function(response) { + // it's the same rule, but this works + var modalInstance = $modal.open({ + templateUrl: 'release_data_modal.html', + controller: 'ReleaseDataCtrl', + size: 'lg', + backdrop: 'static', + resolve: { + release: function () { + return response; + }, + diff: function() { + return false; + } + } + }); + }) + .error(function() { + console.error(arguments); + $scope.failed = true; + }) + .finally(function() { + $scope.loading = false; + }); + }; +}); diff --git a/ui/app/js/controllers/release_scheduled_changes_new_controller.js b/ui/app/js/controllers/release_scheduled_changes_new_controller.js new file mode 100644 index 0000000000..0b32ac13bf --- /dev/null +++ b/ui/app/js/controllers/release_scheduled_changes_new_controller.js @@ -0,0 +1,108 @@ +angular.module("app").controller("NewReleaseScheduledChangeCtrl", +function($scope, $http, $modalInstance, CSRF, Releases, scheduled_changes, sc) { + + + $scope.is_edit = false; + $scope.scheduled_changes = scheduled_changes; + $scope.sc = sc; + $scope.errors = {}; + $scope.saving = false; + $scope.calendar_is_open = false; + $scope.release = { + name: '', + product: '', + version: '' + }; + $scope.products = []; + Releases.getProducts().success(function(response) { + $scope.products = response.product; + }); + + + $scope.setWhen = function(newDate) { + if (!newDate) { + newDate = new Date($("#id_when")[0].value); + $scope.sc.when = newDate; + } + $scope.calendar_is_open = false; + if (newDate <= new Date()) { + $scope.errors.when = ["Scheduled time cannot be in the past"]; + $scope.sc.when = null; + } + else { + $scope.errors.when = null; + } + }; + + $scope.clearWhen = function () { + $scope.sc.when = null; + $scope.errors.when = null; + }; + $scope.fillName = function () { + var file = $scope.dataFile; + $scope.errors.data = []; + $scope.release.name = ""; + + var reader = new FileReader(); + reader.onloadend = function(evt) { + var blob = evt.target.result; + $scope.$apply( function() { + try{ + var name = JSON.parse(blob).name; + if (name) { + $scope.release.name = name; + } + else { + $scope.errors.data = ["Form submission error", "Missing name field in JSON blob.\n"]; + } + }catch(err) { + $scope.errors.data = ["Form submission error", "Malformed JSON file.\n"]; + } + }); + }; + if (typeof file !== 'undefined') { + // should work + reader.readAsText(file); + } + + }; + + $scope.changeName = function () { + //wait for actual file to be loaded + setTimeout($scope.fillName, 0); + }; + + $scope.saveChanges = function() { + $scope.saving = true; + + CSRF.getToken() + .then(function(csrf_token) { + sc = angular.copy($scope.sc); + + Releases.addScheduledChange(sc, csrf_token) + .success(function(response) { + $scope.sc.sc_data_version = 1; + $scope.sc.sc_id = response.sc_id; + $scope.scheduled_changes.push($scope.sc); + $modalInstance.close(); + }) + .error(function(response, status) { + if (typeof response === 'object') { + $scope.errors = response; + sweetAlert( + "Form submission error", + "See fields highlighted in red.", + "error" + ); + } + }) + .finally(function() { + $scope.saving = false; + }); + }); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; +}); diff --git a/ui/app/js/router.js b/ui/app/js/router.js index eed464f012..2e11902fc3 100644 --- a/ui/app/js/router.js +++ b/ui/app/js/router.js @@ -26,6 +26,12 @@ angular.module("app").config(function($routeProvider, $locationProvider) { reloadOnSearch: false }) + .when("/releases/scheduled_changes", { + templateUrl: "release_scheduled_changes.html", + controller: "ReleaseScheduledChangesController", + reloadOnSearch: false + }) + .when('/releases/:name', { templateUrl: 'releases.html', controller: 'ReleasesController', diff --git a/ui/app/js/services/releases_service.js b/ui/app/js/services/releases_service.js index 080ee02f40..3732e2d7cc 100644 --- a/ui/app/js/services/releases_service.js +++ b/ui/app/js/services/releases_service.js @@ -69,7 +69,46 @@ angular.module("app").factory('Releases', function($http, $q) { data.csrf_token = csrf_token; var url = '/api/releases/' + encodeURIComponent(name) + '/revisions'; return $http.post(url, data); - } + }, + + getScheduledChanges: function() { + return $http.get("/api/scheduled_changes/releases?all=1"); + }, + + getScheduledChange: function(sc_id) { + return $http.get("/api/scheduled_changes/releases/" + sc_id); + }, + + addScheduledChange: function(data, csrf_token) { + data = jQuery.extend({}, data); + if (data.when === null) { + data.when = ""; + } + else { + data.when = data.when.getTime(); + } + data.csrf_token = csrf_token; + return $http.post("/api/scheduled_changes/releases", data); + }, + + updateScheduledChange: function(sc_id, data, csrf_token) { + data = jQuery.extend({}, data); + if (data.when === null) { + data.when = ""; + } + else { + data.when = data.when.getTime(); + } + data.csrf_token = csrf_token; + return $http.post("/api/scheduled_changes/releases/" + sc_id, data); + }, + + deleteScheduledChange: function(sc_id, data, csrf_token) { + var url = "/api/scheduled_changes/releases/" + sc_id; + url += '?data_version=' + data.sc_data_version; + url += '&csrf_token=' + encodeURIComponent(csrf_token); + return $http.delete(url); + }, }; return service; diff --git a/ui/app/pages/index.us b/ui/app/pages/index.us index 626d1d3b67..df92d4c092 100644 --- a/ui/app/pages/index.us +++ b/ui/app/pages/index.us @@ -29,7 +29,12 @@
  • Scheduled Changes
  • -
  • Releases
  • +
  • Permissions
  • diff --git a/ui/app/templates/release_scheduled_change_modal.html b/ui/app/templates/release_scheduled_change_modal.html new file mode 100644 index 0000000000..53ff7fcf91 --- /dev/null +++ b/ui/app/templates/release_scheduled_change_modal.html @@ -0,0 +1,68 @@ + + + + diff --git a/ui/app/templates/release_scheduled_changes.html b/ui/app/templates/release_scheduled_changes.html new file mode 100644 index 0000000000..e6ab06588a --- /dev/null +++ b/ui/app/templates/release_scheduled_changes.html @@ -0,0 +1,129 @@ +
    + +

    + Scheduled Release Changes + + + ({{ filtered_items.length | number:0 }} + found) + + + + +

    + + +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    + +

    +
    + + + History +
    + + no product + {{ sc.product }} + : + no channel + {{ sc.channel }} +

    +
    +
    +
    +
    + Scheduled By: {{ sc.scheduled_by }} +
    +
    + When: +
    +
    + Telemetry Product: {{ sc.telemetry_product }} +
    +
    + Telemetry Channel: {{ sc.telemetry_channel }} +
    +
    + Telemetry Uptake: {{ sc.telemetry_uptake }} +
    +
    + Scheduled Change ID: {{ sc.sc_id }} +
    +
    + Data version: {{ sc.sc_data_version }} +
    +
    + +
    +
    Rule ID:
    +
    {{ sc.rule_id }}
    +
    New!
    +
    Data Version:
    +
    {{ sc.data_version }}
    +
    Mapping:
    +
    + {{ sc.mapping }} + + + View + +
    +
    Fallback Mapping:
    +
    {{ sc.fallbackMapping }} + + + View + +
    +
    Product:
    +
    {{ sc.backgroundRate }}
    +
    Data Version:
    +
    {{ sc.priority }}
    +
    Change Type:
    +
    {{ sc.change_type }}
    +
    + +
    + +
    + + +
    + +
    From a5a1c5f258ae17ce4f0f92f826d61771b7bbb2c2 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Tue, 3 Jan 2017 14:19:46 +0530 Subject: [PATCH 03/29] Added new scheduled release button --- .../release_scheduled_changes_controller.js | 10 +- ...elease_scheduled_changes_new_controller.js | 105 ++++++++++++------ .../release_scheduled_change_modal.html | 4 +- 3 files changed, 80 insertions(+), 39 deletions(-) diff --git a/ui/app/js/controllers/release_scheduled_changes_controller.js b/ui/app/js/controllers/release_scheduled_changes_controller.js index 40ff0c681e..095ef6b0e6 100644 --- a/ui/app/js/controllers/release_scheduled_changes_controller.js +++ b/ui/app/js/controllers/release_scheduled_changes_controller.js @@ -85,7 +85,7 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout return $scope.scheduled_changes; }, sc: function() { - // blank new default rule + // blank new default release return { name: '', product: '', @@ -98,8 +98,8 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout $scope.openUpdateModal = function(sc) { var modalInstance = $modal.open({ - templateUrl: "rule_scheduled_change_modal.html", - controller: "EditRuleScheduledChangeCtrl", + templateUrl: "release_scheduled_change_modal.html", + controller: "EditReleaseScheduledChangeCtrl", size: 'lg', backdrop: 'static', resolve: { @@ -113,8 +113,8 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout $scope.openDeleteModal = function(sc) { var modalInstance = $modal.open({ - templateUrl: "rule_scheduled_change_delete_modal.html", - controller: "DeleteRuleScheduledChangeCtrl", + templateUrl: "release_scheduled_change_delete_modal.html", + controller: "DeleteReleaseScheduledChangeCtrl", backdrop: 'static', resolve: { sc: function() { diff --git a/ui/app/js/controllers/release_scheduled_changes_new_controller.js b/ui/app/js/controllers/release_scheduled_changes_new_controller.js index 0b32ac13bf..2c51bfd248 100644 --- a/ui/app/js/controllers/release_scheduled_changes_new_controller.js +++ b/ui/app/js/controllers/release_scheduled_changes_new_controller.js @@ -8,11 +8,7 @@ function($scope, $http, $modalInstance, CSRF, Releases, scheduled_changes, sc) { $scope.errors = {}; $scope.saving = false; $scope.calendar_is_open = false; - $scope.release = { - name: '', - product: '', - version: '' - }; + $scope.products = []; Releases.getProducts().success(function(response) { $scope.products = response.product; @@ -41,7 +37,7 @@ function($scope, $http, $modalInstance, CSRF, Releases, scheduled_changes, sc) { $scope.fillName = function () { var file = $scope.dataFile; $scope.errors.data = []; - $scope.release.name = ""; + $scope.sc.name = ""; var reader = new FileReader(); reader.onloadend = function(evt) { @@ -50,7 +46,7 @@ function($scope, $http, $modalInstance, CSRF, Releases, scheduled_changes, sc) { try{ var name = JSON.parse(blob).name; if (name) { - $scope.release.name = name; + $scope.sc.name = name; } else { $scope.errors.data = ["Form submission error", "Missing name field in JSON blob.\n"]; @@ -72,37 +68,82 @@ function($scope, $http, $modalInstance, CSRF, Releases, scheduled_changes, sc) { setTimeout($scope.fillName, 0); }; - $scope.saveChanges = function() { + $scope.saveChanges = function () { + if (!$scope.sc.product.trim()) { + sweetAlert( + "Form Error", + "Product is required.", + "error" + ); + return; + } + if (!$scope.dataFile) { + sweetAlert( + "Form Error", + "No file has been selected.", + "error" + ); + return; + } + + if (!$scope.sc.name.trim()) { + sweetAlert( + "Form Error", + "Name is required", + "error" + ); + return; + } + $scope.saving = true; + $scope.errors = {}; - CSRF.getToken() - .then(function(csrf_token) { - sc = angular.copy($scope.sc); - - Releases.addScheduledChange(sc, csrf_token) - .success(function(response) { - $scope.sc.sc_data_version = 1; - $scope.sc.sc_id = response.sc_id; - $scope.scheduled_changes.push($scope.sc); - $modalInstance.close(); - }) - .error(function(response, status) { - if (typeof response === 'object') { - $scope.errors = response; - sweetAlert( - "Form submission error", - "See fields highlighted in red.", - "error" - ); - } - }) - .finally(function() { - $scope.saving = false; + var file = $scope.dataFile; + + var reader = new FileReader(); + reader.onload = function(evt) { + var blob = evt.target.result; + CSRF.getToken() + .then(function(csrf_token) { + var data = angular.copy($scope.sc); + data.blob = blob; + Releases.addScheduledChange(data, csrf_token) + .success(function(response){ + $scope.sc.sc_data_version = 1; + $scope.sc.sc_id = response.sc_id; + $scope.scheduled_changes.push($scope.sc); + $modalInstance.close(); + }) + .error(function(response){ + if (typeof response === 'object') { + $scope.errors = response; + sweetAlert( + "Form submission error", + "See fields highlighted in red.", + "error" + ); + } else if (typeof response === 'string') { + // quite possibly an error in the blob validation + sweetAlert( + "Form submission error", + "Unable to submit successfully.\n" + + "(" + response+ ")", + "error" + ); + } + }) + .finally(function() { + $scope.saving = false; + }); }); - }); + }; + // should work + reader.readAsText(file); + }; $scope.cancel = function () { $modalInstance.dismiss('cancel'); }; }); + diff --git a/ui/app/templates/release_scheduled_change_modal.html b/ui/app/templates/release_scheduled_change_modal.html index 53ff7fcf91..b18f0cf0df 100644 --- a/ui/app/templates/release_scheduled_change_modal.html +++ b/ui/app/templates/release_scheduled_change_modal.html @@ -42,12 +42,12 @@

    Releases Details

    - +

    {{ errors.name.join(', ') }}

    -

    {{ errors.product.join(', ') }}

    From bd872d2d615916b97df3a0ae793020852ee19e08 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Wed, 4 Jan 2017 07:06:16 +0530 Subject: [PATCH 04/29] New Release Scheduled change added --- ...elease_scheduled_changes_new_controller.js | 8 +++--- .../templates/release_scheduled_changes.html | 27 +++---------------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/ui/app/js/controllers/release_scheduled_changes_new_controller.js b/ui/app/js/controllers/release_scheduled_changes_new_controller.js index 2c51bfd248..3133f34854 100644 --- a/ui/app/js/controllers/release_scheduled_changes_new_controller.js +++ b/ui/app/js/controllers/release_scheduled_changes_new_controller.js @@ -8,7 +8,7 @@ function($scope, $http, $modalInstance, CSRF, Releases, scheduled_changes, sc) { $scope.errors = {}; $scope.saving = false; $scope.calendar_is_open = false; - + $scope.products = []; Releases.getProducts().success(function(response) { $scope.products = response.product; @@ -105,8 +105,8 @@ function($scope, $http, $modalInstance, CSRF, Releases, scheduled_changes, sc) { var blob = evt.target.result; CSRF.getToken() .then(function(csrf_token) { - var data = angular.copy($scope.sc); - data.blob = blob; + var data = $scope.sc; + data.data = blob; Releases.addScheduledChange(data, csrf_token) .success(function(response){ $scope.sc.sc_data_version = 1; @@ -127,7 +127,7 @@ function($scope, $http, $modalInstance, CSRF, Releases, scheduled_changes, sc) { sweetAlert( "Form submission error", "Unable to submit successfully.\n" + - "(" + response+ ")", + "(" + response + ")", "error" ); } diff --git a/ui/app/templates/release_scheduled_changes.html b/ui/app/templates/release_scheduled_changes.html index e6ab06588a..6174f32b7c 100644 --- a/ui/app/templates/release_scheduled_changes.html +++ b/ui/app/templates/release_scheduled_changes.html @@ -55,9 +55,6 @@

    no product {{ sc.product }} - : - no channel - {{ sc.channel }}

    @@ -86,30 +83,14 @@
    -
    Rule ID:
    -
    {{ sc.rule_id }}
    -
    New!
    +
    Data Version:
    {{ sc.data_version }}
    -
    Mapping:
    -
    - {{ sc.mapping }} - - - View - -
    -
    Fallback Mapping:
    -
    {{ sc.fallbackMapping }} - - - View - +
    +
    Product:
    -
    {{ sc.backgroundRate }}
    -
    Data Version:
    -
    {{ sc.priority }}
    +
    {{ sc.product }}
    Change Type:
    {{ sc.change_type }}
    From abce865acf37163fa09f073488650a9a1353add7 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Wed, 4 Jan 2017 17:27:00 +0530 Subject: [PATCH 05/29] adds scheduled change and scheduled delete buttons to releases --- ...ease_scheduled_change_delete_controller.js | 37 +++++ ...elease_scheduled_change_edit_controller.js | 144 ++++++++++++++++++ .../release_scheduled_changes_controller.js | 37 ++--- ...elease_scheduled_changes_new_controller.js | 2 +- .../release_scheduled_delete_controller.js | 86 +++++++++++ ui/app/js/controllers/releases_controller.js | 39 +++++ ...release_scheduled_change_delete_modal.html | 20 +++ .../release_scheduled_change_modal.html | 6 +- .../templates/release_scheduled_changes.html | 17 +-- .../release_scheduled_delete_modal.html | 45 ++++++ ui/app/templates/releases.html | 2 + 11 files changed, 395 insertions(+), 40 deletions(-) create mode 100644 ui/app/js/controllers/release_scheduled_change_delete_controller.js create mode 100644 ui/app/js/controllers/release_scheduled_change_edit_controller.js create mode 100644 ui/app/js/controllers/release_scheduled_delete_controller.js create mode 100644 ui/app/templates/release_scheduled_change_delete_modal.html create mode 100644 ui/app/templates/release_scheduled_delete_modal.html diff --git a/ui/app/js/controllers/release_scheduled_change_delete_controller.js b/ui/app/js/controllers/release_scheduled_change_delete_controller.js new file mode 100644 index 0000000000..505f99ff61 --- /dev/null +++ b/ui/app/js/controllers/release_scheduled_change_delete_controller.js @@ -0,0 +1,37 @@ +angular.module("app").controller("DeleteReleaseScheduledChangeCtrl", +function ($scope, $modalInstance, CSRF, Releases, sc, scheduled_changes) { + + $scope.sc = sc; + $scope.scheduled_changes = scheduled_changes; + $scope.saving = false; + + $scope.saveChanges = function () { + $scope.saving = true; + CSRF.getToken() + .then(function(csrf_token) { + Releases.deleteScheduledChange($scope.sc.sc_id, $scope.sc, csrf_token) + .success(function(response) { + $scope.scheduled_changes.splice($scope.scheduled_changes.indexOf($scope.sc), 1); + $modalInstance.close(); + }) + .error(function(response) { + if (typeof response === 'object') { + sweetAlert( + { + title: "Form submission error", + text: response.exception + }, + function() { $scope.cancel(); } + ); + } + }) + .finally(function() { + $scope.saving = false; + }); + }); + }; + + $scope.cancel = function () { + $modalInstance.dismiss("cancel"); + }; +}); diff --git a/ui/app/js/controllers/release_scheduled_change_edit_controller.js b/ui/app/js/controllers/release_scheduled_change_edit_controller.js new file mode 100644 index 0000000000..7e4c45fba5 --- /dev/null +++ b/ui/app/js/controllers/release_scheduled_change_edit_controller.js @@ -0,0 +1,144 @@ + +angular.module('app').controller('EditReleaseScheduledChangeCtrl', +function ($scope, $modalInstance, CSRF, Releases, sc) { + + $scope.is_edit = true; + $scope.original_sc = sc; + $scope.sc = angular.copy(sc); + $scope.products = []; + Releases.getProducts().success(function(response) { + $scope.products = response.product; + }); + + $scope.errors = {}; + $scope.saving = false; + + $scope.setWhen = function(newDate) { + if (!newDate) { + newDate = new Date($("#id_when")[0].value); + $scope.sc.when = newDate; + } + $scope.calendar_is_open = false; + if (newDate <= new Date()) { + $scope.errors.when = ["Scheduled time cannot be in the past"]; + $scope.sc.when = $scope.original_sc.when; + } + else { + $scope.errors.when = null; + } + }; + + $scope.clearWhen = function () { + $scope.sc.when = null; + $scope.errors.when = null; + }; + + $scope.fillName = function () { + var file = $scope.dataFile; + $scope.errors.data = []; + var reader = new FileReader(); + reader.onloadend = function(evt) { + var blob = evt.target.result; + $scope.$apply( function() { + try { + var name = JSON.parse(blob).name; + if(!name) { + $scope.errors.data = ["Form submission error", "Name missing in blob.\n"]; + } + else if (name !== $scope.sc.name) { + $scope.errors.data = ["Form submission error", "Name differs compared to name in blob.\n"]; + } + }catch(err) { + $scope.errors.data = ["Form submission error", "Malformed JSON file.\n"]; + } + }); + }; + if (typeof file !== 'undefined') { + // should work + reader.readAsText(file); + } + + }; + + $scope.changeName = function () { + //wait for actual file to be loaded + setTimeout($scope.fillName, 0); + }; + + $scope.saveChanges = function () { + if (!$scope.sc.product.trim()) { + sweetAlert( + "Form Error", + "Product is required.", + "error" + ); + return; + } + if (!$scope.dataFile) { + sweetAlert( + "Form Error", + "No file has been selected.", + "error" + ); + return; + } + if (!$scope.sc.name.trim()) { + sweetAlert( + "Form Error", + "Name is required", + "error" + ); + return; + } + + $scope.saving = true; + + var file = $scope.dataFile; + + var reader = new FileReader(); + reader.onload = function(evt) { + var blob = evt.target.result; + + CSRF.getToken() + .then(function(csrf_token) { + var data = $scope.sc; + data.data = blob; + Releases.updateScheduledChange($scope.sc.sc_id, data, csrf_token) + .success(function(response) { + $scope.sc.data_version = response.new_data_version; + angular.copy($scope.sc, $scope.original_sc); + $scope.saving = false; + $modalInstance.close(); + }) + .error(function(response) { + if (typeof response === 'object') { + $scope.errors = response; + sweetAlert( + "Form submission error", + "See fields highlighted in red.", + "error" + ); + } else if (typeof response === 'string') { + // quite possibly an error in the blob validation + sweetAlert( + "Form submission error", + "Unable to submit successfully.\n" + + "(" + response+ ")", + "error" + ); + } + }) + .finally(function() { + $scope.saving = false; + }); + }); + }; + // should work + reader.readAsText(file); + + }; // /saveChanges + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; +}); diff --git a/ui/app/js/controllers/release_scheduled_changes_controller.js b/ui/app/js/controllers/release_scheduled_changes_controller.js index 095ef6b0e6..9df6770386 100644 --- a/ui/app/js/controllers/release_scheduled_changes_controller.js +++ b/ui/app/js/controllers/release_scheduled_changes_controller.js @@ -126,32 +126,21 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout } }); }; + $scope.openDataModal = function(release) { - $scope.openReleaseDataModal = function(mapping) { - Releases.getRelease(mapping) - .success(function(response) { - // it's the same rule, but this works - var modalInstance = $modal.open({ - templateUrl: 'release_data_modal.html', - controller: 'ReleaseDataCtrl', - size: 'lg', - backdrop: 'static', - resolve: { - release: function () { - return response; - }, - diff: function() { - return false; - } + var modalInstance = $modal.open({ + templateUrl: 'release_data_modal.html', + controller: 'ReleaseDataCtrl', + size: 'lg', + backdrop: 'static', + resolve: { + release: function () { + return release; + }, + diff: function() { + return false; } - }); - }) - .error(function() { - console.error(arguments); - $scope.failed = true; - }) - .finally(function() { - $scope.loading = false; + } }); }; }); diff --git a/ui/app/js/controllers/release_scheduled_changes_new_controller.js b/ui/app/js/controllers/release_scheduled_changes_new_controller.js index 3133f34854..0c7da384d3 100644 --- a/ui/app/js/controllers/release_scheduled_changes_new_controller.js +++ b/ui/app/js/controllers/release_scheduled_changes_new_controller.js @@ -68,7 +68,7 @@ function($scope, $http, $modalInstance, CSRF, Releases, scheduled_changes, sc) { setTimeout($scope.fillName, 0); }; - $scope.saveChanges = function () { + $scope.saveChanges = function () { if (!$scope.sc.product.trim()) { sweetAlert( "Form Error", diff --git a/ui/app/js/controllers/release_scheduled_delete_controller.js b/ui/app/js/controllers/release_scheduled_delete_controller.js new file mode 100644 index 0000000000..e2b5c2a54e --- /dev/null +++ b/ui/app/js/controllers/release_scheduled_delete_controller.js @@ -0,0 +1,86 @@ +angular.module("app").controller("NewReleaseScheduledDeleteCtrl", +function($scope, $http, $modalInstance, CSRF, Releases, scheduled_changes, sc) { + + + $scope.is_edit = false; + $scope.scheduled_changes = scheduled_changes; + $scope.sc = sc; + $scope.errors = {}; + $scope.saving = false; + $scope.calendar_is_open = false; + + + + $scope.setWhen = function(newDate) { + if (!newDate) { + newDate = new Date($("#id_when")[0].value); + $scope.sc.when = newDate; + } + $scope.calendar_is_open = false; + if (newDate <= new Date()) { + $scope.errors.when = ["Scheduled time cannot be in the past"]; + $scope.sc.when = null; + } + else { + $scope.errors.when = null; + } + }; + + $scope.clearWhen = function () { + $scope.sc.when = null; + $scope.errors.when = null; + }; + + + $scope.changeName = function () { + //wait for actual file to be loaded + setTimeout($scope.fillName, 0); + }; + + $scope.saveChanges = function () { + + $scope.saving = true; + $scope.errors = {}; + + + CSRF.getToken() + .then(function(csrf_token) { + var data = $scope.sc; + Releases.addScheduledChange(data, csrf_token) + .success(function(response){ + $scope.sc.sc_data_version = 1; + $scope.sc.sc_id = response.sc_id; + $scope.scheduled_changes.push($scope.sc); + $modalInstance.close(); + }) + .error(function(response){ + if (typeof response === 'object') { + $scope.errors = response; + sweetAlert( + "Form submission error", + "See fields highlighted in red.", + "error" + ); + } else if (typeof response === 'string') { + // quite possibly an error in the blob validation + sweetAlert( + "Form submission error", + "Unable to submit successfully.\n" + + "(" + response + ")", + "error" + ); + } + }) + .finally(function() { + $scope.saving = false; + }); + }); + }; + // should work + + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; +}); + diff --git a/ui/app/js/controllers/releases_controller.js b/ui/app/js/controllers/releases_controller.js index a869435cae..9777ebb5c6 100644 --- a/ui/app/js/controllers/releases_controller.js +++ b/ui/app/js/controllers/releases_controller.js @@ -177,6 +177,45 @@ function($scope, $routeParams, $location, $timeout, Releases, Search, $modal) { }; /* End openUpdateModal */ + $scope.openNewScheduledDeleteModal = function(release) { + + var modalInstance = $modal.open({ + templateUrl: 'release_scheduled_delete_modal.html', + controller: 'NewReleaseScheduledDeleteCtrl', + size: 'lg', + resolve: { + scheduled_changes: function() { + return []; + }, + sc: function() { + sc = angular.copy(release); + sc["change_type"] = "delete"; + return sc; + } + } + }); + }; + + $scope.openNewScheduledReleaseChangeModal = function(release) { + + var modalInstance = $modal.open({ + templateUrl: 'release_scheduled_change_modal.html', + controller: 'NewReleaseScheduledChangeCtrl', + size: 'lg', + backdrop: 'static', + resolve: { + scheduled_changes: function() { + return []; + }, + sc: function() { + sc = angular.copy(release); + sc["change_type"] = "update"; + return sc; + } + } + }); + }; + $scope.openDeleteModal = function(release) { var modalInstance = $modal.open({ diff --git a/ui/app/templates/release_scheduled_change_delete_modal.html b/ui/app/templates/release_scheduled_change_delete_modal.html new file mode 100644 index 0000000000..81c7684727 --- /dev/null +++ b/ui/app/templates/release_scheduled_change_delete_modal.html @@ -0,0 +1,20 @@ + + + diff --git a/ui/app/templates/release_scheduled_change_modal.html b/ui/app/templates/release_scheduled_change_modal.html index b18f0cf0df..1e95d062a3 100644 --- a/ui/app/templates/release_scheduled_change_modal.html +++ b/ui/app/templates/release_scheduled_change_modal.html @@ -33,19 +33,19 @@

    Scheduled Change Details


    Releases Details

    -
    +
    • {{ error }}
    -
    +

    {{ errors.name.join(', ') }}

    -
    +
    diff --git a/ui/app/templates/release_scheduled_changes.html b/ui/app/templates/release_scheduled_changes.html index 6174f32b7c..dd6b12dd46 100644 --- a/ui/app/templates/release_scheduled_changes.html +++ b/ui/app/templates/release_scheduled_changes.html @@ -50,11 +50,10 @@

    - History + History
    - no product - {{ sc.product }} + {{ sc.name }}

    @@ -65,15 +64,7 @@
    When:
    -
    - Telemetry Product: {{ sc.telemetry_product }} -
    -
    - Telemetry Channel: {{ sc.telemetry_channel }} -
    -
    - Telemetry Uptake: {{ sc.telemetry_uptake }} -
    +
    Scheduled Change ID: {{ sc.sc_id }}
    @@ -93,6 +84,8 @@
    {{ sc.product }}
    Change Type:
    {{ sc.change_type }}
    +
    Data:
    +
    diff --git a/ui/app/templates/release_scheduled_delete_modal.html b/ui/app/templates/release_scheduled_delete_modal.html new file mode 100644 index 0000000000..3a86332f8e --- /dev/null +++ b/ui/app/templates/release_scheduled_delete_modal.html @@ -0,0 +1,45 @@ + + + diff --git a/ui/app/templates/releases.html b/ui/app/templates/releases.html index 1bec3e19ab..2486c41d85 100644 --- a/ui/app/templates/releases.html +++ b/ui/app/templates/releases.html @@ -72,6 +72,8 @@

    Current + + Download From 9e29e69eec6ecf8522e8001b2633027714e530ae Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Thu, 5 Jan 2017 15:41:02 +0530 Subject: [PATCH 06/29] Reloving merge conflicts --- agent/tox.ini | 4 +- auslib/admin/views/forms.py | 66 ++++++++--- auslib/db.py | 55 +++++---- auslib/test/admin/views/test_permissions.py | 95 +++++++++++---- auslib/test/admin/views/test_releases.py | 122 ++++++++++++++++---- auslib/test/test_db.py | 19 +++ auslib/test/web/test_client.py | 7 +- auslib/web/base.py | 14 ++- scripts/manage-db.py | 5 +- ui/app/js/controllers/rules_controller.js | 4 +- uwsgi/public.wsgi | 2 +- version.txt | 2 +- 12 files changed, 287 insertions(+), 108 deletions(-) diff --git a/agent/tox.ini b/agent/tox.ini index 107f761ce0..5b4110fae4 100644 --- a/agent/tox.ini +++ b/agent/tox.ini @@ -11,7 +11,9 @@ setenv = deps = asynctest flake8 - coverage +# Pin coverage to work around https://bitbucket.org/ned/coveragepy/issues/541/coverage-43-breaks-nosetest-with-coverage +# Long term fix: https://bugzilla.mozilla.org/show_bug.cgi?id=1326046 + coverage==4.2 pyflakes pytest pytest-catchlog diff --git a/auslib/admin/views/forms.py b/auslib/admin/views/forms.py index c756a6316f..ae0f1dd2f7 100644 --- a/auslib/admin/views/forms.py +++ b/auslib/admin/views/forms.py @@ -158,30 +158,53 @@ class ExistingPermissionForm(DbEditableForm): options = JSONStringField(None, 'Options') -class ScheduledChangeNewPermissionForm(ScheduledChangeTimeForm, NewPermissionForm): + +class ScheduledChangeNewPermissionForm(ScheduledChangeTimeForm): + """Permission and username are required when creating a new Permission, so they + must be provided when Scheduled a Change that does the same. Options may also be + provided.""" permission = StringField('Permission', validators=[Length(0, 50), InputRequired()]) username = StringField('Username', validators=[Length(0, 100), InputRequired()]) + options = JSONStringField(None, 'Options') change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) -class ScheduledChangeExistingPermissionForm(ScheduledChangeTimeForm, ExistingPermissionForm): +class ScheduledChangeExistingPermissionForm(ScheduledChangeTimeForm): + """Permissions and username are required when Scheduling a Change that changes + an existing Permission because they are needed to find that Permission. Options + may also be provided.""" permission = StringField('Permission', validators=[Length(0, 50), InputRequired()]) username = StringField('Username', validators=[Length(0, 100), InputRequired()]) + options = JSONStringField(None, 'Options') + data_version = IntegerField('data_version', validators=[InputRequired()], widget=HiddenInput()) change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) class ScheduledChangeDeletePermissionForm(ScheduledChangeTimeForm): - change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete', 'delete')]) + + """Permissions and username are required when Scheduling a Change that deletes + an existing Permission because they are needed to find that Permission.""" permission = StringField('Permission', validators=[Length(0, 50), InputRequired()]) username = StringField('Username', validators=[Length(0, 100), InputRequired()]) data_version = IntegerField('data_version', validators=[InputRequired()], widget=HiddenInput()) + change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete', 'delete')]) -class EditScheduledChangeNewPermissionForm(ScheduledChangeTimeForm, NewPermissionForm): +class EditScheduledChangeNewPermissionForm(ScheduledChangeTimeForm): + """When editing an existing Scheduled Change for a Permission, any field + may be changed.""" + permission = StringField('Permission', validators=[Length(0, 50), Optional()]) + username = StringField('Username', validators=[Length(0, 100), Optional()]) + options = JSONStringField(None, 'Options') sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) -class EditScheduledChangeExistingPermissionForm(ScheduledChangeTimeForm, ExistingPermissionForm): +class EditScheduledChangeExistingPermissionForm(ScheduledChangeTimeForm): + """When editing an existing Scheduled Change for a Permission only options may be + provided. Because edits are identified by sc_id (in the URL), permission and username + are not required (nor allowed, because they are PK fields).""" + options = JSONStringField(None, 'Options') + data_version = IntegerField('data_version', widget=HiddenInput()) sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) @@ -251,18 +274,17 @@ class ScheduledChangeExistingRuleForm(ScheduledChangeTimeForm, ScheduledChangeUp # URLs that contain them. Scheduled changes, on the other hand, are edited # through URLs that contain scheduled change IDs, so we need to include # the rule_id in the form when editing scheduled changes for rules. - change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) rule_id = IntegerField('Rule ID', validators=[InputRequired()]) + change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) class ScheduledChangeDeleteRuleForm(ScheduledChangeTimeForm, ScheduledChangeUptakeForm): """ ScheduledChangeDeletionForm includes all the PK columns ,ScheduledChangeForm columns and data version """ - change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete', 'delete')]) rule_id = IntegerField('Rule ID', validators=[InputRequired()]) data_version = IntegerField('data_version', validators=[InputRequired()], widget=HiddenInput()) - + change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete', 'delete')]) class EditScheduledChangeNewRuleForm(ScheduledChangeTimeForm, ScheduledChangeUptakeForm, RuleForm): sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) @@ -298,38 +320,48 @@ class ReadOnlyForm(Form): class ScheduledChangeNewReleaseForm(ScheduledChangeTimeForm): + """All Release fields (name, product, data) are required when creating + a new Release, so they must be provided when Scheduling a Change that + does the same.""" name = StringField('Name', validators=[InputRequired()]) product = StringField('Product', validators=[InputRequired()]) data = JSONStringField({}, 'Data', validators=[InputRequired()], widget=FileInput()) - data_version = IntegerField('data_version', widget=HiddenInput()) change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) class ScheduledChangeExistingReleaseForm(ScheduledChangeTimeForm): + """Name must be provided when Scheduling a Change that modifies an existing + Release so that we can identify it. Other Release fields (product, data) are + optional.""" name = StringField('Name', validators=[InputRequired()]) product = StringField('Product', validators=[Optional()]) data = JSONStringField({}, 'Data', validators=[Optional()], widget=FileInput()) - data_version = IntegerField('data_version', widget=HiddenInput()) + data_version = IntegerField('data_version', validators=[InputRequired()], widget=HiddenInput()) change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete'), ('delete')]) class ScheduledChangeDeleteReleaseForm(ScheduledChangeTimeForm): - change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete', 'delete')]) + """Name must be provided when Scheduling a Change that deletes an + existing Permission so that we can find it.""" name = StringField('Name', validators=[InputRequired()]) data_version = IntegerField('data_version', validators=[InputRequired()], widget=HiddenInput()) + change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete', 'delete')]) class EditScheduledChangeNewReleaseForm(ScheduledChangeTimeForm): - name = StringField('Name', validators=[InputRequired()]) - product = StringField('Product', validators=[InputRequired()]) - data = JSONStringField({}, 'Data', validators=[InputRequired()], widget=FileInput()) - data_version = IntegerField('data_version', widget=HiddenInput()) + """Any Release field may be changed when editing an Scheduled Change for a new + Release.""" + name = StringField('Name', validators=[Optional()]) + product = StringField('Product', validators=[Optional()]) + data = JSONStringField({}, 'Data', validators=[Optional()], widget=FileInput()) sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) class EditScheduledChangeExistingReleaseForm(ScheduledChangeTimeForm): - name = StringField('Name', validators=[InputRequired()]) - product = StringField('Product', validators=[Optional()]) + """Only data may be changed when editing an existing Scheduled Change for + a Release. Name cannot be changed because it is a PK field, and product + cannot be changed because it almost never makes sense to (and can be done + by deleting/recreating instead).""" data = JSONStringField({}, 'Data', validators=[Optional()], widget=FileInput()) data_version = IntegerField('data_version', widget=HiddenInput()) sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) diff --git a/auslib/db.py b/auslib/db.py index b9b24e788d..e6bd125675 100644 --- a/auslib/db.py +++ b/auslib/db.py @@ -971,13 +971,6 @@ def validate(self, base_columns, condition_columns, changed_by, sc_id=None, tran base_table_where = [] sc_table_where = [] - if base_columns["change_type"] == "delete": - for pk in self.base_primary_key: - if pk not in base_columns: - raise ValueError("Missing primary key column %s. PK values needed for deletion" % (pk)) - if base_columns[pk] is None: - raise ValueError("%s value found to be None. PK value can not be None for deletion" % (pk)) - for pk in self.base_primary_key: base_column = getattr(self.baseTable, pk) if pk in base_columns: @@ -1020,7 +1013,6 @@ def validate(self, base_columns, condition_columns, changed_by, sc_id=None, tran sc_table_where.append(self.complete == False) # noqa because we need to use == for sqlalchemy operator overloading to work if len(self.select(columns=[self.sc_id], where=sc_table_where)) > 0: raise ChangeScheduledError("Cannot scheduled a change for a row with one already scheduled") - self.log.debug("base_columns: %s" % (base_columns)) self.conditions.validate(condition_columns) self._checkBaseTablePermissions(base_table_where, base_columns, changed_by, transaction) @@ -1147,8 +1139,6 @@ def enactChange(self, sc_id, enacted_by, transaction=None): transaction=transaction ) - # If the scheduled change had a data version, it means the row already - # exists, and we need to use update() to enact it. if change_type == "delete": where = [] for col in self.base_primary_key: @@ -1792,28 +1782,35 @@ def localeExists(self, name, platform, locale, transaction=None): except KeyError: return False - def delete(self, where, changed_by, old_data_version, transaction=None, dryrun=False): - names = [] - for toDelete in self.select(where=where, columns=[self.name, self.product], transaction=transaction): - names.append(toDelete["name"]) - self._proceedIfNotReadOnly(toDelete["name"], transaction=transaction) - if not self.db.hasPermission(changed_by, "release", "delete", toDelete["product"], transaction): - raise PermissionDeniedError("%s is not allowed to delete releases for product %s" % (changed_by, toDelete["product"])) - - for name in names: - mapping_count = dbo.rules.t.count().where(dbo.rules.mapping == name).execute().fetchone()[0] - whitelist_count = dbo.rules.t.count().where(dbo.rules.whitelist == name).execute().fetchone()[0] - fallbackMapping_count = dbo.rules.t.count().where(dbo.rules.fallbackMapping == name).execute().fetchone()[0] - - if mapping_count > 0 or whitelist_count > 0 or fallbackMapping_count > 0: - msg = "%s has rules pointing to it. Hence it cannot be deleted." % (self.name) - raise ValueError(msg) + def isMappedTo(self, name, transaction=None): + if not transaction: + transaction = AUSTransaction(self.getEngine()) + mapping_count = transaction.execute(dbo.rules.t.count().where(dbo.rules.mapping == name)).fetchone()[0] + whitelist_count = transaction.execute(dbo.rules.t.count().where(dbo.rules.whitelist == name)).fetchone()[0] + fallbackMapping_count = transaction.execute(dbo.rules.t.count().where(dbo.rules.fallbackMapping == name)).fetchone()[0] + if mapping_count > 0 or whitelist_count > 0 or fallbackMapping_count > 0: + return True + + return False + + def delete(self, where, changed_by, old_data_version, transaction=None, dryrun=False): + release = self.select(where=where, columns=[self.name, self.product], transaction=transaction) + if len(release) != 1: + raise ValueError("Where clause must match exactly one release to delete.") + release = release[0] + + self._proceedIfNotReadOnly(release["name"], transaction=transaction) + if not self.db.hasPermission(changed_by, "release", "delete", release["product"], transaction): + raise PermissionDeniedError("%s is not allowed to delete releases for product %s" % (changed_by, release["product"])) + + if self.isMappedTo(release["name"], transaction): + msg = "%s has rules pointing to it. Hence it cannot be deleted." % (release["name"]) + raise ValueError(msg) super(Releases, self).delete(where=where, changed_by=changed_by, old_data_version=old_data_version, transaction=transaction, dryrun=dryrun) if not dryrun: - for name in names: - cache.invalidate("blob", name) - cache.invalidate("blob_version", name) + cache.invalidate("blob", release["name"]) + cache.invalidate("blob_version", release["name"]) def isReadOnly(self, name, limit=None, transaction=None): where = [self.name == name] diff --git a/auslib/test/admin/views/test_permissions.py b/auslib/test/admin/views/test_permissions.py index ed07e82203..7af84268d7 100644 --- a/auslib/test/admin/views/test_permissions.py +++ b/auslib/test/admin/views/test_permissions.py @@ -185,6 +185,11 @@ def setUp(self): base_permission="rule", base_username="janet", base_options={"products": ["foo"]}, ) dbo.permissions.scheduled_changes.signoffs.t.insert().execute(sc_id=1, username="bill", role="releng") + + dbo.permissions.scheduled_changes.signoffs.history.t.insert().execute(change_id=1, changed_by="bill", timestamp=30, sc_id=1, username="bill") + dbo.permissions.scheduled_changes.signoffs.history.t.insert().execute(change_id=2, changed_by="bill", timestamp=31, sc_id=1, + username="bill", role="releng") + dbo.permissions.scheduled_changes.conditions.t.insert().execute(sc_id=1, when=10000000, data_version=1) dbo.permissions.scheduled_changes.conditions.history.t.insert().execute(change_id=1, changed_by="bill", timestamp=20, sc_id=1) dbo.permissions.scheduled_changes.conditions.history.t.insert().execute( @@ -227,10 +232,26 @@ def setUp(self): change_id=7, changed_by="bill", timestamp=100, sc_id=3, when=30000000, data_version=2 ) + + dbo.permissions.scheduled_changes.t.insert().execute( + sc_id=4, scheduled_by="bill", change_type="delete", data_version=1, base_permission="scheduled_change", base_username="mary", + complete=False, base_data_version=1, + ) + dbo.permissions.scheduled_changes.history.t.insert().execute(change_id=8, changed_by="bill", timestamp=200, sc_id=4) + dbo.permissions.scheduled_changes.history.t.insert().execute( + change_id=9, changed_by="bill", timestamp=201, sc_id=4, scheduled_by="bill", change_type="delete", data_version=1, + base_permission="scheduled_change", base_username="mary", complete=False, + ) + dbo.permissions.scheduled_changes.conditions.t.insert().execute(sc_id=4, when=76000000, data_version=1) + dbo.permissions.scheduled_changes.conditions.history.t.insert().execute(change_id=8, changed_by="bill", timestamp=200, sc_id=4) + dbo.permissions.scheduled_changes.conditions.history.t.insert().execute( + change_id=9, changed_by="bill", timestamp=201, sc_id=4, when=76000000, data_version=1 + ) + def testGetScheduledChanges(self): ret = self._get("/scheduled_changes/permissions") expected = { - "count": 2, + "count": 3, "scheduled_changes": [ { "sc_id": 1, "when": 10000000, "scheduled_by": "bill", "change_type": "insert", "complete": False, "sc_data_version": 1, @@ -241,6 +262,11 @@ def testGetScheduledChanges(self): "sc_id": 2, "when": 20000000, "scheduled_by": "bill", "change_type": "update", "complete": False, "sc_data_version": 1, "permission": "release_locale", "username": "ashanti", "options": None, "data_version": 1, "signoffs": {}, }, + + { + "sc_id": 4, "when": 76000000, "scheduled_by": "bill", "change_type": "delete", "complete": False, "sc_data_version": 1, + "permission": "scheduled_change", "username": "mary", "options": None, "data_version": 1, "signoffs": {}, + }, ], } self.assertEquals(json.loads(ret.data), expected) @@ -248,7 +274,7 @@ def testGetScheduledChanges(self): def testGetScheduledChangesWithCompleted(self): ret = self._get("/scheduled_changes/permissions", qs={"all": 1}) expected = { - "count": 3, + "count": 4, "scheduled_changes": [ { "sc_id": 1, "when": 10000000, "scheduled_by": "bill", "change_type": "insert", "complete": False, "sc_data_version": 1, @@ -263,6 +289,11 @@ def testGetScheduledChangesWithCompleted(self): "sc_id": 3, "when": 30000000, "scheduled_by": "bill", "change_type": "insert", "complete": True, "sc_data_version": 2, "permission": "permission", "username": "bob", "options": None, "data_version": None, "signoffs": {}, }, + + { + "sc_id": 4, "when": 76000000, "scheduled_by": "bill", "change_type": "delete", "complete": False, "sc_data_version": 1, + "permission": "scheduled_change", "username": "mary", "options": None, "data_version": 1, "signoffs": {}, + }, ], } self.assertEquals(json.loads(ret.data), expected) @@ -274,18 +305,18 @@ def testAddScheduledChangeExistingPermission(self): } ret = self._post("/scheduled_changes/permissions", data=data) self.assertEquals(ret.status_code, 200, ret.data) - self.assertEquals(json.loads(ret.data), {"sc_id": 4}) - r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(json.loads(ret.data), {"sc_id": 5}) + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 5).execute().fetchall() self.assertEquals(len(r), 1) db_data = dict(r[0]) expected = { - "sc_id": 4, "scheduled_by": "bill", "change_type": "update", "complete": False, "data_version": 1, + "sc_id": 5, "scheduled_by": "bill", "change_type": "update", "complete": False, "data_version": 1, "base_permission": "rule", "base_username": "bob", "base_options": None, "base_data_version": 1, } self.assertEquals(db_data, expected) - cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 5).execute().fetchall() self.assertEquals(len(cond), 1) - cond_expected = {"sc_id": 4, "data_version": 1, "when": 400000000} + cond_expected = {"sc_id": 5, "data_version": 1, "when": 400000000} self.assertEquals(dict(cond[0]), cond_expected) @mock.patch("time.time", mock.MagicMock(return_value=300)) @@ -295,19 +326,18 @@ def testAddScheduledChangeNewPermission(self): } ret = self._post("/scheduled_changes/permissions", data=data) self.assertEquals(ret.status_code, 200, ret.data) - self.assertEquals(json.loads(ret.data), {"sc_id": 4}) - r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(json.loads(ret.data), {"sc_id": 5}) + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 5).execute().fetchall() self.assertEquals(len(r), 1) db_data = dict(r[0]) - db_data["base_options"] = db_data["base_options"] expected = { - "sc_id": 4, "scheduled_by": "bill", "change_type": "insert", "complete": False, "data_version": 1, + "sc_id": 5, "scheduled_by": "bill", "change_type": "insert", "complete": False, "data_version": 1, "base_permission": "release", "base_username": "jill", "base_options": {"products": ["a"]}, "base_data_version": None, } self.assertEquals(db_data, expected) - cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 5).execute().fetchall() self.assertEquals(len(cond), 1) - cond_expected = {"sc_id": 4, "data_version": 1, "when": 400000000} + cond_expected = {"sc_id": 5, "data_version": 1, "when": 400000000} self.assertEquals(dict(cond[0]), cond_expected) @mock.patch("time.time", mock.MagicMock(return_value=300)) @@ -317,18 +347,18 @@ def testAddScheduledChangeDeletePermission(self): } ret = self._post("/scheduled_changes/permissions", data=data) self.assertEquals(ret.status_code, 200, ret.data) - self.assertEquals(json.loads(ret.data), {"sc_id": 4}) - r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(json.loads(ret.data), {"sc_id": 5}) + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 5).execute().fetchall() self.assertEquals(len(r), 1) db_data = dict(r[0]) expected = { - "sc_id": 4, "scheduled_by": "bill", "change_type": "delete", "complete": False, "data_version": 1, + "sc_id": 5, "scheduled_by": "bill", "change_type": "delete", "complete": False, "data_version": 1, "base_permission": "build", "base_username": "ashanti", "base_options": None, "base_data_version": 1, } self.assertEquals(db_data, expected) - cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + cond = dbo.permissions.scheduled_changes.conditions.t.select().where(dbo.permissions.scheduled_changes.conditions.sc_id == 5).execute().fetchall() self.assertEquals(len(cond), 1) - cond_expected = {"sc_id": 4, "data_version": 1, "when": 400000000} + cond_expected = {"sc_id": 5, "data_version": 1, "when": 400000000} self.assertEquals(dict(cond[0]), cond_expected) @mock.patch("time.time", mock.MagicMock(return_value=300)) @@ -343,7 +373,6 @@ def testUpdateScheduledChangeExistingPermission(self): r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 2).execute().fetchall() self.assertEquals(len(r), 1) db_data = dict(r[0]) - db_data["base_options"] = db_data["base_options"] expected = { "sc_id": 2, "complete": False, "data_version": 2, "scheduled_by": "bill", "change_type": "update", "base_permission": "release_locale", "base_username": "ashanti", "base_options": {"products": ["Thunderbird"]}, "base_data_version": 1, @@ -366,7 +395,6 @@ def testUpdateScheduledChangeNewPermission(self): r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 1).execute().fetchall() self.assertEquals(len(r), 1) db_data = dict(r[0]) - db_data["base_options"] = db_data["base_options"] expected = { "sc_id": 1, "complete": False, "data_version": 2, "scheduled_by": "bill", "change_type": "insert", "base_permission": "rule", "base_username": "janet", "base_options": {"products": ["Firefox"]}, "base_data_version": None, @@ -413,7 +441,6 @@ def testEnactScheduledChangeNewPermission(self): r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 1).execute().fetchall() self.assertEquals(len(r), 1) db_data = dict(r[0]) - db_data["base_options"] = db_data["base_options"] expected = { "sc_id": 1, "complete": True, "data_version": 2, "scheduled_by": "bill", "change_type": "insert", "base_permission": "rule", "base_username": "janet", "base_options": {"products": ["foo"]}, "base_data_version": None, @@ -428,6 +455,24 @@ def testEnactScheduledChangeNewPermission(self): } self.assertEquals(dict(base_row), base_expected) + def testEnactScheduledChangeDeletePermission(self): + ret = self._post("/scheduled_changes/permissions/4/enact") + self.assertEquals(ret.status_code, 200, ret.data) + + r = dbo.permissions.scheduled_changes.t.select().where(dbo.permissions.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 4, "complete": True, "data_version": 2, "scheduled_by": "bill", "change_type": "delete", "base_permission": "scheduled_change", + "base_username": "mary", "base_options": None, "base_data_version": 1, + } + self.assertEquals(db_data, expected) + + base_row = dbo.permissions.t.select().where(dbo.permissions.username == "mary")\ + .where(dbo.permissions.permission == "scheduled_change")\ + .execute().fetchall() + self.assertEquals(len(base_row), 0) + def testGetScheduledChangeHistoryRevisions(self): ret = self._get("/scheduled_changes/permissions/3/revisions") self.assertEquals(ret.status_code, 200, ret.data) @@ -448,6 +493,8 @@ def testGetScheduledChangeHistoryRevisions(self): } self.assertEquals(json.loads(ret.data), expected) + + @mock.patch("time.time", mock.MagicMock(return_value=100)) def testSignoffWithPermission(self): ret = self._post("/scheduled_changes/permissions/2/signoffs", data=dict(role="qa"), username="bill") self.assertEquals(ret.status_code, 200, ret.data) @@ -456,6 +503,12 @@ def testSignoffWithPermission(self): db_data = dict(r[0]) self.assertEquals(db_data, {"sc_id": 2, "username": "bill", "role": "qa"}) + r = dbo.permissions.scheduled_changes.signoffs.history.t.select()\ + .where(dbo.permissions.scheduled_changes.signoffs.history.sc_id == 2).execute().fetchall() + self.assertEquals(len(r), 2) + self.assertEquals(dict(r[0]), {"change_id": 3, "changed_by": "bill", "timestamp": 99999, "sc_id": 2, "username": "bill", "role": None}) + self.assertEquals(dict(r[1]), {"change_id": 4, "changed_by": "bill", "timestamp": 100000, "sc_id": 2, "username": "bill", "role": "qa"}) + def testSignoffWithoutPermission(self): ret = self._post("/scheduled_changes/permissions/2/signoffs", data=dict(role="relman"), username="bill") self.assertEquals(ret.status_code, 403, ret.data) diff --git a/auslib/test/admin/views/test_releases.py b/auslib/test/admin/views/test_releases.py index c840c08fbe..9d3c49df6e 100644 --- a/auslib/test/admin/views/test_releases.py +++ b/auslib/test/admin/views/test_releases.py @@ -1032,6 +1032,9 @@ def setUp(self): base_product="m", base_data=createBlob(dict(name="m", hashFunction="sha512", schema_version=1)) ) dbo.releases.scheduled_changes.signoffs.t.insert().execute(sc_id=1, username="bill", role="releng") + dbo.releases.scheduled_changes.signoffs.history.t.insert().execute(change_id=1, changed_by="bill", timestamp=100, sc_id=1, username="bill") + dbo.releases.scheduled_changes.signoffs.history.t.insert().execute(change_id=2, changed_by="bill", timestamp=101, sc_id=1, + username="bill", role="releng") dbo.releases.scheduled_changes.conditions.t.insert().execute(sc_id=1, when=4000000000, data_version=1) dbo.releases.scheduled_changes.conditions.history.t.insert().execute(change_id=1, changed_by="bill", timestamp=50, sc_id=1) dbo.releases.scheduled_changes.conditions.history.t.insert().execute( @@ -1075,10 +1078,25 @@ def setUp(self): change_id=7, changed_by="bill", timestamp=25, sc_id=3, when=10000000, data_version=2 ) + + dbo.releases.scheduled_changes.t.insert().execute( + sc_id=4, complete=False, scheduled_by="bill", change_type="delete", data_version=1, base_name="ab", base_data_version=1, + ) + dbo.releases.scheduled_changes.history.t.insert().execute(change_id=8, changed_by="bill", timestamp=25, sc_id=4) + dbo.releases.scheduled_changes.history.t.insert().execute( + change_id=9, changed_by="bill", timestamp=26, sc_id=4, complete=False, scheduled_by="bill", change_type="delete", data_version=1, + base_name="ab", base_data_version=1 + ) + dbo.releases.scheduled_changes.conditions.t.insert().execute(sc_id=4, when=230000000, data_version=1) + dbo.releases.scheduled_changes.conditions.history.t.insert().execute(change_id=8, changed_by="bill", timestamp=25, sc_id=4) + dbo.releases.scheduled_changes.conditions.history.t.insert().execute( + change_id=9, changed_by="bill", timestamp=26, sc_id=4, when=230000000, data_version=1 + ) + def testGetScheduledChanges(self): ret = self._get("/scheduled_changes/releases") expected = { - "count": 2, + "count": 3, "scheduled_changes": [ { "sc_id": 1, "when": 4000000000, "scheduled_by": "bill", "change_type": "insert", "complete": False, "sc_data_version": 1, @@ -1090,6 +1108,11 @@ def testGetScheduledChanges(self): "name": "c", "product": "c", "data": {"name": "c", "hashFunction": "sha512", "schema_version": 1, "extv": "2.0"}, "read_only": False, "data_version": 1, "signoffs": {}, }, + + { + "sc_id": 4, "when": 230000000, "scheduled_by": "bill", "change_type": "delete", "complete": False, "sc_data_version": 1, + "name": "ab", "product": None, "data": None, "read_only": False, "data_version": 1, "signoffs": {}, + }, ] } self.assertEquals(json.loads(ret.data), expected) @@ -1097,7 +1120,7 @@ def testGetScheduledChanges(self): def testGetScheduledChangesWithCompleted(self): ret = self._get("/scheduled_changes/releases", qs={"all": 1}) expected = { - "count": 3, + "count": 4, "scheduled_changes": [ { "sc_id": 1, "when": 4000000000, "scheduled_by": "bill", "change_type": "insert", "complete": False, "sc_data_version": 1, @@ -1114,6 +1137,10 @@ def testGetScheduledChangesWithCompleted(self): "name": "b", "product": "b", "data": {"name": "b", "hashFunction": "sha512", "schema_version": 1}, "read_only": False, "data_version": 1, "signoffs": {}, }, + { + "sc_id": 4, "when": 230000000, "scheduled_by": "bill", "change_type": "delete", "complete": False, "sc_data_version": 1, + "name": "ab", "product": None, "data": None, "read_only": False, "data_version": 1, "signoffs": {}, + }, ] } self.assertEquals(json.loads(ret.data), expected) @@ -1121,23 +1148,23 @@ def testGetScheduledChangesWithCompleted(self): @mock.patch("time.time", mock.MagicMock(return_value=300)) def testAddScheduledChangeExistingRelease(self): data = { - "when": 2300000000, "name": "ab", "data": '{"name": "ab", "hashFunction": "sha256", "schema_version": 1}', - "product": "ab", "data_version": 1, "change_type": "update" + "when": 2300000000, "name": "d", "data": '{"name": "d", "hashFunction": "sha256", "schema_version": 1}', + "product": "d", "data_version": 1, "change_type": "update" } ret = self._post("/scheduled_changes/releases", data=data) self.assertEquals(ret.status_code, 200, ret.data) - self.assertEquals(json.loads(ret.data), {"sc_id": 4}) - r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(json.loads(ret.data), {"sc_id": 5}) + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 5).execute().fetchall() self.assertEquals(len(r), 1) db_data = dict(r[0]) expected = { - "sc_id": 4, "scheduled_by": "bill", "change_type": "update", "complete": False, "data_version": 1, "base_product": "ab", "base_read_only": False, - "base_name": "ab", "base_data": {"name": "ab", "hashFunction": "sha256", "schema_version": 1}, "base_data_version": 1 + "sc_id": 5, "scheduled_by": "bill", "change_type": "update", "complete": False, "data_version": 1, "base_product": "d", "base_read_only": False, + "base_name": "d", "base_data": {"name": "d", "hashFunction": "sha256", "schema_version": 1}, "base_data_version": 1 } self.assertEquals(db_data, expected) - cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 5).execute().fetchall() self.assertEquals(len(cond), 1) - cond_expected = {"sc_id": 4, "data_version": 1, "when": 2300000000} + cond_expected = {"sc_id": 5, "data_version": 1, "when": 2300000000} self.assertEquals(dict(cond[0]), cond_expected) @mock.patch("time.time", mock.MagicMock(return_value=300)) @@ -1147,18 +1174,18 @@ def testAddScheduledChangeDeleteRelease(self): } ret = self._post("/scheduled_changes/releases", data=data) self.assertEquals(ret.status_code, 200, ret.data) - self.assertEquals(json.loads(ret.data), {"sc_id": 4}) - r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(json.loads(ret.data), {"sc_id": 5}) + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 5).execute().fetchall() self.assertEquals(len(r), 1) db_data = dict(r[0]) expected = { - "sc_id": 4, "scheduled_by": "bill", "change_type": "delete", "complete": False, "data_version": 1, "base_product": None, "base_read_only": False, + "sc_id": 5, "scheduled_by": "bill", "change_type": "delete", "complete": False, "data_version": 1, "base_product": None, "base_read_only": False, "base_name": "d", "base_data": None, "base_data_version": 1, } self.assertEquals(db_data, expected) - cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 5).execute().fetchall() self.assertEquals(len(cond), 1) - cond_expected = {"sc_id": 4, "data_version": 1, "when": 4200000000} + cond_expected = {"sc_id": 5, "data_version": 1, "when": 4200000000} self.assertEquals(dict(cond[0]), cond_expected) @mock.patch("time.time", mock.MagicMock(return_value=300)) @@ -1169,24 +1196,24 @@ def testAddScheduledChangeNewRelease(self): } ret = self._post("/scheduled_changes/releases", data=data) self.assertEquals(ret.status_code, 200, ret.data) - self.assertEquals(json.loads(ret.data), {"sc_id": 4}) - r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(json.loads(ret.data), {"sc_id": 5}) + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 5).execute().fetchall() self.assertEquals(len(r), 1) db_data = dict(r[0]) expected = { - "sc_id": 4, "scheduled_by": "bill", "change_type": "insert", "complete": False, "data_version": 1, "base_product": "q", "base_read_only": False, + "sc_id": 5, "scheduled_by": "bill", "change_type": "insert", "complete": False, "data_version": 1, "base_product": "q", "base_read_only": False, "base_name": "q", "base_data": {"name": "q", "hashFunction": "sha512", "schema_version": 1}, "base_data_version": None, } self.assertEquals(db_data, expected) - cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 4).execute().fetchall() + cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 5).execute().fetchall() self.assertEquals(len(cond), 1) - cond_expected = {"sc_id": 4, "data_version": 1, "when": 5200000000} + cond_expected = {"sc_id": 5, "data_version": 1, "when": 5200000000} self.assertEquals(dict(cond[0]), cond_expected) @mock.patch("time.time", mock.MagicMock(return_value=300)) def testUpdateScheduledChangeExistingRelease(self): data = { - "data": '{"name": "c", "hashFunction": "sha512", "extv": "3.0", "schema_version": 1}', "name": "c", "product": "c", + "data": '{"name": "c", "hashFunction": "sha512", "extv": "3.0", "schema_version": 1}', "name": "c", "data_version": 1, "sc_data_version": 1, "when": 78900000000, } ret = self._post("/scheduled_changes/releases/2", data=data) @@ -1231,6 +1258,30 @@ def testUpdateScheduledChangeNewRelease(self): cond_expected = {"sc_id": 1, "data_version": 2, "when": 4000000000} self.assertEquals(dict(cond[0]), cond_expected) + @mock.patch("time.time", mock.MagicMock(return_value=300)) + def testUpdateScheduledChangeNewReleaseChangeName(self): + data = { + "data": '{"name": "mm", "hashFunction": "sha512", "appv": "4.0", "schema_version": 1}', "name": "mm", "product": "mm", + "sc_data_version": 1, + } + ret = self._post("/scheduled_changes/releases/1", data=data) + self.assertEquals(ret.status_code, 200, ret.data) + self.assertEquals(json.loads(ret.data), {"new_data_version": 2}) + + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 1).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 1, "complete": False, "change_type": "insert", "data_version": 2, "scheduled_by": "bill", "base_name": "mm", "base_product": "mm", + "base_read_only": False, "base_data": {"name": "mm", "hashFunction": "sha512", "appv": "4.0", "schema_version": 1}, + "base_data_version": None, + } + self.assertEquals(db_data, expected) + cond = dbo.releases.scheduled_changes.conditions.t.select().where(dbo.releases.scheduled_changes.conditions.sc_id == 1).execute().fetchall() + self.assertEquals(len(cond), 1) + cond_expected = {"sc_id": 1, "data_version": 2, "when": 4000000000} + self.assertEquals(dict(cond[0]), cond_expected) + def testDeleteScheduledChange(self): ret = self._delete("/scheduled_changes/releases/2", qs={"data_version": 1}) self.assertEquals(ret.status_code, 200, ret.data) @@ -1281,6 +1332,22 @@ def testEnactScheduledChangeNewRelease(self): } self.assertEquals(base_row, base_expected) + def testEnactScheduledChangeDeleteRelease(self): + ret = self._post("/scheduled_changes/releases/4/enact") + self.assertEquals(ret.status_code, 200, ret.data) + + r = dbo.releases.scheduled_changes.t.select().where(dbo.releases.scheduled_changes.sc_id == 4).execute().fetchall() + self.assertEquals(len(r), 1) + db_data = dict(r[0]) + expected = { + "sc_id": 4, "complete": True, "data_version": 2, "scheduled_by": "bill", "change_type": "delete", "base_name": "ab", "base_product": None, + "base_read_only": False, "base_data": None, "base_data_version": 1, + } + self.assertEquals(db_data, expected) + + base_row = dbo.releases.t.select().where(dbo.releases.name == "a").execute().fetchall() + self.assertEquals(len(base_row), 1) + def testGetScheduledChangeHistoryRevisions(self): ret = self._get("/scheduled_changes/releases/3/revisions") self.assertEquals(ret.status_code, 200, ret.data) @@ -1302,15 +1369,20 @@ def testGetScheduledChangeHistoryRevisions(self): } self.assertEquals(ret, expected) - def testSignoffWithRelease(self): + @mock.patch("time.time", mock.MagicMock(return_value=100)) + def testSignoffWithPermission(self): ret = self._post("/scheduled_changes/releases/2/signoffs", data=dict(role="qa"), username="bill") self.assertEquals(ret.status_code, 200, ret.data) - r = dbo.releases.scheduled_changes.signoffs.t.select().where(dbo.releases.scheduled_changes.signoffs.sc_id == 1).execute().fetchall() + r = dbo.releases.scheduled_changes.signoffs.t.select().where(dbo.releases.scheduled_changes.signoffs.sc_id == 2).execute().fetchall() self.assertEquals(len(r), 1) db_data = dict(r[0]) - self.assertEquals(db_data, {"sc_id": 1, "username": "bill", "role": "releng"}) + self.assertEquals(db_data, {"sc_id": 2, "username": "bill", "role": "qa"}) + r = dbo.releases.scheduled_changes.signoffs.history.t.select().where(dbo.releases.scheduled_changes.signoffs.history.sc_id == 2).execute().fetchall() + self.assertEquals(len(r), 2) + self.assertEquals(dict(r[0]), {"change_id": 3, "changed_by": "bill", "timestamp": 99999, "sc_id": 2, "username": "bill", "role": None}) + self.assertEquals(dict(r[1]), {"change_id": 4, "changed_by": "bill", "timestamp": 100000, "sc_id": 2, "username": "bill", "role": "qa"}) - def testSignoffWithoutRelease(self): + def testSignoffWithoutPermission(self): ret = self._post("/scheduled_changes/releases/2/signoffs", data=dict(role="relman"), username="bill") self.assertEquals(ret.status_code, 403, ret.data) diff --git a/auslib/test/test_db.py b/auslib/test/test_db.py index 8ca7a528d2..ed4ecd024e 100644 --- a/auslib/test/test_db.py +++ b/auslib/test/test_db.py @@ -764,6 +764,7 @@ def testInsertForExistingRow(self): sc_row = self.sc_table.t.select().where(self.sc_table.sc_id == 7).execute().fetchall()[0] cond_row = self.sc_table.conditions.t.select().where(self.sc_table.conditions.sc_id == 7).execute().fetchall()[0] self.assertEquals(sc_row.scheduled_by, "bob") + self.assertEquals(sc_row.change_type, "update") self.assertEquals(sc_row.data_version, 1) self.assertEquals(sc_row.base_fooid, 3) self.assertEquals(sc_row.base_foo, "thing") @@ -779,6 +780,7 @@ def testInsertForNewRow(self): sc_row = self.sc_table.t.select().where(self.sc_table.sc_id == 7).execute().fetchall()[0] cond_row = self.sc_table.conditions.t.select().where(self.sc_table.conditions.sc_id == 7).execute().fetchall()[0] self.assertEquals(sc_row.scheduled_by, "bob") + self.assertEquals(sc_row.change_type, "insert") self.assertEquals(sc_row.data_version, 1) self.assertEquals(sc_row.base_fooid, None) self.assertEquals(sc_row.base_foo, "newthing1") @@ -804,6 +806,7 @@ def __init__(self, db, metadata): sc_row = table.scheduled_changes.t.select().where(table.scheduled_changes.sc_id == 1).execute().fetchall()[0] cond_row = table.scheduled_changes.conditions.t.select().where(table.scheduled_changes.conditions.sc_id == 1).execute().fetchall()[0] self.assertEquals(sc_row.scheduled_by, "mary") + self.assertEquals(sc_row.change_type, "insert") self.assertEquals(sc_row.data_version, 1) self.assertEquals(sc_row.base_foo_name, "i'm a foo") self.assertEquals(sc_row.base_foo, "123") @@ -913,6 +916,7 @@ def testUpdateNoChangesSinceCreation(self): cond_row = self.sc_table.conditions.t.select().where(self.sc_table.conditions.sc_id == 1).execute().fetchall()[0] cond_history_row = self.sc_table.conditions.history.t.select().where(self.sc_table.conditions.history.sc_id == 1).execute().fetchall()[0] self.assertEquals(sc_row.scheduled_by, "bob") + self.assertEquals(sc_row.change_type, "update") self.assertEquals(sc_row.data_version, 2) self.assertEquals(sc_row.base_fooid, 1) self.assertEquals(sc_row.base_foo, "bb") @@ -920,6 +924,7 @@ def testUpdateNoChangesSinceCreation(self): self.assertEquals(sc_row.base_data_version, 1) self.assertEquals(sc_history_row.changed_by, "bob") self.assertEquals(sc_history_row.scheduled_by, "bob") + self.assertEquals(sc_history_row.change_type, "update") self.assertEquals(sc_history_row.data_version, 2) self.assertEquals(sc_history_row.base_fooid, 1) self.assertEquals(sc_history_row.base_foo, "bb") @@ -942,6 +947,7 @@ def testUpdateChangeIdAndDataVersionStayInSyncWithoutConditionsChange(self): cond_row = self.sc_table.conditions.t.select().where(self.sc_table.conditions.sc_id == 1).execute().fetchall()[0] cond_history_row = self.sc_table.conditions.history.t.select().where(self.sc_table.conditions.history.sc_id == 1).execute().fetchall()[0] self.assertEquals(sc_row.scheduled_by, "bob") + self.assertEquals(sc_row.change_type, "update") self.assertEquals(sc_row.data_version, 2) self.assertEquals(sc_row.base_fooid, 1) self.assertEquals(sc_row.base_foo, "bb") @@ -949,6 +955,7 @@ def testUpdateChangeIdAndDataVersionStayInSyncWithoutConditionsChange(self): self.assertEquals(sc_row.base_data_version, 1) self.assertEquals(sc_history_row.changed_by, "bob") self.assertEquals(sc_history_row.scheduled_by, "bob") + self.assertEquals(sc_history_row.change_type, "update") self.assertEquals(sc_history_row.data_version, 2) self.assertEquals(sc_history_row.base_fooid, 1) self.assertEquals(sc_history_row.base_foo, "bb") @@ -971,6 +978,7 @@ def testUpdateNoChangesSinceCreationWithDict(self): cond_row = self.sc_table.conditions.t.select().where(self.sc_table.conditions.sc_id == 1).execute().fetchall()[0] cond_history_row = self.sc_table.conditions.history.t.select().where(self.sc_table.conditions.history.sc_id == 1).execute().fetchall()[0] self.assertEquals(sc_row.scheduled_by, "bob") + self.assertEquals(sc_row.change_type, "update") self.assertEquals(sc_row.data_version, 2) self.assertEquals(sc_row.base_fooid, 1) self.assertEquals(sc_row.base_foo, "bb") @@ -978,6 +986,7 @@ def testUpdateNoChangesSinceCreationWithDict(self): self.assertEquals(sc_row.base_data_version, 1) self.assertEquals(sc_history_row.changed_by, "bob") self.assertEquals(sc_history_row.scheduled_by, "bob") + self.assertEquals(sc_history_row.change_type, "update") self.assertEquals(sc_history_row.data_version, 2) self.assertEquals(sc_history_row.base_fooid, 1) self.assertEquals(sc_history_row.base_foo, "bb") @@ -1075,6 +1084,7 @@ def testUpdateBaseTableNoConflictChangingToNull(self): # This should end up with the scheduled changed incorporating our new # value for "foo" as well as the new "bar" value. self.assertEquals(sc_row.scheduled_by, "bob") + self.assertEquals(sc_row.change_type, "update") self.assertEquals(sc_row.data_version, 2) self.assertEquals(sc_row.base_fooid, 2) self.assertEquals(sc_row.base_foo, "dd") @@ -1083,6 +1093,7 @@ def testUpdateBaseTableNoConflictChangingToNull(self): # ...As well as a new history table entry. self.assertEquals(sc_history_row.changed_by, "bob") self.assertEquals(sc_history_row.scheduled_by, "bob") + self.assertEquals(sc_history_row.change_type, "update") self.assertEquals(sc_history_row.data_version, 2) self.assertEquals(sc_history_row.base_fooid, 2) self.assertEquals(sc_history_row.base_foo, "dd") @@ -1112,12 +1123,14 @@ def testUpdateDeleteScheduledChange(self): cond_history_row = self.sc_table.conditions.history.t.select().where( self.sc_table.conditions.history.sc_id == 6).execute().fetchall()[0] self.assertEquals(sc_row.scheduled_by, "bob") + self.assertEquals(sc_row.change_type, "delete") self.assertEquals(sc_row.data_version, 2) self.assertEquals(sc_row.base_fooid, 1) self.assertEquals(sc_row.base_foo, "bb") self.assertEquals(sc_row.base_data_version, 1) self.assertEquals(sc_history_row.changed_by, "bob") self.assertEquals(sc_history_row.scheduled_by, "bob") + self.assertEquals(sc_history_row.change_type, "delete") self.assertEquals(sc_history_row.data_version, 2) self.assertEquals(sc_history_row.base_fooid, 1) self.assertEquals(sc_history_row.base_foo, "bb") @@ -1321,6 +1334,7 @@ def testInsertWithEnabledCondition(self): row = self.sc_table.t.select().where(self.sc_table.sc_id == 2).execute().fetchall()[0] cond_row = self.sc_table.conditions.t.select().where(self.sc_table.conditions.sc_id == 2).execute().fetchall()[0] self.assertEquals(row.scheduled_by, "bob") + self.assertEquals(row.change_type, "update") self.assertEquals(row.data_version, 1) self.assertEquals(row.base_fooid, 11) self.assertEquals(row.base_foo, "i") @@ -1344,6 +1358,7 @@ def testUpdateWithNewValueForEnabledCondition(self): history_row = self.sc_table.history.t.select().where(self.sc_table.history.sc_id == 1).execute().fetchall()[0] cond_history_row = self.sc_table.conditions.history.t.select().where(self.sc_table.conditions.history.sc_id == 1).execute().fetchall()[0] self.assertEquals(row.scheduled_by, "bob") + self.assertEquals(row.change_type, "update") self.assertEquals(row.data_version, 2) self.assertEquals(row.base_fooid, 10) self.assertEquals(row.base_foo, "h") @@ -1351,6 +1366,7 @@ def testUpdateWithNewValueForEnabledCondition(self): self.assertEquals(row.base_data_version, 1) self.assertEquals(history_row.changed_by, "bob") self.assertEquals(history_row.scheduled_by, "bob") + self.assertEquals(history_row.change_type, "update") self.assertEquals(history_row.data_version, 2) self.assertEquals(history_row.base_fooid, 10) self.assertEquals(history_row.base_foo, "h") @@ -2027,6 +2043,9 @@ def testDeleteRelease(self): release = self.releases.t.select().where(self.releases.name == 'a').execute().fetchall() self.assertEquals(release, []) + def testDeleteReleaseDontAllowMultiple(self): + self.assertRaises(ValueError, self.releases.delete, {"product": "a"}, changed_by="bill", old_data_version=1) + def testDeleteWithRuleMapping(self): self.releases.t.insert().execute(name='d', product='d', data=createBlob(dict(name="d", schema_version=1, hashFunction="sha512")), data_version=1) diff --git a/auslib/test/web/test_client.py b/auslib/test/web/test_client.py index 9008ce0dab..af8f6eeb02 100644 --- a/auslib/test/web/test_client.py +++ b/auslib/test/web/test_client.py @@ -918,12 +918,11 @@ def testEmptySnippetOn500(self): self.assertEqual('I break!', str(exc.exception.message)) def testSentryBadDataError(self): - with mock.patch("auslib.web.views.client.ClientRequestView.get") as m: + with mock.patch("auslib.web.views.client.ClientRequestView.get") as m, mock.patch("auslib.web.base.sentry") as sentry: m.side_effect = BadDataError("exterminate!") - with mock.patch("auslib.web.base.sentry") as sentry, self.assertRaises(Exception) as exc: - self.client.get("/update/4/b/1.0/1/p/l/a/a/a/a/1/update.xml") + ret = self.client.get("/update/4/b/1.0/1/p/l/a/a/a/a/1/update.xml") self.assertFalse(sentry.captureException.called) - self.assertEqual('exterminate!', str(exc.exception.message)) + self.assertEqual(ret.status_code, 400, ret.data) def testSentryRealError(self): with mock.patch("auslib.web.views.client.ClientRequestView.get") as m: diff --git a/auslib/web/base.py b/auslib/web/base.py index 83d0986ee2..e035019977 100644 --- a/auslib/web/base.py +++ b/auslib/web/base.py @@ -1,7 +1,7 @@ import logging log = logging.getLogger(__name__) -from flask import Flask, make_response, send_from_directory, abort +from flask import Flask, make_response, send_from_directory, abort, Response from raven.contrib.flask import Sentry @@ -38,13 +38,15 @@ def fourohfour(error): @app.errorhandler(Exception) def generic(error): """Deals with any unhandled exceptions. If the exception is not a - BadDataError, it will be sent to Sentry. Regardless of the exception, - a 500 response is returned.""" + BadDataError, it will be sent to Sentry, and a 400 will be returned, + because BadDataErrors are considered to be the client's fault. + Otherwise, the error is just re-raised (which causes a 500).""" - if not isinstance(error, BadDataError): - if sentry.client: - sentry.captureException() + if isinstance(error, BadDataError): + return Response(status=400, response=error.message) + if sentry.client: + sentry.captureException() raise error diff --git a/scripts/manage-db.py b/scripts/manage-db.py index 5785e7909f..458ed64830 100644 --- a/scripts/manage-db.py +++ b/scripts/manage-db.py @@ -116,8 +116,9 @@ def extract_active_data(url, dump_location='dump.sql'): )) popen( - _strip_multiple_spaces('%s %s dockerflow rules rules_history rules_scheduled_changes rules_scheduled_changes_history \ - migrate_version > %s' % (mysql_default_command, database, dump_location,)) + _strip_multiple_spaces('%s %s dockerflow rules rules_history rules_scheduled_changes rules_scheduled_changes_conditions \ + rules_scheduled_changes_conditions_history rules_scheduled_changes_signoffs rules_scheduled_changes_signoffs_history \ + rules_scheduled_changes_history migrate_version > %s' % (mysql_default_command, database, dump_location,)) ) popen( diff --git a/ui/app/js/controllers/rules_controller.js b/ui/app/js/controllers/rules_controller.js index a01fd47fb3..af74cad8d1 100644 --- a/ui/app/js/controllers/rules_controller.js +++ b/ui/app/js/controllers/rules_controller.js @@ -130,7 +130,9 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout $scope.removeFilterSearchWord = Search.removeFilterSearchWord; $scope.filterBySelect = function(rule) { - if ($scope.pr_ch_selected[0].toLowerCase() === "all rules") { + // Always return all entries if "all rules" is the filter + // or if $scope.rule_id is set (meaning we're on the history page). + if ($scope.pr_ch_selected[0].toLowerCase() === "all rules" || $scope.rule_id) { return true; } else if ($scope.pr_ch_selected && $scope.pr_ch_selected.length > 1) { diff --git a/uwsgi/public.wsgi b/uwsgi/public.wsgi index a0d72888b4..c669931dca 100644 --- a/uwsgi/public.wsgi +++ b/uwsgi/public.wsgi @@ -55,7 +55,7 @@ application.config["VERSION_FILE"] = "/app/version.json" if os.environ.get('SENTRY_DSN'): application.config['SENTRY_DSN'] = os.environ.get('SENTRY_DSN') from auslib.web.base import sentry - sentry.init_app(application) + sentry.init_app(application, register_signal=False) if os.environ.get("CACHE_CONTROL"): application.config["CACHE_CONTROL"] = os.environ["CACHE_CONTROL"] diff --git a/version.txt b/version.txt index e3d0696453..5c6fb54899 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.15 +2.17 From dcf7ee51999fac9e54cd7859148ff33154db044a Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Thu, 5 Jan 2017 15:48:21 +0530 Subject: [PATCH 07/29] Removing blank lines --- auslib/admin/views/forms.py | 2 +- auslib/db.py | 1 - auslib/test/admin/views/test_permissions.py | 3 --- auslib/test/admin/views/test_releases.py | 2 -- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/auslib/admin/views/forms.py b/auslib/admin/views/forms.py index ae0f1dd2f7..3e9e92cc49 100644 --- a/auslib/admin/views/forms.py +++ b/auslib/admin/views/forms.py @@ -158,7 +158,6 @@ class ExistingPermissionForm(DbEditableForm): options = JSONStringField(None, 'Options') - class ScheduledChangeNewPermissionForm(ScheduledChangeTimeForm): """Permission and username are required when creating a new Permission, so they must be provided when Scheduled a Change that does the same. Options may also be @@ -286,6 +285,7 @@ class ScheduledChangeDeleteRuleForm(ScheduledChangeTimeForm, ScheduledChangeUpta data_version = IntegerField('data_version', validators=[InputRequired()], widget=HiddenInput()) change_type = SelectField("Change Type", choices=[('insert', 'insert'), ('update', 'update'), ('delete', 'delete')]) + class EditScheduledChangeNewRuleForm(ScheduledChangeTimeForm, ScheduledChangeUptakeForm, RuleForm): sc_data_version = IntegerField('sc_data_version', validators=[InputRequired()], widget=HiddenInput()) diff --git a/auslib/db.py b/auslib/db.py index e6bd125675..be35e9435d 100644 --- a/auslib/db.py +++ b/auslib/db.py @@ -1782,7 +1782,6 @@ def localeExists(self, name, platform, locale, transaction=None): except KeyError: return False - def isMappedTo(self, name, transaction=None): if not transaction: transaction = AUSTransaction(self.getEngine()) diff --git a/auslib/test/admin/views/test_permissions.py b/auslib/test/admin/views/test_permissions.py index 7af84268d7..32f7caa2e4 100644 --- a/auslib/test/admin/views/test_permissions.py +++ b/auslib/test/admin/views/test_permissions.py @@ -231,8 +231,6 @@ def setUp(self): dbo.permissions.scheduled_changes.conditions.history.t.insert().execute( change_id=7, changed_by="bill", timestamp=100, sc_id=3, when=30000000, data_version=2 ) - - dbo.permissions.scheduled_changes.t.insert().execute( sc_id=4, scheduled_by="bill", change_type="delete", data_version=1, base_permission="scheduled_change", base_username="mary", complete=False, base_data_version=1, @@ -493,7 +491,6 @@ def testGetScheduledChangeHistoryRevisions(self): } self.assertEquals(json.loads(ret.data), expected) - @mock.patch("time.time", mock.MagicMock(return_value=100)) def testSignoffWithPermission(self): ret = self._post("/scheduled_changes/permissions/2/signoffs", data=dict(role="qa"), username="bill") diff --git a/auslib/test/admin/views/test_releases.py b/auslib/test/admin/views/test_releases.py index 9d3c49df6e..d946fbb1ee 100644 --- a/auslib/test/admin/views/test_releases.py +++ b/auslib/test/admin/views/test_releases.py @@ -1077,8 +1077,6 @@ def setUp(self): dbo.releases.scheduled_changes.conditions.history.t.insert().execute( change_id=7, changed_by="bill", timestamp=25, sc_id=3, when=10000000, data_version=2 ) - - dbo.releases.scheduled_changes.t.insert().execute( sc_id=4, complete=False, scheduled_by="bill", change_type="delete", data_version=1, base_name="ab", base_data_version=1, ) From fc515b03e03d630b4306bd7e2fc7e58d7cbb00d6 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Sun, 8 Jan 2017 09:51:33 +0530 Subject: [PATCH 08/29] Adds release name for update --- ui/app/templates/release_scheduled_change_modal.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/app/templates/release_scheduled_change_modal.html b/ui/app/templates/release_scheduled_change_modal.html index 1e95d062a3..a41addb09e 100644 --- a/ui/app/templates/release_scheduled_change_modal.html +++ b/ui/app/templates/release_scheduled_change_modal.html @@ -1,5 +1,7 @@


    -

    Releases Details

    +

    Release Details

    From 758ff1d6849265be16a41414543c25ebc2bb1647 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Sun, 8 Jan 2017 09:55:42 +0530 Subject: [PATCH 09/29] Corrects ordering --- .../controllers/release_scheduled_changes_controller.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/js/controllers/release_scheduled_changes_controller.js b/ui/app/js/controllers/release_scheduled_changes_controller.js index 9df6770386..a6d6366e03 100644 --- a/ui/app/js/controllers/release_scheduled_changes_controller.js +++ b/ui/app/js/controllers/release_scheduled_changes_controller.js @@ -33,8 +33,8 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout value: "when" }, { - text: "Product, Channel", - value: "product,channel" + text: "Product, Name", + value: "product, Name" }, ]; @@ -126,7 +126,7 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout } }); }; - $scope.openDataModal = function(release) { + $scope.openDataModal = function(sc) { var modalInstance = $modal.open({ templateUrl: 'release_data_modal.html', @@ -135,7 +135,7 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout backdrop: 'static', resolve: { release: function () { - return release; + return sc; }, diff: function() { return false; From ce9efac055c52ee68682d9aed61bc44235b32e5c Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Tue, 10 Jan 2017 00:21:39 +0530 Subject: [PATCH 10/29] Implements history page --- .../release_scheduled_changes_controller.js | 26 ++++++++++++++++++- ui/app/js/router.js | 6 +++++ ui/app/js/services/releases_service.js | 6 +++++ .../release_scheduled_change_modal.html | 4 +-- .../templates/release_scheduled_changes.html | 13 ++++++---- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/ui/app/js/controllers/release_scheduled_changes_controller.js b/ui/app/js/controllers/release_scheduled_changes_controller.js index a6d6366e03..439d472896 100644 --- a/ui/app/js/controllers/release_scheduled_changes_controller.js +++ b/ui/app/js/controllers/release_scheduled_changes_controller.js @@ -4,6 +4,30 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout $scope.loading = true; $scope.failed = false; + $scope.sc_id = $routeParams.sc_id; + + function loadPage(newPage) { + Releases.getScheduledChangeHistory($scope.sc_id, $scope.pageSize, newPage) + .success(function(response) { + // it's the same release, but this works + $scope.scheduled_changes = response.revisions; + $scope.scheduled_changes_count = response.count; + }) + .error(function() { + console.error(arguments); + $scope.failed = true; + }) + .finally(function() { + $scope.loading = false; + }); + } + + if ($scope.sc_id) { + $scope.$watch("currentPage", function(newPage) { + loadPage(newPage); + }); + } else { + Releases.getScheduledChanges() .success(function(response) { // "when" is a unix timestamp, but it's much easier to work with Date objects, @@ -22,7 +46,7 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout .finally(function() { $scope.loading = false; }); - +} $scope.$watch("ordering_str", function(value) { $scope.ordering = value.value.split(","); }); diff --git a/ui/app/js/router.js b/ui/app/js/router.js index 2e11902fc3..73cc2fba23 100644 --- a/ui/app/js/router.js +++ b/ui/app/js/router.js @@ -32,6 +32,12 @@ angular.module("app").config(function($routeProvider, $locationProvider) { reloadOnSearch: false }) + .when("/scheduled_changes/releases/:sc_id", { + templateUrl: "release_scheduled_changes.html", + controller: "ReleaseScheduledChangesController", + reloadOnSearch: false + }) + .when('/releases/:name', { templateUrl: 'releases.html', controller: 'ReleasesController', diff --git a/ui/app/js/services/releases_service.js b/ui/app/js/services/releases_service.js index 3732e2d7cc..138ff7df9f 100644 --- a/ui/app/js/services/releases_service.js +++ b/ui/app/js/services/releases_service.js @@ -78,6 +78,11 @@ angular.module("app").factory('Releases', function($http, $q) { getScheduledChange: function(sc_id) { return $http.get("/api/scheduled_changes/releases/" + sc_id); }, + getScheduledChangeHistory: function(sc_id, limit, page) { + url = '/api/scheduled_changes/releases/' + encodeURIComponent(sc_id) + '/revisions'; + url += '?limit=' + limit + '&page=' + page; + return $http.get(url); + }, addScheduledChange: function(data, csrf_token) { data = jQuery.extend({}, data); @@ -110,6 +115,7 @@ angular.module("app").factory('Releases', function($http, $q) { return $http.delete(url); }, }; + return service; }); diff --git a/ui/app/templates/release_scheduled_change_modal.html b/ui/app/templates/release_scheduled_change_modal.html index a41addb09e..9c5c6dbbb5 100644 --- a/ui/app/templates/release_scheduled_change_modal.html +++ b/ui/app/templates/release_scheduled_change_modal.html @@ -1,7 +1,7 @@
    +

    - - - History + Current + + + History
    {{ sc.name }} From a55d29ff8ec78d3a66f706753120d2e9138b3944 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Tue, 10 Jan 2017 00:23:29 +0530 Subject: [PATCH 11/29] Adds data controller --- .../release_scheduled_data_controller.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 ui/app/js/controllers/release_scheduled_data_controller.js diff --git a/ui/app/js/controllers/release_scheduled_data_controller.js b/ui/app/js/controllers/release_scheduled_data_controller.js new file mode 100644 index 0000000000..ba5b2569a6 --- /dev/null +++ b/ui/app/js/controllers/release_scheduled_data_controller.js @@ -0,0 +1,30 @@ +angular.module('app').controller('ScheduledReleaseDataCtrl', +function($scope, $http, $modalInstance, Releases, Rules, release, diff) { + $scope.release = release; + $scope.diff = diff; + + if (release.change_id) { + if (diff) { + Releases.getDiff(release.change_id) + .then(function(response) { + $scope.release.diff = response.data; + }); + + } else { + Releases.getScheduledData(release.change_id) + .then(function(response) { + $scope.release.data = response.data; + }); + } + + } else { + Releases.getScheduledChange(release.name) + .then(function(response) { + $scope.release.data = response.data; + }); + } + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; +}); From 161fc8a42ecadab8a5af53b26a0ba2fe7f80bb5a Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Wed, 11 Jan 2017 05:31:58 +0530 Subject: [PATCH 12/29] Removing View data button --- .../release_scheduled_changes_controller.js | 17 ----------- .../release_scheduled_data_controller.js | 30 ------------------- .../templates/release_scheduled_changes.html | 2 -- 3 files changed, 49 deletions(-) delete mode 100644 ui/app/js/controllers/release_scheduled_data_controller.js diff --git a/ui/app/js/controllers/release_scheduled_changes_controller.js b/ui/app/js/controllers/release_scheduled_changes_controller.js index 439d472896..4c30926dfc 100644 --- a/ui/app/js/controllers/release_scheduled_changes_controller.js +++ b/ui/app/js/controllers/release_scheduled_changes_controller.js @@ -150,21 +150,4 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout } }); }; - $scope.openDataModal = function(sc) { - - var modalInstance = $modal.open({ - templateUrl: 'release_data_modal.html', - controller: 'ReleaseDataCtrl', - size: 'lg', - backdrop: 'static', - resolve: { - release: function () { - return sc; - }, - diff: function() { - return false; - } - } - }); - }; }); diff --git a/ui/app/js/controllers/release_scheduled_data_controller.js b/ui/app/js/controllers/release_scheduled_data_controller.js deleted file mode 100644 index ba5b2569a6..0000000000 --- a/ui/app/js/controllers/release_scheduled_data_controller.js +++ /dev/null @@ -1,30 +0,0 @@ -angular.module('app').controller('ScheduledReleaseDataCtrl', -function($scope, $http, $modalInstance, Releases, Rules, release, diff) { - $scope.release = release; - $scope.diff = diff; - - if (release.change_id) { - if (diff) { - Releases.getDiff(release.change_id) - .then(function(response) { - $scope.release.diff = response.data; - }); - - } else { - Releases.getScheduledData(release.change_id) - .then(function(response) { - $scope.release.data = response.data; - }); - } - - } else { - Releases.getScheduledChange(release.name) - .then(function(response) { - $scope.release.data = response.data; - }); - } - - $scope.cancel = function () { - $modalInstance.dismiss('cancel'); - }; -}); diff --git a/ui/app/templates/release_scheduled_changes.html b/ui/app/templates/release_scheduled_changes.html index e47017c781..1c8d3e0d31 100644 --- a/ui/app/templates/release_scheduled_changes.html +++ b/ui/app/templates/release_scheduled_changes.html @@ -87,8 +87,6 @@
    {{ sc.product }}
    Change Type:
    {{ sc.change_type }}
    -
    Data:
    -

    From 803cd9cd6a808c6c4c76c465df06058d8c0eebb9 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Tue, 17 Jan 2017 20:50:34 +0530 Subject: [PATCH 13/29] Merging origin/master --- .taskcluster.yml | 54 ++++++++++++++++++++- auslib/admin/views/forms.py | 1 - auslib/db.py | 1 + auslib/test/admin/views/test_permissions.py | 4 -- auslib/test/admin/views/test_releases.py | 4 -- 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/.taskcluster.yml b/.taskcluster.yml index 8205306e12..2b0299d4f0 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -75,7 +75,6 @@ tasks: env: true events: - push - - create branches: - master metadata: @@ -155,7 +154,6 @@ tasks: env: true events: - push - - create branches: - master metadata: @@ -163,3 +161,55 @@ tasks: description: Balrog Docker Image Creation owner: "{{ event.head.user.email }}" source: "{{ event.head.repo.url }}" + +# TODO: make this depend on the test task after https://bugzilla.mozilla.org/show_bug.cgi?id=1252144 is fixed + - provisionerId: "{{ taskcluster.docker.provisionerId }}" + workerType: "{{ taskcluster.docker.workerType }}" + scopes: + - secrets:get:repo:github.com/mozilla/balrog:dockerhub + payload: + maxRunTime: 3600 + image: "taskcluster/image_builder:0.1.3" + features: + dind: true + taskclusterProxy: true + command: + - "/bin/bash" + - "-c" + - "git clone -b {{ event.version }} {{ event.head.repo.url }} && cd balrog && scripts/push-dockerimage.sh" + extra: + github: + env: true + events: + - release + metadata: + name: Balrog Docker Image Creation + description: Balrog Docker Image Creation + owner: "{{ event.head.user.email }}" + source: "{{ event.head.repo.url }}" + +# TODO: make this depend on the test task after https://bugzilla.mozilla.org/show_bug.cgi?id=1252144 is fixed + - provisionerId: "{{ taskcluster.docker.provisionerId }}" + workerType: "{{ taskcluster.docker.workerType }}" + scopes: + - secrets:get:repo:github.com/mozilla/balrog:dockerhub + payload: + maxRunTime: 3600 + image: "taskcluster/image_builder:0.1.3" + features: + dind: true + taskclusterProxy: true + command: + - "/bin/bash" + - "-c" + - "git clone -b {{ event.version }} {{ event.head.repo.url }} && cd balrog/agent && scripts/push-dockerimage.sh" + extra: + github: + env: true + events: + - release + metadata: + name: Balrog Agent Docker Image Creation + description: Balrog Agent Docker Image Creation + owner: "{{ event.head.user.email }}" + source: "{{ event.head.repo.url }}" diff --git a/auslib/admin/views/forms.py b/auslib/admin/views/forms.py index 3e9e92cc49..24293175c5 100644 --- a/auslib/admin/views/forms.py +++ b/auslib/admin/views/forms.py @@ -180,7 +180,6 @@ class ScheduledChangeExistingPermissionForm(ScheduledChangeTimeForm): class ScheduledChangeDeletePermissionForm(ScheduledChangeTimeForm): - """Permissions and username are required when Scheduling a Change that deletes an existing Permission because they are needed to find that Permission.""" permission = StringField('Permission', validators=[Length(0, 50), InputRequired()]) diff --git a/auslib/db.py b/auslib/db.py index be35e9435d..983c193fd2 100644 --- a/auslib/db.py +++ b/auslib/db.py @@ -1806,6 +1806,7 @@ def delete(self, where, changed_by, old_data_version, transaction=None, dryrun=F if self.isMappedTo(release["name"], transaction): msg = "%s has rules pointing to it. Hence it cannot be deleted." % (release["name"]) raise ValueError(msg) + super(Releases, self).delete(where=where, changed_by=changed_by, old_data_version=old_data_version, transaction=transaction, dryrun=dryrun) if not dryrun: cache.invalidate("blob", release["name"]) diff --git a/auslib/test/admin/views/test_permissions.py b/auslib/test/admin/views/test_permissions.py index 32f7caa2e4..a7201808b1 100644 --- a/auslib/test/admin/views/test_permissions.py +++ b/auslib/test/admin/views/test_permissions.py @@ -189,7 +189,6 @@ def setUp(self): dbo.permissions.scheduled_changes.signoffs.history.t.insert().execute(change_id=1, changed_by="bill", timestamp=30, sc_id=1, username="bill") dbo.permissions.scheduled_changes.signoffs.history.t.insert().execute(change_id=2, changed_by="bill", timestamp=31, sc_id=1, username="bill", role="releng") - dbo.permissions.scheduled_changes.conditions.t.insert().execute(sc_id=1, when=10000000, data_version=1) dbo.permissions.scheduled_changes.conditions.history.t.insert().execute(change_id=1, changed_by="bill", timestamp=20, sc_id=1) dbo.permissions.scheduled_changes.conditions.history.t.insert().execute( @@ -260,7 +259,6 @@ def testGetScheduledChanges(self): "sc_id": 2, "when": 20000000, "scheduled_by": "bill", "change_type": "update", "complete": False, "sc_data_version": 1, "permission": "release_locale", "username": "ashanti", "options": None, "data_version": 1, "signoffs": {}, }, - { "sc_id": 4, "when": 76000000, "scheduled_by": "bill", "change_type": "delete", "complete": False, "sc_data_version": 1, "permission": "scheduled_change", "username": "mary", "options": None, "data_version": 1, "signoffs": {}, @@ -287,7 +285,6 @@ def testGetScheduledChangesWithCompleted(self): "sc_id": 3, "when": 30000000, "scheduled_by": "bill", "change_type": "insert", "complete": True, "sc_data_version": 2, "permission": "permission", "username": "bob", "options": None, "data_version": None, "signoffs": {}, }, - { "sc_id": 4, "when": 76000000, "scheduled_by": "bill", "change_type": "delete", "complete": False, "sc_data_version": 1, "permission": "scheduled_change", "username": "mary", "options": None, "data_version": 1, "signoffs": {}, @@ -499,7 +496,6 @@ def testSignoffWithPermission(self): self.assertEquals(len(r), 1) db_data = dict(r[0]) self.assertEquals(db_data, {"sc_id": 2, "username": "bill", "role": "qa"}) - r = dbo.permissions.scheduled_changes.signoffs.history.t.select()\ .where(dbo.permissions.scheduled_changes.signoffs.history.sc_id == 2).execute().fetchall() self.assertEquals(len(r), 2) diff --git a/auslib/test/admin/views/test_releases.py b/auslib/test/admin/views/test_releases.py index d946fbb1ee..22e17a942a 100644 --- a/auslib/test/admin/views/test_releases.py +++ b/auslib/test/admin/views/test_releases.py @@ -1106,7 +1106,6 @@ def testGetScheduledChanges(self): "name": "c", "product": "c", "data": {"name": "c", "hashFunction": "sha512", "schema_version": 1, "extv": "2.0"}, "read_only": False, "data_version": 1, "signoffs": {}, }, - { "sc_id": 4, "when": 230000000, "scheduled_by": "bill", "change_type": "delete", "complete": False, "sc_data_version": 1, "name": "ab", "product": None, "data": None, "read_only": False, "data_version": 1, "signoffs": {}, @@ -1343,9 +1342,6 @@ def testEnactScheduledChangeDeleteRelease(self): } self.assertEquals(db_data, expected) - base_row = dbo.releases.t.select().where(dbo.releases.name == "a").execute().fetchall() - self.assertEquals(len(base_row), 1) - def testGetScheduledChangeHistoryRevisions(self): ret = self._get("/scheduled_changes/releases/3/revisions") self.assertEquals(ret.status_code, 200, ret.data) From 2343a1e8efd98461c87da8d5aa626ac2c331a5a2 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Wed, 18 Jan 2017 02:39:54 +0530 Subject: [PATCH 14/29] updating checks for change_type delete --- ...elease_scheduled_change_edit_controller.js | 65 +++++++++---------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/ui/app/js/controllers/release_scheduled_change_edit_controller.js b/ui/app/js/controllers/release_scheduled_change_edit_controller.js index 7e4c45fba5..9d266e782c 100644 --- a/ui/app/js/controllers/release_scheduled_change_edit_controller.js +++ b/ui/app/js/controllers/release_scheduled_change_edit_controller.js @@ -66,43 +66,43 @@ function ($scope, $modalInstance, CSRF, Releases, sc) { }; $scope.saveChanges = function () { - if (!$scope.sc.product.trim()) { - sweetAlert( - "Form Error", - "Product is required.", - "error" - ); - return; - } - if (!$scope.dataFile) { - sweetAlert( - "Form Error", - "No file has been selected.", - "error" - ); - return; - } - if (!$scope.sc.name.trim()) { - sweetAlert( - "Form Error", - "Name is required", - "error" - ); - return; - } + if($scope.sc.change_type!=="delete") { + if (!$scope.sc.product.trim()) { + sweetAlert( + "Form Error", + "Product is required.", + "error" + ); + return; + } + if (!$scope.dataFile) { + $scope.sc.data = $scope.sc.data; + } else { + var file = $scope.dataFile; - $scope.saving = true; + var reader = new FileReader(); + reader.onload = function(evt) { + $scope.sc.data = evt.target.result; + }; + // should work + reader.readAsText(file); + } + if (!$scope.sc.name.trim()) { + sweetAlert( + "Form Error", + "Name is required", + "error" + ); + return; + } + } - var file = $scope.dataFile; + $scope.saving = true; - var reader = new FileReader(); - reader.onload = function(evt) { - var blob = evt.target.result; CSRF.getToken() .then(function(csrf_token) { var data = $scope.sc; - data.data = blob; Releases.updateScheduledChange($scope.sc.sc_id, data, csrf_token) .success(function(response) { $scope.sc.data_version = response.new_data_version; @@ -132,9 +132,8 @@ function ($scope, $modalInstance, CSRF, Releases, sc) { $scope.saving = false; }); }); - }; - // should work - reader.readAsText(file); + + }; // /saveChanges From 67026be64b3080276afde26a50d462c2ccf830c5 Mon Sep 17 00:00:00 2001 From: Ninad Bhat Date: Wed, 18 Jan 2017 04:11:19 +0530 Subject: [PATCH 15/29] Suggested changes --- .../release_scheduled_change_edit_controller.js | 2 +- .../release_scheduled_changes_controller.js | 10 +++++++--- ui/app/templates/release_scheduled_delete_modal.html | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ui/app/js/controllers/release_scheduled_change_edit_controller.js b/ui/app/js/controllers/release_scheduled_change_edit_controller.js index 9d266e782c..a88f460e7d 100644 --- a/ui/app/js/controllers/release_scheduled_change_edit_controller.js +++ b/ui/app/js/controllers/release_scheduled_change_edit_controller.js @@ -76,7 +76,7 @@ function ($scope, $modalInstance, CSRF, Releases, sc) { return; } if (!$scope.dataFile) { - $scope.sc.data = $scope.sc.data; + $scope.sc.data = $scope.original_sc.data; } else { var file = $scope.dataFile; diff --git a/ui/app/js/controllers/release_scheduled_changes_controller.js b/ui/app/js/controllers/release_scheduled_changes_controller.js index 1aa4f2be2b..7af77ae20e 100644 --- a/ui/app/js/controllers/release_scheduled_changes_controller.js +++ b/ui/app/js/controllers/release_scheduled_changes_controller.js @@ -1,5 +1,5 @@ angular.module("app").controller("ReleaseScheduledChangesController", -function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $route, Releases) { +function($scope, $routeParams, $location, $timeout, Search, $modal, $route, Releases) { $scope.loading = true; $scope.failed = false; @@ -64,8 +64,12 @@ function($scope, $routeParams, $location, $timeout, Rules, Search, $modal, $rout value: "when" }, { - text: "Product, Name", - value: "product, Name" + text: "Product", + value: "product" + }, + { + text: "Name", + value: "name" }, ]; } diff --git a/ui/app/templates/release_scheduled_delete_modal.html b/ui/app/templates/release_scheduled_delete_modal.html index 3a86332f8e..b458e0208c 100644 --- a/ui/app/templates/release_scheduled_delete_modal.html +++ b/ui/app/templates/release_scheduled_delete_modal.html @@ -1,6 +1,6 @@