Manipulate groups in iCloud with CardDAV protocol - icloud

I am currently trying to manipulate groups of my contact list in iCloud through CardDav protocol.
For that, I created a group containing a contact in Contact.app:
Screenshot
When I export the vCard of my contact directly from Contact app, I have the groups associated with my contacts in the CATEGORIES property :
BEGIN:VCARD
VERSION:3.0
PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN
N:Lebron;Candide;;;
FN:Candide Lebron
ORG:Podbox;
NOTE:Un petit peu candide.
CATEGORIES:GroupForAutomaticTestDELTA
UID:d4c1baf6-f603-4fb5-8f19-d45eb1e7fb23
X-ABUID:D4C1BAF6-F603-4FB5-8F19-D45EB1E7FB23:ABPerson
END:VCARD
However when I request my iCloud server to retrieve this contact I do not get CATEGORIES property set but I retrieve two vCards, one for the contact and one for the group. The group vCard contains references to its members.
Request:
curl --request REPORT --user ****#*****:**** --header "Content-Type: text/xml" --data '
<?xml version="1.0" encoding="utf-8" ?>
<C:addressbook-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:prop>
<C:address-data/>
</D:prop>
</C:addressbook-query>' https://contacts.icloud.com/802592377/carddavhome/card/
contact VCard:
BEGIN:VCARD
VERSION:3.0
PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN
N:Lebron;Candide;;;
FN:Candide Lebron
ORG:Podbox;
NOTE:Un petit peu candide.
REV:2014-06-12T16:53:51Z
UID:d4c1baf6-f603-4fb5-8f19-d45eb1e7fb23
END:VCARD
group vCard:
BEGIN:VCARD
VERSION:3.0
PRODID:-//Apple Inc.//AddressBook 8.0//EN
N:GroupToBeAddedTO
FN:GroupToBeAddedTO
X-ADDRESSBOOKSERVER-KIND:group
X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:d4c1baf6-f603-4fb5-8f19-d45eb1e7fb23
REV:2014-06-12T16:43:04Z
UID:d59c9f0c-27aa-47e3-96e7-43717bbc1d7e
END:VCARD
Notice the CATEGORIES property appearing in contact export but not with a CardDAV request. My guess would be that the Contact app constructs the CATEGORIES when exporting but I am not sure.
Do someone has an idea of how iCloud works exactly? Am I missing something preventing me to retrieve CATEGORIES through CardDAV?

OK. Let's start at the beginning. What is a CardDAV collection. A CardDAV collection is like a folder containing vCard files. Some (Many?) servers allow multiple of such folders, some don't. iCloud (currently) is in the latter category - there is only one 'folder' on the server to store all vCards of one particular iCloud user. Other servers sometimes call secondary CardDAV collections 'groups'.
What is a group. A group is a collection of contacts. Addressbook.app uses a special group vCard to represent this relationship. This 'group vCard' has references to it's members via the X-ADDRESSBOOKSERVER-MEMBER property. It contains the UID of the contact vCard (or other group vCard) which is supposed to be a member of this group.
In vCard 3 - which is what most CardDAV infrastructure uses - this is kinda Apple specific (hence X-ADDRESSBOOKSERVER-xyz). In vCard 4 this has been included in the spec.
Addressbook.app AFAIK does not support real CATEGORIES (commonly known as tags). The vCard CATEGORIES field is originally for stuff like 'VIP' or 'Nice Guy', it's not for 'groups' per se.
Now when Addressbook.app exports a vCard, it tries to preseve the group information in the CATEGORIES field. Which is a bit weird but somewhat reasonable. A CardDAV group only really makes sense within a CardDAV collection, the group membership information is not contained within the vCard itself (but in that separate vCard record for the group). Remember that the export is intendet to transfer stuff between systems
Sidenote: If you are coming from the Exchange/Outlook world: CardDAV collections are like Exchange folders. CardDAV groups are like Exchange Distribution Lists.
I guess what you really want to know is how to add/remove members to a CardDAV group? Let's assume you have a record like:
BEGIN:VCARD
UID:joe
N:User;Joe;;;
END:VCARD
And you want to add him to a group, like 'Friends'. Assuming the group already exists, it'll look like this (with two exisiting members):
BEGIN:VCARD
N:Friends
FN:Friends
X-ADDRESSBOOKSERVER-KIND:group
X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:AwesomeO
X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:Xavier
UID:group-uid
END:VCARD
What you need to do to add a member is fetch the group and then add the UID, the result would like this:
BEGIN:VCARD
N:Friends
FN:Friends
X-ADDRESSBOOKSERVER-KIND:group
X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:AwesomeO
X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:Xavier
X-ADDRESSBOOKSERVER-MEMBER:urn:uuid:joe
UID:group-uid
END:VCARD
Makes sense?
Don't be confused by the categories hack of the export. This is not how groups work.

