def test_storage(arg):
        lib = {"foo": "baz"}
        storage = LibStorage(lib, "foo")

        storage.store("bin")

        assert lib["foo"] == "bin"
    def test_storage_when_key_does_not_exist(arg):
        lib = {}
        storage = LibStorage(lib, "foo")

        storage.store("cat")

        assert lib["foo"] == "cat"
    def __init__(self, window):
        self.window = window
        self.font = window._font
        self.label = 'Feedback'
        self.identifier = 'ghostlinesFeedback'
        self.icon = 'feedback.pdf'
        self.enabled = False

        self.font_family_id_storage = LibStorage(self.font.lib, "fontFamilyId")

        self.popover = Popover((300, 400), parentView=self.content_view)
        self.popover.web = WebView((0, 0, -0, -0))

        # Binding to "should close" instead of "close", as the event doesn't
        # seem to fire...
        self.window.window().bind("should close", self.clear_webview)
Exemple #4
0
    def create(self, _):
        token = AppStorage('accessToken').retrieve()
        api = Ghostlines("v1", token=token)
        name = self.window.family_name.get()
        designer_name = self.window.designer_name.get()
        response = api.create_font_family(name, designer_name)
        json = response.json()
        if response.status_code == 201:
            family_id_storage = LibStorage(self.font.lib, "fontFamilyId")
            family_id_storage.store(json["id"])

            if self.font.info.openTypeNameDesigner is None:
                self.font.info.openTypeNameDesigner = designer_name

            if self.font.info.familyName is None:
                self.font.info.familyName = name

            self.success_window(self.font).open()
            self.window.close()
        else:
            ErrorMessage('Could not create a font family',
                         json['errors']).open()
