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"])
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() == ""
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)
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()