Related

Replacing full referrer using REGEX Google Data Studio

I'm using Google Data Studio to create a report analyzing specific referral sites. My data source is my site Google Analytics.
I want to replace the Full Referrer (e.g. of the format webaddress.com/page-name-one) with a text only value (i.e Page name one), so that it's clearer to see in the report which page is which in my charts and tables.
I've used the below formulae in the calculated fields, but none of them seem to change Full Referrer to match what I need it to. Data studio recognizes them all as valid formulae too.
I've anonymised my examples, but it has the same principles. I've tried:
REGEXP_REPLACE(Full Referrer,"[webaddress\\.com\\/page\\-name\\-one].*","Page name one")
REGEXP_REPLACE(Full Referrer, 'webaddress.com/page-name-one', 'Page name one')
REGEXP_REPLACE(Full Referrer, 'webaddress\\.com\\/page\\-name\\-one', 'Page name one')
REGEXP_REPLACE(Full Referrer, 'name', 'Page name one')
REGEXP_REPLACE(Full Referrer, 'page-name-one', 'Page name one')
REGEXP_REPLACE(Full Referrer, 'page\\-name\\-one', 'Page name one')
In testing this on one of my own GA data sources, I was able to achieve this using one of your patterns:
REGEXP_REPLACE(Full Referrer,'webaddress.com/page-name-one','Page name one')
It should be noted, however, that the . should be properly escaped (either by \ or wrapping it in a character class like [.]; see re2 syntax for details). Because you have to double-backslash, I also prefer to use something Data Studio borrowed from BigQuery (sort of an undocumented feature), which is the regular expression string type (r"" or r''). When using this, you only have to single-backslash (unless you want a literal backslash):
REGEXP_REPLACE(Full Referrer,r'webaddress\.com/page-name-one','Page name one')
Because you're using REGEXP_REPLACE, anything before or after your match string will still exist after the replacement—meaning that for a Full Referrer of "m.facebook.com/l", REGEXP_REPLACE(Full Referrer,r'facebook\.com','FB') would return "m.FB/l"
So your pattern above will match the value anywhere in the string, which likely isn't what you want. To anchor it to the beginning, use the ^ (start of string) assertion:
REGEXP_REPLACE(Full Referrer,r'^webaddress\.com/page-name-one','Page name one')
If you want to only match that exact value of Full Referrer (i.e. not including any additional path levels), make sure to use the $ (end of string) assertion as well:
REGEXP_REPLACE(Full Referrer,r'^webaddress\.com/page-name-one$','Page name one')
Keep in mind that if you're doing this in the data source as a calculated field, you aren't actually changing the original metric—you're working on a copy of it. So you need to replace Full Referrer with whatever you named your calculated field in the data source.
Often you're wanting to do this for a bunch of sites or pages, so you can use CASE and REGEXP_MATCH to handle all this logic in a single field:
CASE
WHEN REGEXP_MATCH(Full Referrer,r'^webaddress\.com/page-name-one$') THEN 'Page name one'
WHEN REGEXP_MATCH(Full Referrer,r'^site2\.com/example$') THEN 'S2 Example'
ELSE Full Referrer
END
These matches are done in order, so you can even match a specific page or pages, and then still provide a different value for anything on that domain that you didn't match:
CASE
WHEN REGEXP_MATCH(Full Referrer,r'^site\.com/$') THEN 'Site - Home'
WHEN REGEXP_MATCH(Full Referrer,r'^site\.com/about$') THEN 'Site - About'
WHEN REGEXP_MATCH(Full Referrer,r'^site\.com/') THEN 'Site - (other)'
ELSE Full Referrer
END
You can also use the ELSE if you want to bucket all of the unmatched values into an "other" grouping instead of just leaving the original value.
Another thing to remember is that due to shared fields in GA, things like Source (utm_source) also show up in Full Referrer, so you could be seeing values there that you wouldn't normally expect. Often you can get rid of these by also filtering to only the Default Channel Grouping of "Referral".
If your patterns still aren't matching, please update the question with some additional details such as what the output actually is, whether there's an error message, etc.—and also whether you're doing this as a calculated field in the data source or the "Create Field" button on a single chart.