class CommentsMenuItem(object):

    def __init__(self, window):
        self.window = window
        self.font = window._font
        self.label = 'Feedback'
        self.identifier = 'ghostlinesFeedback'
        self.icon = 'feedback.pdf'
        self.enabled = False

        self.font_family_id_storage = LibStorage(self.font.lib, "fontFamilyId")

        self.popover = Popover((300, 400), parentView=self.content_view)
        self.popover.web = WebView((0, 0, -0, -0))

        # Binding to "should close" instead of "close", as the event doesn't
        # seem to fire...
        self.window.window().bind("should close", self.clear_webview)

    def dispatch(self, sender):
        token = AppStorage("accessToken").retrieve()
        font_family_id = self.font_family_id_storage.retrieve(default=None)

        if font_family_id is not None and token is not None and token is not '':
            window = self.window.window().getNSWindow()
            mouseDown = window.mouseLocationOutsideOfEventStream()
            height = self.content_view.frame().size.height
            rect = (mouseDown.x, height - 1, 1, 1)
            headers = {'Authorization': 'Bearer {}'.format(token)}

            self.popover.open(preferredEdge="top", relativeRect=rect)

            if not self.popover.web.url:
                comment_ui_url ="{}/ui/font_families/{}/comments".format(env.api_url, font_family_id)
                self.popover.web.load(comment_ui_url, headers)
        else:
            message("You must register a font first", "Click on the Ghostlines menu item to create a record for this font on Ghostlines. You will then be able to view the comments from all of your releases by clicking this icon.")

    def clear_webview(self, *_):
        if self.popover.web.url:
            self.popover.web.unload()

        return True

    @property
    def content_view(self):
        return self.window.window().getNSWindow().contentView()
 def has_family(self, font):
     family_id_storage = LibStorage(font.lib, "fontFamilyId")
     return family_id_storage.retrieve(default=None) is not None
 def has_legacy_data(self, font):
     legacy_storage = LibStorage(font.lib, "recipients")
     return legacy_storage.retrieve(default=None) is not None
    def test_retrieval(arg):
        lib = {"foo": "baz"}
        storage = LibStorage(lib, "foo")

        assert storage.retrieve() == "baz"
    def test_retrieval_when_key_does_not_exist(arg):
        lib = {}
        storage = LibStorage(lib, "foo")

        assert storage.retrieve() == ""
    def __init__(self, font):
        self.font = font
        self.recipients = FontRecipients(self.font)
        self.applicants = []
        self.note_draft_storage = LibStorage(self.font.lib,
                                             "release_notes_draft")
        self.email_storage = LibStorage(self.font.lib,
                                        "designer_email_address")
        self.license_storage = LibStorage(self.font.lib, "license_filepath")

        self.window.banner_background = Background((0, -40, 0, 40), 1)
        self.window.upgrade_tip = TextBox(
            (15, -30, -15, 22),
            WhiteText(
                "Ghostlines is out of Beta. If you have an account, upgrade:"))
        self.window.upgrade_button = Button((-205, -31, 185, 22),
                                            "Migrate from Beta",
                                            callback=self.migrate,
                                            sizeStyle="small")

        self.window.background = Background((0, 0, -0, 235))
        self.window.attribution = AttributionText((15, 15, -15, 22), font)
        self.window.send_button = CounterButton(
            (-215, 12, 200, 24), ("Send Release to All", "Send Release to {}"),
            callback=self.send)

        self.window.notes_field_label = TextBox((15, 52, -15, 22),
                                                WhiteText("Release Notes"))
        self.window.notes_field = NotesEditor(
            (15, 75, -15, 80), draft_storage=self.note_draft_storage)

        self.window.email_address_field_label = TextBox(
            (15, 170, 270, 22), WhiteText("Contact Email Included in Release"))
        self.window.email_address_field = EmailAddressField(
            (15, 193, 270, 22), storage=self.email_storage)

        self.window.license_field_label = TextBox((-285, 170, -15, 22),
                                                  WhiteText("License"))
        self.window.license_field = FileUploadField(
            (-285, 193, -15, 22), storage=self.license_storage)

        self.window.recipients_label = TextBox((-285, 250, -15, 22),
                                               "Subscribers")
        self.window.recipients = List(
            (-285, 273, 270, -89),
            self.recipients,
            selectionCallback=self.update_send_button)
        self.window.recipients.setSelection([])

        self.window.recipients_tip = TextBox((-200, -73, 185, 14),
                                             "cmd+click to select subset",
                                             alignment="right",
                                             sizeStyle="small")

        self.window.add_recipient_button = Button((-285, -79, 30, 24),
                                                  "+",
                                                  callback=self.add_recipient)
        self.window.remove_recipient_button = Button(
            (-246, -79, 30, 24), "-", callback=self.remove_recipient)

        self.window.applicants = ApplicantList(
            (15, 250, 270, 235),
            self.font,
            self.applicants,
            self.recipients,
            after_approve=self.add_approved_applicant)

        self.window.bind("became main", self.fetch_applicants)

        self.window.setDefaultButton(self.window.send_button)
class LegacyReleaseWindow(BaseWindowController):
    def __init__(self, font):
        self.font = font
        self.recipients = FontRecipients(self.font)
        self.applicants = []
        self.note_draft_storage = LibStorage(self.font.lib,
                                             "release_notes_draft")
        self.email_storage = LibStorage(self.font.lib,
                                        "designer_email_address")
        self.license_storage = LibStorage(self.font.lib, "license_filepath")

        self.window.banner_background = Background((0, -40, 0, 40), 1)
        self.window.upgrade_tip = TextBox(
            (15, -30, -15, 22),
            WhiteText(
                "Ghostlines is out of Beta. If you have an account, upgrade:"))
        self.window.upgrade_button = Button((-205, -31, 185, 22),
                                            "Migrate from Beta",
                                            callback=self.migrate,
                                            sizeStyle="small")

        self.window.background = Background((0, 0, -0, 235))
        self.window.attribution = AttributionText((15, 15, -15, 22), font)
        self.window.send_button = CounterButton(
            (-215, 12, 200, 24), ("Send Release to All", "Send Release to {}"),
            callback=self.send)

        self.window.notes_field_label = TextBox((15, 52, -15, 22),
                                                WhiteText("Release Notes"))
        self.window.notes_field = NotesEditor(
            (15, 75, -15, 80), draft_storage=self.note_draft_storage)

        self.window.email_address_field_label = TextBox(
            (15, 170, 270, 22), WhiteText("Contact Email Included in Release"))
        self.window.email_address_field = EmailAddressField(
            (15, 193, 270, 22), storage=self.email_storage)

        self.window.license_field_label = TextBox((-285, 170, -15, 22),
                                                  WhiteText("License"))
        self.window.license_field = FileUploadField(
            (-285, 193, -15, 22), storage=self.license_storage)

        self.window.recipients_label = TextBox((-285, 250, -15, 22),
                                               "Subscribers")
        self.window.recipients = List(
            (-285, 273, 270, -89),
            self.recipients,
            selectionCallback=self.update_send_button)
        self.window.recipients.setSelection([])

        self.window.recipients_tip = TextBox((-200, -73, 185, 14),
                                             "cmd+click to select subset",
                                             alignment="right",
                                             sizeStyle="small")

        self.window.add_recipient_button = Button((-285, -79, 30, 24),
                                                  "+",
                                                  callback=self.add_recipient)
        self.window.remove_recipient_button = Button(
            (-246, -79, 30, 24), "-", callback=self.remove_recipient)

        self.window.applicants = ApplicantList(
            (15, 250, 270, 235),
            self.font,
            self.applicants,
            self.recipients,
            after_approve=self.add_approved_applicant)

        self.window.bind("became main", self.fetch_applicants)

        self.window.setDefaultButton(self.window.send_button)

    def fetch_applicants(self, sender):
        self.window.applicants.fetch()

    def add_approved_applicant(self, email):
        self.recipients.append(email)
        self.window.recipients.set(self.recipients)

    def open(self):
        if not self.font.info.familyName:
            message("Family Name must be set", full_requirements_message)
            return

        if not self.font.info.openTypeNameDesigner:
            message("Designer must be set", full_requirements_message)
            return

        self.window.open()

    def send(self, sender):
        recipients = self.window.recipients.get()
        selection = [
            recipients[i] for i in self.window.recipients.getSelection()
        ]

        if selection == []:
            selection = self.window.recipients.get()

        recipients = ', '.join(selection)

        progress = ProgressWindow('', tickCount=3, parentWindow=self.window)

        try:
            tmpdir = tempfile.mkdtemp(prefix="ghostlines")

            progress.update('Generating OTF')

            # Should be controlled which options are used somewhere
            filename = os.path.join(tmpdir, self.font.info.familyName + '.otf')

            self.font.generate(format="otf",
                               path=filename,
                               decompose=True,
                               checkOutlines=True,
                               autohint=True)

            progress.update('Sending via Ghostlines')

            with open(filename, 'rb') as otf:
                params = dict(otf=otf,
                              recipients=recipients,
                              notes=self.window.notes_field.get(),
                              designer_email_address=self.window.
                              email_address_field.get())

                license_path = self.license_storage.retrieve()

                if license_path is not '' and os.path.exists(license_path):
                    with open(license_path, 'rb') as license:
                        filename = os.path.basename(license_path)
                        _, extension = os.path.splitext(license_path)
                        content_type = filetypes[extension]
                        params['license'] = (filename, license, content_type)
                        response = Ghostlines('v0.1').send(**params)
                else:
                    response = Ghostlines('v0.1').send(**params)

            if response.status_code == requests.codes.created:
                message("{} was delivered".format(self.font.info.familyName))
            else:
                print repr(response)
                message(
                    "{} could not be delivered".format(
                        self.font.info.familyName),
                    "Error code: {}\n{}".format(response.status_code,
                                                response.json()))
        finally:
            progress.close()

    def remove_recipient(self, sender):
        for index in self.window.recipients.getSelection():
            del self.recipients[index]

        self.window.recipients.set(self.recipients)

    def add_recipient(self, sender):
        self.window.sheet = Sheet((250, 89), self.window)
        self.window.sheet.recipient = EditText((15, 15, -15, 22),
                                               "",
                                               placeholder="Email Address")
        self.window.sheet.cancel_button = Button((-190, 52, 80, 22),
                                                 'Cancel',
                                                 callback=self.close_sheet)
        self.window.sheet.create_button = Button(
            (-95, 52, 80, 22), 'Add', callback=self.create_recipient)
        self.window.sheet.setDefaultButton(self.window.sheet.create_button)
        self.window.sheet.open()

    def close_sheet(self, *args):
        self.window.sheet.close()

    def create_recipient(self, *args):
        email = self.window.sheet.recipient.get()

        if not email is '':
            self.recipients.append(email)
            self.window.recipients.set(self.recipients)
            self.close_sheet()

    def update_send_button(self, sender):
        self.window.send_button.amount = len(
            self.window.recipients.getSelection())

    def migrate(self, sender):
        MigrationAssistant(self.font).open()
        self.window.close()

    @property
    def title(self):
        return "Deliver {}".format(self.font.info.familyName)

    @lazy_property
    def window(self):
        return Window((600, 540),
                      autosaveName=self.__class__.__name__,
                      title=self.title)
    def migrate(self, *_):
        token = AppStorage("accessToken").retrieve()
        api = Ghostlines("v1", token=token)
        response = api.migrate(list(self.recipients),
                               self.font.info.familyName,
                               self.font.info.openTypeNameDesigner,
                               self.roster_token)
        json = response.json()

        if response.status_code == 201:
            LibStorage(self.font.lib, "fontFamilyId").store(json["id"])

            expired_keys = [
                'pm.ghostlines.ghostlines.registry_token',
                'pm.ghostlines.ghostlines.recipients',
                'pm.ghostlines.ghostlines.designer_email_address'
            ]

            for key in expired_keys:
                if key in self.font.lib:
                    del self.font.lib[key]

            old_note_storage = LibStorage(self.font.lib, "release_notes_draft")
            old_license_storage = LibStorage(self.font.lib, "license_filepath")
            LibStorage(self.font.lib, "releaseNotesDraft").store(
                old_note_storage.retrieve(default=None))
            LibStorage(self.font.lib, "licenseFilepath").store(
                old_license_storage.retrieve(default=None))
            old_note_storage.store(None)
            old_license_storage.store(None)

            from ghostlines.windows.release_window import ReleaseWindow

            self.window.close()
            ReleaseWindow(self.font).open()
        else:
            ErrorMessage("Oops", json["errors"])