QuickBooks Desktop Invoice Integration and InvoiceGroupLine Limitations

I have been Integrating Invoice module of Quickbooks Desktop Enterprise with an application using "qbxml", i encountered that with InvoiceLine i can send many fields including custom fields, But when it comes to InvoiceGroupLine i can't send the following which i need too:
1.Service Date field;
2.Other1;
3.Other2.
(all of the above mentioned fields to be added at the last row of GroupItem where description of it is placed as shown in picture below.)
Also attached picture shows the custom field called "Employee" was populated at the first line of this, which should be at the bottom where the last description is.
Secondly, you can see i was unable to pass through 'service date' and 'other1' which is 'Patient' and 'other2' which is 'MR Number' fields for GroupItem.
The reason why i asked about population on the bottom is because, only the last line of GroupItem copies to Print/Print Preview of Invoices in Quickbooks.
How can i able to achieve my goal of sending over these fields as i mentioned above to QuickBooks to their respective places/positions and fields? thanks.

XML submitted just fine to Amazon MWS but price not being updated

I created my own repricer of sorts but the price isn't being updated on Amazon's side.
My code seems to work just fine based off the response from Amazon after submitting it. I'm hoping someone here knows more about why its not actually updating the price.
Here's the XML submitted:
<?xml version="1.0" encoding="utf-8" ?>
<AmazonEnvelope
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="amzn-envelope.xsd">
<Header>
<DocumentVersion>1.01</DocumentVersion>
<MerchantIdentifier>MERCHANTID</MerchantIdentifier>
</Header>
<MessageType>Price</MessageType>
<Message>
<MessageID>1</MessageID>
<Price>
<SKU>mysku</SKU>
<StandardPrice currency="USD">350.50</StandardPrice>
</Price>
</Message>
</AmazonEnvelope>
Heres the response:
GetFeedSubmissionResultResponse{}(ResponseMetadata: <Element_?/ResponseMetadata_0x7fee61f74248>, GetFeedSubmissionResultResult: <Element_?/GetFeedSubmissionResultResult_0x7fee61f74248>, AmazonEnvelope:
{'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:noNamespaceSchemaLocation': 'amzn-envelope.xsd'}, DocumentVersion: '1.02', MerchantIdentifier: 'M_EXAMPLE_1234', Header: '\n\t', MessageType: 'ProcessingReport', MessageID: '1', DocumentTransactionID: '4200000000', StatusCode: 'Complete', MessagesProcessed: '1', MessagesSuccessful: '1', MessagesWithError: '0', MessagesWithWarning: '0', ProcessingSummary: '\n\t\t\t', ProcessingReport: '\n\t\t', Message: '\n\t')
I don't know if showing my code will help in this instance since I get a successful response from Amazon. Here it is regardless:
...
# Provide credentials.
conn = MWSConnection(
aws_access_key_id=AWS_ACCESS_KEY_ID,
aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
Merchant=AMZ_SELLER_ID
)
# Get the service resource
sqs = boto3.resource('sqs')
# Get the queue
queue = sqs.get_queue_by_name(QueueName=SQS_QUEUE_NAME)
for index, message in enumerate(queue.receive_messages(MaxNumberOfMessages=10)):
...
import time
from jinja2 import Environment, PackageLoader
env = Environment(loader=PackageLoader('repricer', 'xml_templates'), trim_blocks=True, lstrip_blocks=True)
template = env.get_template('_POST_PRODUCT_PRICING_DATA_.xml')
class Message(object):
def __init__(self, s, price):
self.SKU = s
self.PRICE = round(price, 2)
self.CURRENCY = USD_CURRENCY
feed_messages = [
Message(sku.sku, new_price),
]
namespace = dict(MerchantId=AMZ_SELLER_ID, FeedMessages=feed_messages)
feed_content = template.render(namespace).encode('utf-8')
print(feed_content)
feed = conn.submit_feed(
FeedType='_POST_PRODUCT_PRICING_DATA_',
PurgeAndReplace=False,
MarketplaceIdList=[MARKETPLACE_ID],
content_type='text/xml',
FeedContent=feed_content
)
feed_info = feed.SubmitFeedResult.FeedSubmissionInfo
print('Submitted product feed: ' + str(feed_info))
while True:
submission_list = conn.get_feed_submission_list(
FeedSubmissionIdList=[feed_info.FeedSubmissionId]
)
info = submission_list.GetFeedSubmissionListResult.FeedSubmissionInfo[0]
submission_id = info.FeedSubmissionId
status = info.FeedProcessingStatus
print('Submission Id: {}. Current status: {}'.format(submission_id, status))
if status in ('_SUBMITTED_', '_IN_PROGRESS_', '_UNCONFIRMED_'):
print('Sleeping and check again....')
time.sleep(60)
elif status == '_DONE_':
feedResult = conn.get_feed_submission_result(FeedSubmissionId=submission_id)
print(feedResult)
break
else:
print("Submission processing error. Quit.")
break
There are a couple of possible reasons, listed roughly in the order of likelihood:
1. Amazon is slower to update values than they say they are. It is possible that although the feed was successful, there is still a period of time before that change reflects on Amazon (even changing values from SellerCentral comes with a messages that it isn't instant).
Wait a few minutes and see if the change eventually shows up.
2. You could have an alternate repricing service still active. If you are currently using another repricer for this SKU, it might be competing with your attempts and reverting the price based on its own ruleset.
It's possible to use the GetFeedSubmissionList call to see if another _POST_PRODUCT_PRICING_DATA_ feed was submitted after yours (though with no way to view the submitted contents).
3. There might be a conflict with the min and max prices on the SKU (whether you set them or not), and the price you tried to set is outside of the allowed range. This is a result of one of Amazon's policies requiring new and updated SKU's to have those set or it uses a default criteria.
In our continued effort to reduce price error risks to sellers and to avoid potentially negative customer experiences, starting on January 14, 2015, you will not be able to use your Seller Central preferences to select a blanket “opt-out” from all potential low- and high-pricing error rules. Instead, you will need to set a minimum and maximum allowed selling price for each product in your inventory if you do not want Amazon’s default potential pricing error rules to apply to that product.
I can't find an announcement page on this so it may have been an email, but it is quoted as such on the forums
Under those circumstances the feed will report back successful (because its references/format are correct), but the price change will silently fail because of the price range limits that are set.
You can verify if this is your issue by viewing the SKU under SellerCentral Manage Inventory page. You may have to turn on the min/max columns to view current values depending on your preferences set for that page.
Unfortunately, there is no way to pull min/max prices on inventory items to know if this will be an issue ahead of time:
Dear Seller,
I am Sharon from Amazon Seller Support and I will be assisting you with your concern today.
From the content of your email, I understand that you are concerned if there's any report where you can download the report for 'Minimum Price' and 'Maximum Price'.
I regret to inform you that as of now the reports which are available will only provide information for 'standard_price' and 'list_price'.
I understand that this is a disappointment for you but please understand that as of not this feature of including 'Minimum Price' and 'Maximum Price' in the inventory reports has not been included and I sincerely apologize for all the inconvenience caused to you in this regard.
via support ticket to Amazon MWS team, Jul 03, 2016
4. It could be possible Amazon does not allow the feed to update a price during an active promotion. You should be able to check if an item is on sale by viewing the SellerCentral Manage Inventory page, where the "price" column would be bordered in green.
Seems unlikely as they require the "StandardPrice" element to be provided with the "Sale" element, but Amazon's own "Automate Pricing" tool lists it as a possible reason for the tool failing.
5. You are applying the price update to the wrong marketplace.
If the id provided to the call under MarketplaceIdList=[MARKETPLACE_ID], is for a different marketplace than the one you are checking, you won't see the price change.
Amazon does fail the feed submission request if you submit to a marketplace you do not have access to, so this may not be the issue if you only have one marketplace.
6. You are looking for the new price in the wrong spot.
If you are looking under the SellerCentral Manage Inventory page, make sure you are looking at the "Price" column and not the "Lowest Price" column.
If you are looking at the product's detail or offer page (on Amazon's storefront), make sure you are looking at your offer. You may not be the main offer shown on the detail page or the top offer shown on the offer listing page.
And of course, make sure you have the right SKU / ASIN.
7. This is for a different feed, but a user has reported that Amazon just doesn't update information sometimes, requiring the feed to be resent.
There is an alternate feed you can try using to update price information _POST_FLAT_FILE_INVLOADER_DATA_, but it is a flat file type (tab delimited) so your XML schema would not transfer over. Probably only worth trying if you think the issue is related to the specific feed you're using.
I don't know Python but your XML looks ok, here is my PHP code which I use to do price change for last 5 years and it works fine. I don't know if this helps you as it's PHP.
$feed = <<< EOD
<?xml version="1.0" encoding="utf-8"?>
<AmazonEnvelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="amzn-envelope.xsd">
<Header>
<DocumentVersion>1.01</DocumentVersion>
<MerchantIdentifier>$merchant_token</MerchantIdentifier>
</Header>
<MessageType>Price</MessageType>
<Message>
<MessageID>1</MessageID>
<Price>
<SKU>$sku</SKU>
<StandardPrice currency="$currency">$new_price</StandardPrice>
</Price>
</Message>
</AmazonEnvelope>
EOD;
$feed = trim($feed);
$feedHandle = #fopen('php://temp', 'rw+');
fwrite($feedHandle, $feed);
rewind($feedHandle);
$parameters = array(
'Merchant' => $MERCHANT_ID,
'MarketplaceIdList' => $marketplaceIdArray,
'FeedType' => '_POST_PRODUCT_PRICING_DATA_',
'FeedContent' => $feedHandle,
'PurgeAndReplace' => false, //Leave this PurgeAndReplace to false so that it want replace whole product in amazon inventory
'ContentMd5' => base64_encode(md5(stream_get_contents($feedHandle), true))
);
rewind($feedHandle);
$request = new MarketplaceWebService_Model_SubmitFeedRequest($parameters);
$return_feed = invokeSubmitFeed($service, $request, $price_change_info_log);
fclose($feedHandle);
I ended up contacting Amazon api support and they found out that it takes up to 15 minutes for the price to change. Also I had another script that uploaded new products and updated the inventory & price for existing products...this script was competing with my repricing script.
I resolved the issue by changing how the second script updates price for existing products.

Sitecore EXM creating contact list via importing csv

I have recently came across scenario where I need to create list via importing csv file. I have few queries to understand how it works
Does importing csv creates contact for each record in csv?
If yes what is the default contact identifier (Name or email)?
Importing contacts for EXM is done by using the List Manager module from Sitecore and there are two ways to import contacts:
Import contacts to the contacts database.
Import contacts and add them to a new Contact list.
When you import contacts from a CSV file, the List Manager allows you to manually map the fields, including the unique indentifier, based on which Sitecore will create a new contact or update an existing.
It's recommended to have a strategy when building this unique identifier, like contact's date of birth or zip code in combination with the first name rather than email address - as a person can have multiple email addresses and it might end up having multiple entries in your contacts database.
So to answer your question, Yes - the List Manager will create a new contact if it doesn't find a match based on the Indentifier you provide. The default identifier is the email address.
Official guideline from Sitecore
Import and export contacts from a list
https://doc.sitecore.net/sitecore_experience_platform/digital_marketing/the_list_manager/creating_lists/import_and_export_contacts_from_a_list
Important
List Manager uses the contact identifier to identify the contacts in your database and to ensure that unnecessary duplicates are not created when you import new contacts. Therefore, before you import a list of contacts, it is important that you create a strategy for the contact identifier. For example, if you use the contact's date of birth or zip code in combination with the first name, this makes a more suitable contact identifier than an email address alone. Using the email address as the contact identifier can result in a contact appearing multiple times in the database if they have more than one email address.
How to add a contact to a list
https://doc.sitecore.net/sitecore_experience_platform/digital_marketing/the_list_manager/creating_lists/add_a_contact_to_a_list
If you do not want to use the email address as the contact identifier, select the Manually map contact identifiers check box and then as the Identifier, select the field in the import file that you want to use as the unique identifier for the contacts.