Exemple #13
0
    def __init__(self, font, document=None):
        self.font = font

        self.note_draft_storage = LibStorage(self.font.lib,
                                             "releaseNotesDraft")
        self.license_storage = LibStorage(self.font.lib, "licenseFilepath")
        self.family_id_storage = LibStorage(self.font.lib, "fontFamilyId")

        self.subscribers = self.font_family["subscribers"]

        if document is not None:
            self.window.assignToDocument(document)

        self.window.background = Background((301, -52, 299, 52), alpha=0.05)

        self.window.release_info = Group((315, 15, 270, -15))

        self.window.release_info.font_name_label = TextBox((0, 0, -0, 22),
                                                           "Font Name",
                                                           sizeStyle="small")
        self.window.release_info.font_name = TextBox((0, 19, -0, 22),
                                                     self.font_family["name"])
        self.window.release_info.font_author_label = TextBox((0, 60, -0, 22),
                                                             "Designer",
                                                             sizeStyle="small")
        self.window.release_info.font_author = TextBox(
            (0, 79, -0, 22), self.font_family["designer_name"])
        self.window.release_info.version_label = TextBox((0, 120, -0, 22),
                                                         "Version Number",
                                                         sizeStyle="small")
        self.window.release_info.version = TextBox((0, 139, -0, 22),
                                                   self.font_version)

        self.window.release_info.notes_field_label = TextBox((0, 176, -0, 22),
                                                             "Release Notes",
                                                             sizeStyle="small")
        self.window.release_info.notes_field = NotesEditor(
            (0, 198, -0, 175), draft_storage=self.note_draft_storage)

        self.window.release_info.license_field_label = TextBox(
            (0, 393, -0, 22), "Attach License", sizeStyle="small")
        self.window.release_info.license_field = FileUploadField(
            (0, 410, -0, 22), storage=self.license_storage)

        self.window.release_info.send_button = CounterButton(
            (0, -24, -0, 24), ("Send Release to All", "Send Release to {}"),
            callback=self.send)

        self.window.release_subscriber_divider = VerticalLine((300, 0, 1, -0))

        self.window.subscriber_info = Group((15, 15, 270, -15))

        self.window.subscriber_info.subscribers_label = TextBox(
            (0, 0, -0, 22), "Subscribers", sizeStyle="small")
        self.window.subscriber_info.subscribers = List(
            (0, 22, -0, 205),
            self.subscribers,
            columnDescriptions=[{
                "title": "Name",
                "key": "name",
                "editable": False
            }, {
                "title": "Email Address",
                "key": "email_address",
                "editable": False
            }],
            selectionCallback=self.update_send_button)
        self.window.subscriber_info.subscribers.setSelection([])

        self.window.subscriber_info.subscribers_tip = TextBox(
            (0, 238, -0, 14),
            "cmd+click to select subset",
            alignment="right",
            sizeStyle="small")

        self.window.subscriber_info.show_subscriber_sheet_button = Button(
            (0, 233, 30, 24), "+", callback=self.show_subscriber_sheet)
        self.window.subscriber_info.remove_subscriber_button = Button(
            (40, 233, 30, 24), "-", callback=self.remove_subscriber)

        self.window.subscriber_info.applicants = \
            ApplicantList((0, 280, 270, 210),
                          self.font_family["applicant_roster"],
                          self.family_id_storage.retrieve(),
                          after_approve=self.refresh_subscribers)

        self.window.release_releases_divider = VerticalLine((600, 0, 1, -0))

        self.window.releases_info = Group((615, 15, 270, -15))

        self.window.releases_info.log_label = TextBox((0, 0, -0, 22),
                                                      "Releases",
                                                      sizeStyle="small")
        self.window.releases_info.log = \
            ReleaseLog((0, 22, -0, -0),
                       self.font_family["releases"][::-1],
                       columnDescriptions=[
                      {
                          "title": "Created",
                          "key": "created_at",
                          "editable": False
                      }, {
                          "title": "Version",
                          "key": "version",
                          "editable": False,
                          "width": 50
                      }, {
                          "title": "# subs.",
                          "key": "subscriber_count",
                          "editable": False,
                          "width": 50
                      }
                  ])

        self.resize_window_for_releases()
        self.window.bind("became main", self.fetch_applicants)