Django: structuring a complex relationship intended for use with built-in admin site

I have a fairly complex relationship that I am trying to make work with the Django admin site. I have spent quite some time trying to get this right and it just seems like I am not getting the philosophy behind the Django models.
There is a list of Groups. Each Group has multiple departments. There are also Employees. Each Employee belongs to a single group, but some employees also belong to a single Department within a Group. (Some employees might belong to only a Group but no Department, but no Employee will belong only to a Department).
Here is a simplified version of what I currently have:
class Group:
name = models.CharField(max_length=128)
class Department
group = models.ForeignKey(Group)
class Employee
department = models.ForeignKey(Department)
group = models.ForeignKey(Group)
The problem with this is that the Department select box on the Employees page must display all Departments, because a group has not yet been set. I tried to rectify this by making an EmployeeInline for the GroupAdmin page, but it is not good to have 500+ employees on a non-paginated inline. I must be able to use the models.ModelAdmin page for Employees (unless there is a way to search, sort, collapse and perform actions on inlines).
If I make EmployeeInline an inline of DepartmentAdmin (instead of having a DepartmentInline in GroupAdmin), then things are even worse, because it is not possible to have an Employee that does not belong to a Group.
Given my description of the relationships, am I missing out on some part of the Django ORM that will allow me to structure this relationship the way it 'should be' instead of hacking around and trying to make things come together?
Thanks a lot.
It sounds like what you want is for the Department options to only be those that are ForeignKey'ed to Group? The standard answer is that the admin site is only for simple CRUD operations.
But doing what you're supposed to do is boring.
You could probably overcome this limitation with some ninja javascript and JSON.
So first of all, we need an API that can let us know which departments are available for each group.
def api_departments_from_group(request, group_id):
departments = Department.objects.filter(group__id=group_id)
return json(departments) # Note: serialize, however
Once the API is in place we can add some javascript to change the <option>'s on the department select...
$(function() {
// On page load...
if ($('#id_group')) {
// Trap when the group box is changed
$('#id_group').bind('blur', function() {
$.getJSON('/api/get-departments/' + $('#id_group').val() + '/', function(data) {
// Clear existing options
$('#id_department').children().remove();
// Parse JSON and turn into <option> tags
$.each(data, function(i, item) {
$('#id_department').append('<option>' + item.name + '</option>');
});
});
});
}
});
Save that to admin-ninja.js. Then you can include it on the admin model itself...
class EmployeeAdmin(models.ModelAdmin):
# ...
class Media:
js = ('/media/admin-ninja.js',)
Yeah, so I didn't test a drop of this, but you can get some ideas hopefully. Also, I didn't get fancy with anything, for example the javascript doesn't account for an option already already being selected (and then re-select it).