Exemple #14
0
class ReleaseWindow(BaseWindowController):
    def __init__(self, font, document=None):
        self.font = font

        self.note_draft_storage = LibStorage(self.font.lib,
                                             "releaseNotesDraft")
        self.license_storage = LibStorage(self.font.lib, "licenseFilepath")
        self.family_id_storage = LibStorage(self.font.lib, "fontFamilyId")

        self.subscribers = self.font_family["subscribers"]

        if document is not None:
            self.window.assignToDocument(document)

        self.window.background = Background((301, -52, 299, 52), alpha=0.05)

        self.window.release_info = Group((315, 15, 270, -15))

        self.window.release_info.font_name_label = TextBox((0, 0, -0, 22),
                                                           "Font Name",
                                                           sizeStyle="small")
        self.window.release_info.font_name = TextBox((0, 19, -0, 22),
                                                     self.font_family["name"])
        self.window.release_info.font_author_label = TextBox((0, 60, -0, 22),
                                                             "Designer",
                                                             sizeStyle="small")
        self.window.release_info.font_author = TextBox(
            (0, 79, -0, 22), self.font_family["designer_name"])
        self.window.release_info.version_label = TextBox((0, 120, -0, 22),
                                                         "Version Number",
                                                         sizeStyle="small")
        self.window.release_info.version = TextBox((0, 139, -0, 22),
                                                   self.font_version)

        self.window.release_info.notes_field_label = TextBox((0, 176, -0, 22),
                                                             "Release Notes",
                                                             sizeStyle="small")
        self.window.release_info.notes_field = NotesEditor(
            (0, 198, -0, 175), draft_storage=self.note_draft_storage)

        self.window.release_info.license_field_label = TextBox(
            (0, 393, -0, 22), "Attach License", sizeStyle="small")
        self.window.release_info.license_field = FileUploadField(
            (0, 410, -0, 22), storage=self.license_storage)

        self.window.release_info.send_button = CounterButton(
            (0, -24, -0, 24), ("Send Release to All", "Send Release to {}"),
            callback=self.send)

        self.window.release_subscriber_divider = VerticalLine((300, 0, 1, -0))

        self.window.subscriber_info = Group((15, 15, 270, -15))

        self.window.subscriber_info.subscribers_label = TextBox(
            (0, 0, -0, 22), "Subscribers", sizeStyle="small")
        self.window.subscriber_info.subscribers = List(
            (0, 22, -0, 205),
            self.subscribers,
            columnDescriptions=[{
                "title": "Name",
                "key": "name",
                "editable": False
            }, {
                "title": "Email Address",
                "key": "email_address",
                "editable": False
            }],
            selectionCallback=self.update_send_button)
        self.window.subscriber_info.subscribers.setSelection([])

        self.window.subscriber_info.subscribers_tip = TextBox(
            (0, 238, -0, 14),
            "cmd+click to select subset",
            alignment="right",
            sizeStyle="small")

        self.window.subscriber_info.show_subscriber_sheet_button = Button(
            (0, 233, 30, 24), "+", callback=self.show_subscriber_sheet)
        self.window.subscriber_info.remove_subscriber_button = Button(
            (40, 233, 30, 24), "-", callback=self.remove_subscriber)

        self.window.subscriber_info.applicants = \
            ApplicantList((0, 280, 270, 210),
                          self.font_family["applicant_roster"],
                          self.family_id_storage.retrieve(),
                          after_approve=self.refresh_subscribers)

        self.window.release_releases_divider = VerticalLine((600, 0, 1, -0))

        self.window.releases_info = Group((615, 15, 270, -15))

        self.window.releases_info.log_label = TextBox((0, 0, -0, 22),
                                                      "Releases",
                                                      sizeStyle="small")
        self.window.releases_info.log = \
            ReleaseLog((0, 22, -0, -0),
                       self.font_family["releases"][::-1],
                       columnDescriptions=[
                      {
                          "title": "Created",
                          "key": "created_at",
                          "editable": False
                      }, {
                          "title": "Version",
                          "key": "version",
                          "editable": False,
                          "width": 50
                      }, {
                          "title": "# subs.",
                          "key": "subscriber_count",
                          "editable": False,
                          "width": 50
                      }
                  ])

        self.resize_window_for_releases()
        self.window.bind("became main", self.fetch_applicants)

    def fetch_applicants(self, sender):
        self.window.subscriber_info.applicants.fetch()

    def refresh_subscribers(self, *args):
        self.refresh_font_family()
        self.window.subscriber_info.subscribers.set(
            self.font_family["subscribers"])

    def refresh_releases(self, *args):
        self.refresh_font_family()
        self.window.releases_info.log.set(self.font_family["releases"][::-1])

    def refresh_font_family(self, *args):
        token = AppStorage("accessToken").retrieve()
        api = Ghostlines("v1", token=token)
        self.font_family = api.font_family(
            self.family_id_storage.retrieve()).json()
        self.resize_window_for_releases()

    def resize_window_for_releases(self, *args):
        if self.font_family["releases"]:
            width = 900
        else:
            width = 600

        left, top, _, height = self.window.getPosSize()
        self.window.setPosSize((left, top, width, height))

    def open(self):
        if not self.font.info.familyName:
            message("Family Name must be set", full_requirements_message)
            return

        if not self.font.info.openTypeNameDesigner:
            message("Designer must be set", full_requirements_message)
            return

        self.window.open()

    def send(self, *_):
        subscribers = self.window.subscriber_info.subscribers.get()
        subscriber_ids = [
            subscribers[i]["id"]
            for i in self.window.subscriber_info.subscribers.getSelection()
        ]
        notes = self.note_draft_storage.retrieve()
        font_family_id = self.family_id_storage.retrieve()
        license_path = self.license_storage.retrieve()

        progress = ProgressWindow('', tickCount=3, parentWindow=self.window)

        try:
            tmpdir = tempfile.mkdtemp(prefix="ghostlines")

            progress.update('Generating OTF')

            # Should be controlled which options are used somewhere
            otf_path = os.path.join(tmpdir,
                                    '{}.otf'.format(self.font.info.familyName))
            self.font.generate(format="otf",
                               path=otf_path,
                               decompose=True,
                               checkOutlines=True,
                               autohint=True)

            progress.update('Sending via Ghostlines')

            params = dict(notes=notes, font_family_id=font_family_id)

            with open(otf_path, 'rb') as otf:
                params['otfs'] = [(os.path.basename(otf_path), otf.read(),
                                   "application/octet-stream")]

            if subscriber_ids:
                params['subscriber_ids[]'] = subscriber_ids

            if self.license_exists:
                with open(license_path, 'rb') as license:
                    filename = os.path.basename(license_path)
                    _, extension = os.path.splitext(license_path)
                    content_type = filetypes[extension]
                    params['license'] = (filename, license.read(),
                                         content_type)

            token = AppStorage("accessToken").retrieve()
            response = Ghostlines('v1', token=token).create_release(**params)

            if response.status_code == requests.codes.created:
                message("{} was delivered".format(self.font.info.familyName))

                self.refresh_releases()
            else:
                ErrorMessage(
                    "{} could not be delivered".format(
                        self.font.info.familyName),
                    response.json()["errors"])
        finally:
            progress.close()

    def show_subscriber_sheet(self, sender):
        self.window.sheet = Sheet((250, 107), self.window)
        self.window.sheet.name = EditText((15, 15, -15, 22),
                                          "",
                                          placeholder="Name")
        self.window.sheet.email_address = EditText((15, 43, -15, 22),
                                                   "",
                                                   placeholder="Email Address")
        self.window.sheet.cancel_button = Button((-190, 70, 80, 22),
                                                 'Cancel',
                                                 callback=self.close_sheet)
        self.window.sheet.create_button = Button(
            (-95, 70, 80, 22), 'Add', callback=self.create_subscriber)
        self.window.sheet.setDefaultButton(self.window.sheet.create_button)
        self.window.sheet.open()

    def create_subscriber(self, *args):
        name = self.window.sheet.name.get()
        email_address = self.window.sheet.email_address.get()
        token = AppStorage("accessToken").retrieve()
        api = Ghostlines("v1", token=token)
        response = api.create_subscriber(self.family_id_storage.retrieve(),
                                         name, email_address)
        json = response.json()

        if response.status_code == 201:
            self.refresh_subscribers()
            self.close_sheet()
        else:
            ErrorMessage("Couldn't create that subscriber", json["errors"])

    def remove_subscriber(self, sender):
        token = AppStorage("accessToken").retrieve()
        api = Ghostlines("v1", token=token)

        for index in self.window.subscriber_info.subscribers.getSelection():
            subscriber = self.window.subscriber_info.subscribers[index]
            api.delete_subscriber(subscriber["id"])

        self.refresh_subscribers()

    def close_sheet(self, *args):
        self.window.sheet.close()

    def update_send_button(self, sender):
        self.window.release_info.send_button.amount = len(
            self.window.subscriber_info.subscribers.getSelection())

    @property
    def font_version(self):
        major = self.font.info.versionMajor
        minor = self.font.info.versionMinor
        if major is not None and minor is not None:
            return "{}.{}".format(major, minor)
        else:
            return u"\u2014"

    @property
    def title(self):
        return "Ghostlines: Release {}".format(self.font.info.familyName)

    @property
    def license_exists(self):
        filepath = self.license_storage.retrieve()
        return filepath is not '' and os.path.exists(filepath)

    @lazy_property
    def window(self):
        return Window((900, 520),
                      autosaveName=self.__class__.__name__,
                      title=self.title)

    @lazy_property
    def font_family(self):
        token = AppStorage("accessToken").retrieve()
        return Ghostlines("v1", token=token).font_family(
            self.family_id_storage.retrieve()).json()