William Heimerman was the long-term director of the Photographers' Gallery and Workshop in South Yarra, where museum quality exhibitions were held from the mid 1970s.
William Heimerman. Photo: Jeff Busby.
The Photographers' Gallery, was established in 1975 by Rod McNicol, Paul Cox, and John Williams, then sold to photographer Ian Lobb who convinced William to assist in running the gallery – from that point known as the Photographers' Gallery & Workshop.
They set out on a journey to exhibit Australian and international photographic work of the highest quality. After a few years of co-directorship, from 1977 until 2015 William was the sole director, energetically seeking out the finest work from Australia, the USA, and Europe.
A long list of renowned Photographers have exhibited at the gallery over the years including Jennifer Aitken, Lynn Bender, Robert Besanko, John Billan, Edouard Boubat, Warren Breninger, Wynn Bullock, Marcus Bunyan, Francis Busby, Jeff Busby, Harry Callahan, Paul Caponigro, Jack Cato, John Cato, Larry Clark, Nigel Clarke, William Clift, Christine Cornish, John Divola, William Eggleston, Rennie Ellis, Greg Elms, Fiona Hall, Franco Fontana, Oliver Gagliani, Ralph Gibson, Christine Godden, Michael Goldsmith, Emmet Gowin, Kylie Hamill, Marion Hardman, Paul Hill, Paul Hopper, Eikoh Hosoe, Graham Howe, Carol Jerrems, Peter Leiss, Jane McLennan, Jean Marc Le Pechoux, Eliot Porter, Ian Lobb, Stephen Lojewski, Rosemary McKeown, Jane McLennan, Rod McNicol, Duane Michals, Lisette Model, Boone Morrison, Harry Nankin, Tony Perry, Lyn Pool, Eliot Porter, Susan Purdy, Kay Rentil, August Sander, Aaron Siskind, Gayle Slater, David Sterry, Virginia Stobart, Henry Talbot, Ingeborg Tyssen, Jerry Uelsmann, Colin Vickery, Les Walkling, Greg Wayn, Brett Weston, and Konrad Winkler.
Many more artists exhibited work in group and student exhibitions.
The gallery also offered workshops in all aspects of photography taught by Australian & international photographers including John Cato, William Clift, Ralph Gibson, Ian Lobb, Stephen Lojewski, and Les Walkling. At the invitation of The Photographers' Gallery renowned photographer Harry Callahan visited Australia to present a public lecture.
Tony Perry in 1980 wrote about the gallery: 'Today the Photographers' Gallery is run solely by Bill Heimerman…with an enviable record of exhibitions and workshops is seen as the premier gallery in Australia; a show there is a genuine honour'.
In 1974 William accepted a teaching position with the Victorian Education Department and taught English at Coburg Technical School. It was here that he met Ian Lobb and the late Carol Jerrems, both avid photographers, who initiated his interest in and appreciation of photography. William subsequently moved to Brighton Technical College where he and other staff members established a photography program. Later William taught photography at Melbourne College of Advanced Education and Council of Adult Education, inculcating students in the techniques of fine printing and the zone system.
William was himself an accomplished photographer, producing work in abstract, portrait and landscape veins. His work is art of great poetry that will be exhibited in a posthumous exhibition that is in preparation.
William passed away on October 1 at Hammond Care in Caulfield after a relatively short period of declining health. William was a brother to Patricia, Jeff, Kevin, Jane, Ted and Robert (dec), brother in law to John, Sarah, Diane, John, Paula, and Susan. Many nieces and nephews, extended relatives and friends, survive him. He will be greatly missed by lifelong friend Barbara Derrick, and the many artists and art collectors that he befriended.
This entry was posted in Deck right, Opinion and tagged bill heimerman, Photographers' Gallery and Workshop, William Heimerman by Keith Shipton. Bookmark the permalink.


Last year I bought a gift from the Gorgeous Feet range at Boots and really wanted to keep it for myself (you can see the review here - it is still in stock). So this year I bought myself something from the range. But I thought I would review it as this would make a lovely extra gift as it's under £5!
On a side note I do seem to be treating myself a lot lately instead of buying Christmas presents - I need to stop this?!
You can find the lovely set here for only £4.50 (normally £6.00 - currently 25% off) which contains - Soothing Foot Soak, Sensationally Smooth Foot Scrub and Deeply Intensive Moisturiser all 50ml in size and they come in lovely glass jars so they don't feel cheap at all.
Now for a long time I really didn't believe in foot scrubs or foot creams... I mean surely a normal body scrub and moisturiser would do the exact same job. But then I read in some over 50's magazine (I'm thinking it was 'Yours' magazine) that the ingredients in feet and hand products are quite different to body products and actually a lot more affective. So I bought a Scholl foot cream and was actually amazed to find it is true - moisturiser really doesn't cut the mustard with me anymore!
Which is why I actually like giving foot care products as gifts... it is slightly odd I know. But if you think about it, in winter we definitely all neglect our feet more and also we do a lot more shopping than usual due to Christmas!
Deeply Intensive Moisturizer - This is the product I have been using the most as this really works, isn't greasy and smells amazing! The consistency of this is slightly thinner than a body butter and really does hydrate the feet instantly. As for the smell it smells exactly like the other two products in the set and it definitely reminds me of the smell of rice pudding! A lovely vanilla scent and quite soft smelling - really nice!
Soothing Foot Soak - Sadly I haven't used this as it contains Sodium laureth sulfate which I don't use (though I'm sure I would be fine to just use this on my feet). But again this smells really good and yummy! This is actually for placing in warm water and soaking the feet in for 10 minutes or you could even place this in a foot spa!
Sensationally Smooth Foot Scrub - I have used this a few times now and again nothing to fault here just a nice foot scrub that isn't at all harsh but does the job. Again the smell of this is really nice.
Also there is a bigger gift set available here for only £7.50 which includes the three products above and seven other products including some cosy socks and a Natural Collection nail polish! Really great value for money I would say.
Plus of course you can do the whole collect free from store at Boots which is very handy when products are on offer!


text:
How to Help a Loved One in a Mental Health Crisis
By Temma Ehrenfeld @temmaehrenfeld
To force the issue, insist that he's "a danger to himself or others."
For about a decade, my dearest love was a man I'll call Hans, who became a suicidal alcoholic. His illness crept up slowly but in the last year of his life he didn't leave his apartment. I was in touch with his psychiatrist and spoke to Hans every day, though he kept refusing to have me visit and sometimes missed his psychiatry appointments.
Hans endearingly had the ability to sound kind and engaged with me, despite his own decline. When he began to seem remote, I knew a shift had occurred, One Sunday, against his wishes, I traveled to see Hans (about a 45-minute trip by subway in New York City) and he didn't answer the bell. I left a note with his super who had his keys, asking him to go in.
When I got Hans on the phone that evening, he was too weak to speak. He was touched I'd tried to see him and said "goodbye." "You're going to die if this goes on," I said. "Good-bye," he said. I called his psychiatrist immediately and left a phone message saying, "I think he said a last goodbye to me. Please get him into the hospital."
This is a cautionary tale: I learned later that the super knocked on the door and left when Hans told him to go away. His psychiatrist tried to reach Hans by phone for four days. I kept calling the psychiatrist and Hans and getting answering machines. On Friday, the psychiatrist finally called to tell me that the police had entered Hans' apartment that morning and discovered him dead.
Hans fell through the cracks in a mental health system riddled with cracks.
Sometimes mental health crises come as a complete surprise to other family members, but that's rare. A psychotic break, bipolar mania or depression, or suicidal depression usually is the latest sign of an evolving illness.
When you see someone developing disturbing symptoms, prepare yourself by finding the mental health services available locally. Check the listings online at the Substance Abuse and Mental Health Services Administration (SAMHA). Should you run into a crisis, a hospital ER may not be your best choice. The waits are long and the psychiatric services limited. Before that day comes, also locate a psychiatrist you trust. A psychiatrist will be one of the first people you speak to when you sense an emergency and provide care after your family member leaves a psychiatric facility.
If you have guns in the house, keep them locked up and unloaded. Keep dangerous medications out of reach, too.
When you're caring for someone in crisis who lives alone, as Hans did, take precautions early on while he's still cooperative, even if it seems overly dramatic at the time. Things can deteriorate fast. Remove guns and unnecessary dangerous medications. Get a set of keys.
Learn as much as you can about the illness and know the danger signs. There are a number of myths about suicide, for example, that will lead you astray. One myth is that a person who talks about suicide just wants attention and isn't serious. Don't count on it. Take it seriously if you hear someone talk about having no reason to live, being a burden to others, feeling trapped, or experiencing unbearable pain. You're right to be alarmed if someone you care about is giving away possessions, visiting or calling people to say goodbye, withdrawing from activities, shutting out family and friends, or acting recklessly or aggressively. Alcohol is estimated to be a factor in at least 25 to 30 percent of all suicides, and in one study, heavy drinking increased the risk of a suicide attempt 16-fold.
When should you call 911 or take someone to a psychiatric facility or emergency room yourself? If you can honestly say, "He's in immediate danger to himself or others."
I had done some of the necessary steps. I was in touch with a psychiatrist and told him when I sensed an emergency. I'd alerted the super who had his keys. But I didn't say the magic words, "He's an immediate danger to himself or others." Many people told me later that I had to say those words to the pscychiatrist or call 911 and say them myself.
Take fast action if you see people showing signs of psychosis that affect their functioning — delusions, or intense fear, extreme agitation, and an inability to sit still or sleep. Behavior changes that come out of nowhere could be a sign of poisoning.
Handling a mental health crisis will wear you down, sometimes in unpredictable ways. I am frugal. I entered a strange mental state on the day Hans wouldn't let me in his apartment. On my way home, I stopped to buy a pair of $300 shoes. No, I couldn't afford them. I mostly wear sneakers and rubber sandals. I did other strange things, too.
Get support from family, friends, or support groups. Mental health crises aren't rare. In 2013, 16 percent of females ages 12 to 17 experienced a significant depression, according to government figures. You already know someone who has seen a family member go through one, or suffered one himself.
Don't be passive and trust that your loved ones as adults will seek the help they need — sadly, it's just not true. SAMHA estimates that about a third of American adults with severe mental illness in 2013 did not receive treatment. People typically go for more than two months between the onset of symptoms and treatment, if they do get it. Many people with severe mental illness don't recognize that they have a problem and may resist treatment or medication. Most people with alcohol and drug problems don't get treated, either.
Don't assume someone else will intervene. It may not be obvious who needs to step up. Hans had a college-age son and an ex-wife who lived nearby. But I knew he was more in touch with me. Clear up this kind of confusion well in advance. You also can't trust medical professionals to act quickly or decisively unless you push. You need to be the advocate.
Persist — anti-psychotics and mood stabilizers dramatically cut the chances that your loved one will commit violence.
After Hans' death, many people said to me, "There was nothing you could do if he really wanted to die." This didn't comfort me. It also isn't true. Hans died because the psychiatrist didn't believe my warning was an emergency and I didn't call 911 myself. I also hadn't educated myself to know the magic words. I didn't have his keys.
Even when a sick person tells you to go away, barge in. Statistics and my gut tell me that but for our inaction Hans would be alive today. He might well be thriving. Most suicidal people get better. Even among people who have been hospitalized because they were suicidal, according to one estimate, less than 9 percent actually die by suicide.
Remember the words "He's an immediate danger to himself or others." Don't let your loved one commit violence against himself — or others. That's one news story in which you never want to read your name.
Christopher Nystuen, MD, MBA

redpajama_set_name: RedPajamaCommonCrawl


text:
Blackfriars Road station
Published on 12th April 2014
What remains of the original Blackfriars station on the Charing Cross railway in London – this signage – was restored in 2005, and is clearly visible within a short distance of the current Tube station at Southwark. The site of the station, now referred to as Blackfriars Road in order to avoid confusion with the modern Blackfriars station on the opposite side of the river Thames, was only open for five years in the late nineteenth century before being replaced by the original Waterloo Junction station, now called Waterloo East.
blackfriars, blackfriars road, charing cross railway, History, London, England, railway, southwark, station, Travel and tourism


You can't speak before an audience and think that others will automatically know what you're saying. You need to fight for their attention and respect. To get results, you must work for it.
Don't give up if you are among those who finds public speaking. Read on to learn all about public speaking.
Use a timer when you practice so you can verify the length of your speech. This ensures your speech fills the time you have. If the speech is not long enough, add more time by finding more information. It is important not to rush through your speech.
You cannot speak in public and believe that others will automatically follow what you are saying. You must work hard to attract their attention and harder to keep it.
Once your speech is memorized, practice it over and over again. Tweak it as necessary. Be sure to master your pace and breathing. Make sure there are pauses between key points so that people can react with applause. Practice your speaking in the environment where you will give your speech.
TIP! Once you've memorized your speech, make sure to practice it repeatedly. Practice it frequently, and make adjustments as you see fit.
Use a timer when you practice so you can verify the length of your written speech is. This will allow you to edit your speech to keep it within the ability to stay on course. If the speech is not long enough, get some more material by doing more research. Never rush when delivering a speech publically.
You need to be very comfortable with the subject of your speech. Research it from various angles so you have a good understanding of the topic. Hone in your points carefully and get your remarks ready in notes that are clear and easy to follow. This will pay off during the question and answer session.
TIP! During your preparation for the speech, it is vital that you have thorough knowledge about the subject you are speaking on. Researching well lets you see all parts of the subject.
Once you can recite your speech from memory at any point of day, then the delivery method you choose can be worked on. Memorizing the speech itself also makes ad libbing on stage more comfortable.
Be aware of the audience you are speaking in front of. Look up the list of attendees at the event. Prior to the speech, even say hello to some of them at the door. Getting familiarized with some of the audience can make the event feel friendlier.
Always face the audience when speaking. Do not turn your attention to things in the area.
Practice all that you can prior to your speech. You can practice before your mirror or make a recording you can play back. However, doing a practice run for family or friends is ideal, as they will be able to critique you well.
TIP! Practicing is the best way to ensure that you know what you are going to say. You can practice before your mirror or make a recording you can play back.
Make sure you have a good understanding about your topic before preparing your speech.Do broad research that allows you to see the topic from all sides. You will be better prepared when you know how to answer their questions.
Even if you are speaking at an informal event, dress nicely. You will speak more confidently, if you are dressed nicely. Wear a necktie if you want to draw the audience's attention towards your face.
Know the audience that you will be talking to. Having a personal familiarity with a few people in the crowd can make it seem more friendly.
Be sure you are very familiar with your presentation so you'll feel confident when speaking publicly. Pick something that is interesting to you. Keep a conversational tone and impress your audience.
TIP! Stay as confident as possible when in front of a crowd. Select a topic that is truly of interest to you, preferably one with which you have first-hand experience.
You can become better at public speaking by telling a story to engage your audience.Make sure to outline your speech prior to beginning your speaking engagement. Be sure the story is true life events in order to sound really authentic.
Bring the audience to your side, and then begin speaking. Smile when entering the room, and you can try shaking hands if you're able. This helps you to appear positive and friendly, and it makes people interested in the contents of your speech.
TIP! Begin by engaging the audience. If possible, walk around and personally greet people, but always remember to smile.
Become familiar with the venue in which you will be giving your public speech. Test the acoustics and the mic to see how much you aren't using a microphone. Use the equipment is available and get used to it. Learn how to use any of the visual aids that might be present. Get an idea of how much range of eye contact you will need to make.
Try practicing your speech every day. You will feel more at ease with the material once you have done this. Even if your speech is memorized, it's still worth bringing some notes when you actually deliver the speech. The notes will help you out in case you do forget something important.
Stopping somewhere in the sentence may ruin the entire speech. In addition, the audience is not going to know that you missed a small portion of your prepared speech.
Be certain you have visual aids that are not the main attraction. These aids should only be used to help illustrate key points as an enhancement. They should not take over the entire presentation. Use visual aids of a high quality when you want to illustrate a key point. Your visual aids should be attractive and colorful; however, they should not distract your audience from your speech.
If a speech is coming up on the horizon, use visualization techniques. Also consider the audience's reactions in your visualization. When you see yourself speaking effectively in front of an attentive, clapping audience, then you can really help your self-esteem for the real event.
TIP! If you have a public speech coming up, try picturing the speech in your mind. You want to visualize the speech and the audience reactions.
Practice making your speech you are going to give every day. This can boost your confidence when it comes time to deliver your speech. Even though you might think you know your speech by heart, bring the notes with you when you take the podium.
Don't wait until your speech wraps up to let the audience ask questions. They may not be able to remember what they need to ask. Most people will appreciate a time to ask questions when needed.
Practice your speech until you possibly can. Do it in front of the mirror and watch yourself. They may not have noticed and help you improve.
Warm the audience up before you give a speech. You may begin with a joke or some other ice breaker. You can talk about something from your life experience that you think is relevant to the topic. This can establish that first connection with the audience.
TIP! Get the audience warmed up before starting your presentation. You don't have to tell a joke.
Most people do not want to give a speech to a group of people. If you have the right tools, it need not bring you anxiety. Use the tips above and speak like a pro.
Never try to wing it. This idea can seriously backfire on you, no matter how extensive your knowledge on the subject is or how much passion you have for it. You should be able to string together some sentences. On the other hand, at the end of the speech, you will immediately regret all the critical points you didn't make.


I have recently been re-reading the book by Simon Sinek; Start with WHY. If you have not read this book or viewed his TED video it is well worth your time. I have taken it upon myself to write out my own WHY statement. Specifically, WHY I help the owners of private companies create value and plan for the transition from their business. I found the process very enlightening and learned a lot about me that I had not previously understood. All of this unleased a passion within that I did not know existed and upon which I can grow my business.
Passion is the essence of what Simon is trying to get across. According to Simon, people do not buy what you do; they buy why you do it. Think about this statement from your own buying patterns. Are you drawn to someone who presents all the features of a product or are you drawn to someone who can speak passionately about the benefits? I was at a wine tasting the other night. There were about 10 different tables with wines from all over. All of the table hosts where very competent in describing the wines, where they came from and what grapes were used. There was one table however that I could not leave. The wines were all of high quality with high ratings, but more than that the table host spoke passionately about the wines and the process used to produce very unique tastes. It made me want to buy everything at the table and he never once tried to sell me.
In the example above all the other table hosts had a job to do, this one had a clear understanding about WHY he was doing what he did, and it showed. Now in my opinion, this process is true whether you are looking for a full time position or building a consulting practice. What is the WHY behind what you are selling? Will the passion for your WHY come out during an interview, or will you just speak to how you can do the job? Can you describe with passion, the benefits that you bring to your future employer?
I was recently at the annual partner meeting of B2B CFO®, the firm for which I am a partner. At the meeting we unveiled a corporate why statement, something that every company should do. Attached is a link to the video of our WHY statement: https://www.b2bcfo.com/. This video describes why we as a firm exist and follows along very closely with my own why statement; which is to serve the owners of privately held companies.


text:
Adam Shefter's Wife Sharri Shefter
Friday, December 5, 2014 Saturday, June 11, 2016 by PW Staff
Meet Sharri Maio Shefter, wife of NFL Insider and analyst for ESPN, Adam Shefter.
Adam and Sharri first met each other on July 4th, 2006, after being set up on a blind date by one of Adam's friends.
After dating for awhile, Adam decided she was the right woman and presented Sharri with a half a carat ring and asked her to marry him. She said "Yes!" Soon after, Sharri and her son (from a previous marriage) moved in with Adam and they got married in New York in 2007. Schefter has since adopted Sharri's son, Devon.
Maio's first husband, Joseph, was killed during the September 11th, 2001 attacks while he was working for Cantor Fitzgerald in downtown Manhattan.
Photo Source/Credit: Adam Shefter/Twitter
Categories Adam Shefter Wife Sports Analyst ReportersTags Adam Shefter's bio Adam Shefter's family Adam Shefter's girlfriend Adam Shefter's kids Adam Shefter's married Adam Shefter's twitter Adam Shefter's Wife Adam Shefter's wiki Sharri Maio Shefter Sharri Maio's bio
Ilya Bryzgalov's Wife Zjenia Bryzgalov
Who is Heidi Watney's Husband/Boyfriend?
Adam Gase's Wife Jennifer Gase
Adam Wainwright's Wife Jenny Curry
Adam Lallana's Wife Emily Jubb
Adam Dunn's Wife Rachel Dunn
Adam Johnson's Girlfriend Stacey Flounders
Adam LaRoche's Wife Jennifer LaRoche


text:
Mace Announces Irish Hospice Foundation As New Charity Partner
Published on Oct 14 2014 6:51 AM in Retail
BWG Foods' Mace brand has unveiled The Irish Hospice Foundation as its new charity partner for 2014/15, with the goal of raising €100,000 for the charity. In particular, the partnership will focus spe...
BWG Foods' Mace brand has unveiled The Irish Hospice Foundation as its new charity partner for 2014/15, with the goal of raising €100,000 for the charity. In particular, the partnership will focus specifically on fundraising for the Foundation's Nurses for Night Care program.
Mace will kick off its partnership with the sale of Halloween chocolate lollipops for €1 at stores nationwide, proceeds of which will go to the charity, as well as donating 10 cent from bottles of MACE water purchased along with a contribution from 50 other everyday items in the MACE Own Brand Range.
"For the first time, we asked retailers to decide who the MACE charity partner should be and the Irish Hospice Foundation stood out because the charity is centered on a cause that touches a lot of people and carries out great work in local communities," said Alex Banahan, Sales Director, MACE.
"Also, it has a nationwide footprint and this was very important to our retailers who also operate right across the country. We have an ambitious target to raise €100,000 for the charity, so we will be working with our independent retailers on several national and store level initiatives, and hope that our customers will be keen to show their support."
The Nurses for Night Care programme helps non-cancer patients with life limiting illness end their days at home surrounded by their families.
Retail spending in Dublin recorded further growth in the fourth quarter of 2021 as consumer demand remained strong in the capital's economy.
Irish Consumer Sentiment Recovers In January, Says KBC Bank
Grafton Street, Dublin city.


iTrue provides Self-Sovereign Biometric Identity for Decentralized Web. "Self-Sovereign" also known as self-ownership is the idea that people should own their own data. Using iTrue, businesses cannot just freely use data from users, they must request the data and the user must consent to this usage. Users are rewarded each time they decide to share data with business but it is a voluntary sharing of data. "Biometric Identity" means they can offer face identification online and offline (i.e. visitors to trade centers, hotels, conferences, taxis, etc. so businesses know when to reward or punish someone in case they misbehaved).
True provides Self-Sovereign Biometric Identity for Decentralized Web. "Self-Sovereign" also known as self-ownership is the idea that people should own their own data. Using iTrue, businesses cannot just freely use data from users, they must request the data and the user must consent to this usage. Users are rewarded each time they decide to share data with business but it is a voluntary sharing of data. "Biometric Identity" means they can offer face identification online and offline (i.e. visitors to trade centers, hotels, conferences, taxis, etc. so businesses know when to reward or punish someone in case they misbehaved).
Mikhail Podlinev: Chief Operating Officer.
Abhishek Gangwar: Chief CV Engineer.
iTrue ICO began on December 21th, 2018. The ICO token supply represents 60% of the total token supply, so there is a total of 4,800,000,000 tokens available, for 0,003 USD each. The ICO funding target is unknown, the funding cap is 11,000,000 USD and is expected to end on March 21th, 2019 or when the funding cap is reached. Any unsold/remaining tokens will be burned.
iTrue ICO campaign features Bonus and bounties.


text:
from collections import defaultdict
from gettext import gettext as _
from os.path import basename
from typing import Optional
import os

from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import Q

import subdomains.utils
from autoslug import AutoSlugField
from autoslug.utils import slugify
from sanitizer.models import SanitizedCharField

from dataporten.models import DataportenUser

DEFAULT_STUDY_PROGRAM_SLUG = getattr(settings, 'DEFAULT_STUDY_PROGRAM_SLUG', 'fysmat')

def upload_path(instance, filename):
    """
    For setting the upload_to parameter of FileFields and ImageFields,
        e.g. field_name = FileField(upload_to=upload_path)
    Returns a MEDIA_ROOT file path that is quite descriptive as it uses
    the instance.__str__ method. It is assumed that this method is defined.
    """
    return os.path.join(instance.__class__.__name__, slugify(str(instance)), filename)

def norwegian_slugify(instance: models.Model) -> str:
    """
    Given a norwegian phrase instance.display_name,
    replace norwegian characters with latin characters
    """
    text = instance.display_name
    replacements = [('æ', 'e'), ('ø', 'o'), ('å', 'a')]
    for (nor, eng) in replacements:
        text = text.replace(nor, eng)

    return text

class StudyProgram(models.Model):
    """
    Contains top level information for a specific study program.
    """
    full_name = models.CharField(
        _('fullt navn'),
        max_length=60,
        help_text=_('F.eks. "Fysikk og matematikk"')
    )
    display_name = models.CharField(
        _('visningsnavn / kallenavn'),
        max_length=60,
        help_text=_('F.eks. "Fysmat"')
    )
    slug = AutoSlugField(
        populate_from=norwegian_slugify,
        always_update=True,
        unique=True
    )
    has_archive = models.BooleanField(
        _('har arkiv'),
        default=False,
        help_text=_('Huk av hvis studieprogrammet har filer i arkivet på wikilinks.no/arkiv.')
    )
    published = models.BooleanField(
        _('publisert'),
        default=False,
        help_text=_('Studieprogrammet dukker ikke opp i studieprogramlisten i navigasjonsbaren før det er publisert, '
                    'men det er fortsatt mulig å besøke studieprogrammet manuelt (URL: wikilinks.no/visningsnavn) '
                    'for å teste resultatet før du publiserer.')
    )

    @property
    def simple_semesters(self):
        """
        Returns all of the study program's semesters that are not part of any main profile
        """
        return self.semesters.filter(main_profile=None, published=True)

    @property
    def grouped_split_semesters(self):
        """
        Grouping the split semesters (semesters part of a main profile) by semester.number
        """
        split_semesters = self.semesters.exclude(main_profile=None).filter(published=True)
        _grouped_split_semesters = defaultdict(list)
        for split_semester in split_semesters:
            _grouped_split_semesters[split_semester.number].append(split_semester)
        return _grouped_split_semesters.items()

    @property
    def resource_link_lists(self):
        """
        Get the ResourceLinkLists, falling back on the default ones if there are no custom ones for the study program
        """
        # TODO: Return QuerySets instead, making prefetch_related possible.
        if self._resource_link_lists.exists():
            _resource_link_lists = self._resource_link_lists.all()
        else:
            _resource_link_lists = ResourceLinkList.objects.filter(default=True)[:2]
        return _resource_link_lists

    def check_access(self, user):
        return self in user.contributor.accessible_study_programs()

    def get_absolute_url(self):
        return reverse('semesterpage-studyprogram', args=[self.slug])

    def __str__(self):
        return self.full_name

    class Meta:
        ordering = ['display_name']
        verbose_name = _('studieprogram')
        verbose_name_plural = _('studieprogram')

class MainProfile(models.Model):
    """
    Contains top level information about a main profile within a given study
    program.
    """
    full_name = models.CharField(
        _('fullt navn'),
        max_length=60,
        help_text=_('F.eks. "Industriell matematikk"')
    )
    display_name = models.CharField(
        _('visningsnavn / kallenavn'),
        max_length=60,
        help_text=_('F.eks. "InMat"')
    )
    study_program = models.ForeignKey(
        StudyProgram,
        on_delete=models.CASCADE,
        related_name='main_profiles',
        verbose_name=_('studieprogram')
    )
    slug = AutoSlugField(
        populate_from=norwegian_slugify,
        always_update=True,
        unique_with='study_program'
    )

    def check_access(self, user):
        return self in user.contributor.accessible_main_profiles()

    def get_absolute_url(self):
        return reverse(
            'semesterpage-mainprofile',
            args=[self.study_program.slug, self.slug],
        )

    def __str__(self):
        return self.display_name + ', ' + str(self.study_program)

    class Meta:
        ordering = ['display_name']
        verbose_name = _('hovedprofil')
        verbose_name_plural = _('hovedprofiler')

class Semester(models.Model):
    """
    Contains an instance of a specific semester connected to a main profile.
    """
    number = models.PositiveSmallIntegerField(
        _('semesternummer'),
        help_text=_('F.eks. "2"')
    )
    study_program = models.ForeignKey(
        StudyProgram,
        on_delete=models.CASCADE,
        related_name='semesters',
        verbose_name=_('studieprogram')
    )
    main_profile = models.ForeignKey(
        MainProfile,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        default=None,
        related_name='semesters',
        verbose_name=_('hovedprofil')
    )
    has_electives = models.BooleanField(
        _('valgfag'),
        default=False,
        help_text=_('Huk av hvis dette semesteret har valgfag. I så fall vises et ekstra generisk "valgfag" på '
                    'nettsiden til semesteret, hvor brukeren bes om å logge på for å velge valgfagene.'),
    )
    published = models.BooleanField(
        _('publisert'),
        default=False,
        help_text=_('Semesteret dukker ikke opp i navigasjonsbaren før det er publisert, men det er fortsatt mulig å '
                    'besøke semesteret manuelt (URL: wikilinks.no/studieprogram/hovedprofil/semesternummer) for å '
                    'teste resultatet før du publiserer.'),
    )

    @staticmethod
    def get(
        study_program: str,
        main_profile: Optional[str] = None,
        number: Optional[str] = None,
    ) -> 'Semester':
        """
        Retrieves a semester, given the display name slug of its related study
        program, and optionally its main profile.

        If main profile is not specified, it retrieves a 'common' semester
        without any main profile attachment.

        If number is not specified, it returns the lowest available semester
        satisfying the criteria.

        Raises Semester.DoesNotExist if nothing matches the given arguments.
        """
        q = Q(study_program__slug__iexact=study_program)

        if main_profile:
            # Semester related to a main profile
            q = q & Q(main_profile__slug__iexact=main_profile)

        if number:
            q = q & Q(number=int(number))
            return Semester.objects.get(q)
        else:
            # No number has specified, and we fall back to the lowest available
            # semester.
            try:
                return Semester.\
                    objects.\
                    filter(q).\
                    order_by('-main_profile', 'number')[0]
            except IndexError:
                # No matching semester, since empty list has been returned
                raise Semester.DoesNotExist

    def check_access(self, user):
        return self in user.contributor.accessible_semesters()

    def get_archive_url(self):
        # Returns the url to the archive section for the semester. Note the use of title().
        if self.main_profile:
            return subdomains.utils.reverse(
                'semesterpage-splitarchive',
                subdomain='arkiv',
                args=[self.study_program.slug.title(), self.number, self.main_profile.slug.title()]
            )
        else:
            return subdomains.utils.reverse(
                'semesterpage-simplearchive',
                subdomain='arkiv',
                args=[self.study_program.slug.title(), self.number]
                )

    def get_absolute_url(self):
        kwargs = {
            'study_program': self.study_program.slug,
            'semester_number': self.number,
        }
        if self.main_profile:
            kwargs['main_profile'] = self.main_profile.slug

        return reverse('semesterpage-semester', kwargs=kwargs)

    def __str__(self):
        if self.main_profile is not None:
            string = self.main_profile.display_name + ' '
        else:
            string = 'Felles '
        string += str(self.number) + '. semester, '
        string += self.study_program.full_name

        return string

    class Meta:
        ordering = ['main_profile__display_name', 'number']
        verbose_name = _('semester')
        verbose_name_plural = _('semestere')

class LinkList(models.Model):
    """
    An abstract model which Course and ResourceLinkList derive from. It is
    displayed as an article element on the semesterpage view / courses template.
    """
    full_name = models.CharField(
        _('fullt navn'),
        max_length=120,
        help_text=_('F.eks. "Prosedyre- og Objektorientert Programmering"')
    )
    # When display name is set as blank, the display name is deduced
    # from full_name instead
    display_name = models.CharField(
        _('visningsnavn'),
        blank=True,
        max_length=60,
        help_text=_('Dette er teksten som vises under logen til faget i '
                    'fagoversikten. F.eks. "C++".'),
    )
    safe_logo = models.ImageField(
        verbose_name=_('logo'),
        upload_to=upload_path,
        help_text=_('Bildet vises over alle lenkene knyttet til faget. '
                    'NB! Bildet skaleres automatisk til å bli kvadratisk, '
                    '.svg bilder er ikke støttet.'),
        blank=True,
        null=True
    )
    # Allows admins to upload potentially unsafe SVG logos if so desired
    unsafe_logo = models.FileField(
        verbose_name=_('SVG logo'),
        upload_to=upload_path,
        blank=True,
        null=True
    )
    homepage = models.URLField(
        _('Fagets hjemmeside'),
        blank=True,
        help_text=_('F.eks. "http://www.phys.ntnu.no/fysikkfag/". '
                    'Denne lenken kan besøkes ved å trykke på ikonet til faget.')
    )

    @property
    def logo(self):
        # If an 'unsafe' SVG is uploaded by the admin, use that one
        if self.unsafe_logo:
            return self.unsafe_logo
        else:
            return self.safe_logo

    def __str__(self):
        return self.full_name

    def clean(self):
        # Can't allow upload of both a safe logo and an unsafe one
        if self.safe_logo and self.unsafe_logo:
            raise ValidationError(_('En administrator har allerede satt en logo for dette faget, '
                                    'og du kan derfor ikke velge en ny logo.'))

    class Meta:
        abstract = True

class Course(LinkList):
    """
    Contains a specific course with a logo for display on the semesterpage.
    Can be connected to several different semesters.
    """
    # We do a lot of filtering by course_code, so we index this collumn
    course_code = models.CharField(
        _('emnekode'),
        unique=True,
        db_index=True,
        max_length=10,
        help_text=_('F.eks. "TDT4102"')
    )
    semesters = models.ManyToManyField(
        Semester,
        blank=True,
        related_name='courses',
        verbose_name=_('semestre'),
        help_text=_('Hvis du lager et fag for deg selv, så kan du bare hoppe over dette valget.')
    )
    contributors = models.ManyToManyField(
        'Contributor',
        related_name='courses',
        blank=True,
        help_text=_('Bidragsytere som har redigeringstilgang til faget.')
    )
    created_by = models.ForeignKey(
        'Contributor',
        related_name='created_courses',
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        help_text=_('Hvem som opprettet faget.')
    )
    # The unique id provided from dataporten, intended for future db_index when the entire
    # database has been provided with dataporten_uid
    dataporten_uid = models.CharField(
        _('dataporten uid'),
        unique=True,
        null=True,
        max_length=60,
        help_text=_('Dataporten unik id'),
    )

    def check_access(self, user):
        return self in user.contributor.accessible_courses()

    def get_admin_url(self):
        info = (self._meta.app_label, self._meta.model_name)
        return reverse('admin:%s_%s_change' % info, args=(self.pk,))

    def get_absolute_url(self):
        if self.semesters.exists():
            return self.semesters.all()[0].get_absolute_url()
        else:
            try:
                return Options.objects.filter(
                    self_chosen_courses__in=[self],
                )[0].get_absolute_url()
            except IndexError:
                return reverse('semesterpage-homepage')

    def save(self, *args, **kwargs):
        """
        Custom save method in order to guarantee that the course code 'TMA2400'
        is entirely capaitalized.
        """
        self.course_code = self.course_code.upper()
        super(Course, self).save(*args, **kwargs)

    @property
    def short_name(self):
        """
        Gives the name of the course, used as the title of the course in templates.
        Is generated such that it does fit in the column representation in the HTML/CSS.
        """
        # If display name is set, use this in the template
        if self.display_name:
            return self.display_name

        # If the full name is short enough, just use it
        if len(self.full_name) <= 12:
            return self.full_name

        # Upper case acronym of the full name
        acronym = ''.join(
            word[0]
            for word
            in self.full_name.split()
        ).upper()

        # One letter acronyms don't make sense
        if len(acronym) > 1:
            return acronym

        # Last option:
        # Truncate the full name with ellipses
        return self.full_name[0:11] + '...'

    @property
    def url(self) -> str:
        """
        Gives the url which the course logo and header should redirect to. If
        the course has no homepage url set, it should redirect to the course
        admin instead.
        """
        if self.homepage:
            return self.homepage
        else:
            return self.get_admin_url()

    def __str__(self):
        return self.course_code + ' - ' + self.full_name

    class Meta:
        ordering = ['display_name']
        verbose_name = _('fag')
        verbose_name_plural = _('fag')

def course_file_path(instance: 'CourseUpload', filename: str) -> str:
    """
    Returns the file path of a user uploaded course file, used in the upload_to
    parameter of the FileField.
    """
    return os.path.join(
        'fagfiler',
        slugify(instance.course.course_code).upper(),
        filename,
    )

class CourseUpload(models.Model):
    course = models.ForeignKey(
        Course,
        related_name='uploads',
    )
    file = models.FileField(
        upload_to=course_file_path,
    )
    author = models.ForeignKey(
        User,
        blank=True,
        null=True,
        related_name='course_uploads',
    )
    display_name = models.CharField(
        _('visningsnavn'),
        blank=True,
        null=False,
        max_length=60,
        help_text=_('F.eks. "Kompendium"'),
    )
    order = models.PositiveSmallIntegerField(
        _('rekkefølge'),
        default=0,
        blank=False,
        null=False,
        help_text=_(
            'Bestemmer hvilken rekkefølge filene skal vises i. Lavest kommer '
            'først.'
        ),
    )

    @property
    def url(self) -> str:
        return self.file.url

    @property
    def filename(self) -> str:
        return basename(self.file.name)

    def check_access(self, user):
        return self in user.contributor.accessible_course_uploads()

    def __str__(self) -> str:
        if self.display_name:
            return self.display_name
        else:
            return self.filename

    class Meta:
        verbose_name = _('fil')
        verbose_name_plural = _('filer')
        ordering = ('order',)

class ResourceLinkList(LinkList):
    """
    Used to portray resource links which are common across all the semesters.
    Almost identical to Course, except for not being connected to any specific
    semester, but a study program instead, and not having a course code.
    """
    study_programs = models.ManyToManyField(
        StudyProgram,
        blank=True,
        related_name='_resource_link_lists',
        verbose_name=_('studieprogram')
    )
    default = models.BooleanField(
        default=False,
        verbose_name=_('standard ressurslenkeliste'),
        help_text=_('Skal denne ressurslenkelisten brukes i alle studieprogram som ikke har satt sine egendefinerte '
                    'ressurslenkelister?')
    )
    order = models.PositiveSmallIntegerField(
        default=0,
        blank=False,
        null=False,
        verbose_name=_('Rekkefølge'),
        help_text=_('Bestemmer hvilken rekkefølge ressurslenkelistene skal vises i. Lavest kommer først.')
    )

    def check_access(self, user):
        return self in user.contributor.accessible_resource_link_lists()

    class Meta:
        ordering = ['order']
        verbose_name = _('Ressurslenkeliste')
        verbose_name_plural = _('Ressurslenkelister')

class CustomLinkCategory(models.Model):
    """
    Contains a category for the link model, including a thumbnail. The
    thumbnail is used on the semester page for styling the list item containig
    the link. Allows a custom thumbnail when none of the defaults will do.
    Only available in ResourceLinkList.
    """
    name = models.CharField(
        _('Egendefinert kategori'),
        unique=True,
        max_length=60
    )
    thumbnail = models.FileField(
        _('ikon for kategori'),
        upload_to=upload_path,
        blank=True
    )

    def check_access(self, user):
        return self in user.contributor.accessible_custom_link_categories()

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = _('lenkekategori')
        verbose_name_plural = _('lenkekategorier')

# Filenames for static thumbnails for the default link categories
# The thumbnails must be stored in:
#       semesterpage/static/semesterpage/link_categories/
TASKS = 'tasks.svg'
SOLUTIONS = 'solutions.svg'
BLACKBOARD = 'blackboard.svg'
VIDEOS = 'videos.svg'
TIMETABLE = 'timetable.svg'
SYLLABUS = 'syllabus.svg'
FORMULAS = 'formulas.svg'
EXAMS = 'exams.svg'
INFO = 'info.svg'
IMPORTANT_INFO = 'important_info.svg'
NTNU = 'ntnu.svg'
WIKIPENDIUM = 'wikipendium.svg'
BOOK = 'book.svg'
QUIZ = 'quiz.svg'
FACEBOOK = 'facebook.svg'
SOFTWARE = 'software.svg'
CHEMISTRY = 'chemistry.svg'

# Descriptive text of the link category choices
DEFAULT_LINK_CATEGORIES = (
    (TASKS, _('Øvinger og prosjekter')),
    (BLACKBOARD, _('Blackboard')),
    (SOLUTIONS, _('Løsningsforslag')),
    (VIDEOS, _('Videoforelesninger')),
    (TIMETABLE, _('Framdrifts- og timeplaner')),
    (SYLLABUS, _('Pensum')),
    (FORMULAS, _('Formelark')),
    (EXAMS, _('Eksamener')),
    (FACEBOOK, _('Facebook')),
    (INFO, _('Informasjon')),
    (IMPORTANT_INFO, _('Viktig informasjon')),
    (NTNU, _('NTNU-lenker')),
    (WIKIPENDIUM, _('Wikipendium')),
    (BOOK, _('Pensumbok')),
    (QUIZ, _('Quiz og punktlister')),
    (SOFTWARE, _('Programvare')),
)

class Link(models.Model):
    """
    Contains a URL link connected to a specific course. The link category
    field determines the mini-icon used when portraying the link as a part
    of a list with custom bullet-point thumbnails
    """
    url = models.URLField(
        'URL',
        help_text=_('F.eks. "http://www.phys.ntnu.no/fysikkfag/gamleeksamener.html"')
    )
    category = models.CharField(
        _('Kateogri'),
        blank=True,
        null=True,
        default=None,
        max_length=60,
        choices=DEFAULT_LINK_CATEGORIES,
        help_text=_('F.eks. "Løsningsforslag". Valget bestemmer hvilket '
                    '"mini-ikon" som plasseres ved siden av lenken.')
    )
    custom_category = models.ForeignKey(
        CustomLinkCategory,
        default=None,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        verbose_name=_('(Egendefinert kategori)'),
        help_text=_('Hvis du ønsker å bruke et egendefinert "mini-ikon".')
    )
    order = models.PositiveSmallIntegerField(
        _('rekkefølge'),
        default=0,
        blank=False,
        null=False,
        help_text=_('Bestemmer hvilken rekkefølge lenkene skal vises i. Lavest kommer først.')
    )

    def __str__(self):
        return self.title

    def clean(self):
        # Can't allow selection of both a category and a custom category at the
        # same time
        if self.category is not None and self.custom_category is not None:
            raise ValidationError(_('Kan ikke velge både en kateogri '
                                    'og en egendefinert kateogri for en lenke '
                                    'samtidig. Du kan kun velge én av delene, '
                                    'eller ingen av delene. Denne meldingen kan '
                                    'dukke opp når en administrator allerede har '
                                    'satt en kategori for denne lenken.'))

    class Meta:
        abstract = True
        ordering = ('order',)

class CourseLink(Link):
    """
    A link connected to a course, without the ability to add custom thumbnails.
    """
    title = SanitizedCharField(
        verbose_name=_('tittel'),
        allowed_tags=['em', 'strong', 'i', 'b'],
        max_length=100,
        strip=False,
        help_text=_('F.eks "Gamle eksamenssett"')
    )
    course = models.ForeignKey(
        Course,
        verbose_name=_('fag'),
        on_delete=models.CASCADE,
        related_name='links'
    )

    def check_access(self, user):
        return self in user.contributor.accessible_course_links()

    def __str__(self):
        return self.title + ' ('+ str(self.course.course_code) + ')'

    class Meta(Link.Meta):
        verbose_name = _('lenke')
        verbose_name_plural = _('lenker')

class ResourceLink(Link):
    """
    A link connected to one of the two ResourceLinkList elements displayed on
    each semesterpage, with the ability to add custom thumbnails.
    """
    title = SanitizedCharField(
        verbose_name=_('tittel'),
        allowed_tags=['em', 'strong', 'i', 'b'],
        max_length=100,
        strip=False,
        help_text=_('F.eks "Wolfram Alpha"')
    )
    resource_link_list = models.ForeignKey(
        ResourceLinkList,
        on_delete=models.CASCADE,
        related_name='links',
        verbose_name=_('ressurslenkeliste')
    )

    def check_access(self, user):
        return self in user.contributor.accessible_resource_links()

    def __str__(self):
        return self.title + ' (' + str(self.resource_link_list.full_name) + ')'

    class Meta(Link.Meta):
        verbose_name = _('ressurslenke')
        verbose_name_plural = _('ressurslenker')

NO_ACCESS = 0
COURSES = 1
SEMESTER = 2
MAIN_PROFILE = 3
STUDY_PROGRAM = 4
ACCESS_LEVELS = (
    (NO_ACCESS, _('Ingen tilgang')),
    (COURSES, _('Opprettede fag')),
    (SEMESTER, _('Kun semesteret')),
    (MAIN_PROFILE, _('Hele hovedprofilen')),
    (STUDY_PROGRAM, _('Hele studieprogrammet')),
)

class Contributor(models.Model):
    """
    A student contributor connected to a given semester, with a one-to-one relationship to the User model. Has an access_level
    field used to grant edit and delete permissions to Semesterpage models in the admin panel.
    All permissions relevant data goes into this model, while any user preference related data goes into Options.
    """
    user = models.OneToOneField(
        User,
        related_name='contributor',
        blank=False,
        null=False,
        on_delete=models.CASCADE,
        verbose_name=_('bruker')
    )
    access_level = models.SmallIntegerField(
        _('tilgangsnivå'),
        choices=ACCESS_LEVELS,
        default=COURSES,
        help_text=_('Gir muligheten til å endre på lenker o.l. knyttet til semesteret spesifisert nedenfor.')
    )
    semester = models.ForeignKey(
        Semester,
        blank=True,
        null=True,
        default=None,
        on_delete=models.SET_NULL,
        related_name='contributors',
        verbose_name=_('bidragsytersemester')
    )

    @property
    def study_program(self):
        try:
            return self.semester.study_program
        except AttributeError:
            # Contributor semester not set by the site administrator
            return StudyProgram.objects.none()

    @property
    def main_profile(self):
        try:
            return self.semester.main_profile
        except AttributeError:
            # Contributor semester not set by the site administrator
            return MainProfile.objects.none()

    """
    Methods for retrieving the model instances that should be accessible to the contributor.
    """
    def has_contributor_access_to(self, object):
        if self.user.is_superuser:
            return True
        else:
            # This check was the only way to fix a bug related to
            # CourseUploadInline for non superusers. The inline never appears
            # if this is not done. TODO: Find the reason why.
            if object is None:
                return True
            try:
                return object.check_access(self.user)
            except AttributeError:
                return False

    def accessible_study_programs(self):
        if self.access_level == STUDY_PROGRAM:
            return StudyProgram.objects.filter(pk=self.study_program.pk)
        else:
            return StudyProgram.objects.none()

    def accessible_main_profiles(self):
        if self.access_level == SEMESTER or self.access_level == COURSES:
            return MainProfile.objects.none()
        elif self.access_level == MAIN_PROFILE:
            return MainProfile.objects.filter(semesters__in=[self.semester])
        elif self.access_level == STUDY_PROGRAM:
            return self.study_program.main_profiles.all()
        else:
            raise RuntimeError('Invalid contributor access level.')

    def accessible_semesters(self):
        if self.access_level == SEMESTER:
            return Semester.objects.filter(pk=self.semester.pk)
        elif self.access_level == MAIN_PROFILE:
            return Semester.objects.filter(study_program=self.study_program, main_profile=self.main_profile)
        elif self.access_level == STUDY_PROGRAM:
            return self.study_program.semesters.all()
        elif self.access_level == COURSES:
            return Semester.objects.none()
        else:
            raise RuntimeError('Invalid contributor access level.')

    def accessible_courses(self):
        # If the user has access to a 'parent object', i.e. the related
        # semester, then we grant access to all children
        parent_access = Q(semesters__in=self.accessible_semesters())

        # Also includes courses where the contributor is part of the contributors ManyToManyField,
        # which the user is added to if he/she created the course
        earlier_contributor = Q(contributors__in=[self])
        access_criterion = parent_access | earlier_contributor

        # If the user has recently taken the course, according to dataporten,
        # we grant access. Here, recently means either the current semester,
        # or the last one
        if isinstance(self.user, DataportenUser):
            recent_course = Q(course_code__in=self.user.dataporten.courses.less_semesters_ago(than=2))
            access_criterion = access_criterion | recent_course

        return Course.objects.filter(access_criterion).distinct()

    def accessible_resource_link_lists(self):
        return ResourceLinkList.objects.filter(study_programs__in=self.accessible_study_programs())

    def accessible_custom_link_categories(self):
        return CustomLinkCategory.objects.filter(links__resource_link_list__study_programs__in=self.accessible_study_programs())

    def accessible_course_links(self):
        return CourseLink.objects.filter(course__in=self.accessible_courses())

    def accessible_course_uploads(self):
        return CourseUpload.objects.filter(course__in=self.accessible_courses())

    def accessible_resource_links(self):
        return ResourceLink.objects.filter(resource_link_list__study_programs__in=self.accessible_study_programs())

    def __str__(self):
        return str(self.user.options)

    class Meta:
        verbose_name = _('bidragsyter')
        verbose_name_plural = _('bidragsytere')

class Options(models.Model):
    """
    Fields that should be available to the student. It is important to notice that Options is a stand in for
    Semester in the template rendering of the userpage view, and thus needs many of the same methods and member
    variables
    """
    user = models.OneToOneField(
        User,
        blank=False,
        null=False,
        on_delete=models.CASCADE,
        related_name='options',
        verbose_name=_('bruker')
    )
    self_chosen_courses = models.ManyToManyField(
        Course,
        default=None,
        related_name='students',
        blank=True,
        verbose_name=_('fag'),
        help_text=_('Dette er fagene som vises på profilsiden din.'),
    )
    # We allow null values for last_user_modification, as null indicates that
    # the user has not yet visited the options view. Elsewise, the date
    # indicates the last time the user has confirmed that these options are
    # correct.
    last_user_modification = models.DateField(
        blank=True,
        null=True,
        default=None,
        verbose_name=_('sist redigert'),
    )

    # The following dataporten field saves the last observed data from dataporten.
    # This is not intended for authentification, but rather for knowing when the
    # dataporten data has changed, such that 'self_chosen_courses' can be updated.
    # See .adapters.sync_options_of_user_with_dataporten
    active_dataporten_courses = models.ManyToManyField(
        Course,
        default=None,
        related_name='active_students',
        blank=True,
        verbose_name=_('dataporten fag'),
        help_text=_('Aktive fag fra dataporten'),
    )
    calendar_name = models.CharField(
        _('1024-kalendernavn'),
        max_length=60,
        blank=True,
        null=True,
        default=None,
        help_text=_('Tast inn ditt kalendernavn på ntnu.1024.no.')
    )

    @property
    def study_program(self):
        return StudyProgram.objects.get(slug=DEFAULT_STUDY_PROGRAM_SLUG)

    @property
    def main_profile(self):
        return MainProfile.objects.none()

    def get_archive_url(self):
        # Returns the link to the root of the archive site
        return subdomains.utils.reverse(
            'semesterpage-homepage',
            subdomain='arkiv',
        )

    @property
    def courses(self):
        """
        The courses that should be displayed to the user on their homepage.
        """
        return self.self_chosen_courses.all()

    def check_access(self, user):
        return self.user.username == user.username

    def get_admin_url(self):
        info = (self._meta.app_label, self._meta.model_name)
        return reverse('admin:%s_%s_change' % info, args=(self.pk,))

    def get_absolute_url(self):
        return reverse('semesterpage-studyprogram', args=(self.user.username,))

    def __str__(self):
        return self.user.username.title()

    class Meta(Link.Meta):
        verbose_name = _('instillinger')
        verbose_name_plural = _('instillinger')
        ordering = ('user__username',)

redpajama_set_name: RedPajamaGithub


Police have stopped using the most controversial part of the city's new noise ordinance that allows officers to impound cars if they can hear its stereo more than 10 yards away.
The ordinance made Sarasota the most restrictive, and costly, place in Florida to blast music from a vehicle. But the temporary suspension of seizing vehicles eliminates the most costly part of the law, possibly for another year.
The city attorney says the move was made to limit the legal exposure for the city, which faces a lawsuit from two drivers who are seeking damages in federal court.
"We just didn't want to give rise to numerous claims," City Attorney Robert Fournier said.
Sarasota police had seized 11 vehicles and issued 60 tickets under the ordinance in its first six months, even though they said they would give drivers some leeway.
Police said they have not seized any vehicles since the lawsuits were filed. Fournier recommended not impounding any more vehicles until the case is resolved. The case is set for trial in November 2010.
The American Civil Liberties Union and a local criminal defense attorney are representing the two drivers.
Mark Cannon of Sarasota had his car impounded for loud music, and Latrese Allen was ticketed when she was singing along with a tribute song to a friend who had died.
The lawsuit asks the courts to stop enforcement of the ordinance. It also seeks damages for the cost of the seizure and fines incurred by those whose vehicles had been impounded, as well as attorney fees.
The ACLU argues the ordinance is unconstitutional because it gives too much power to officers, and people have no way to know how to follow it.
The law states that the stereo must be "plainly audible" at 25 feet, but that can change based on the officer's hearing, ambient noise and type of music playing at the moment, attorney Andrea Mogensen has said.
The first-time offense is a $74 fine. The additional impound fee is $125.
The legal battle over the controversial ordinance is not unexpected, and the City Commission discussed the likelihood of a challenge when voting to enact it.
Defending the lawsuit could cost taxpayers up to $20,000 in attorney fees, the commission was told.


text:
Leduc-Beaumont-Devon was a provincial electoral district in Alberta, Canada mandated to return a single member to the Legislative Assembly of Alberta using the first-past-the-post method of voting from 2004 to 2012.

The district and its antecedents have traditionally been a stronghold of votes for the Progressive Conservatives over the last few decades.

The district was created in the 2004 boundary re-distribution from the old Leduc electoral district. It was named after the City of Leduc and Leduc County as well as the towns of Beaumont and Devon. The district is mixed urban and rural as it also contains large rural portions.

Leduc-Beaumont-Devon history

Boundary history

Electoral history
Leduc-Beaumont-Devon electoral district was created from the old district of Leduc in the 2003 electoral boundary re-distribution. The riding remained similar with only minor changes but added Beaumont and Devon to the name.

The first election saw Progressive Conservative candidate George Rogers win over 50% of the vote over a slate of five other candidates. He ran for re-election to a second term in 2008 and won the district with a landslide.

The Leduc-Beaumont-Devon electoral district was dissolved in the 2010 electoral boundary re-distribution, the western portion was reformed as the Leduc-Beaumont electoral district, while the eastern portion was included in Battle River-Wainwright electoral district.

Election results

2004 general election

2008 general election

Alberta Senate election

2004 Senate nominee election district results
Voters had the option of selecting 4 Candidates on the Ballot

2004 Student Vote

On November 19, 2004 a Student Vote was conducted at participating Alberta schools to parallel the 2004 Alberta general election results. The vote was designed to educate students and simulate the electoral process for persons who have not yet reached the legal majority. The vote was conducted in 80 of the 83 provincial electoral districts with students voting for actual election candidates. Schools with a large student body that reside in another electoral district had the option to vote for candidates outside of the electoral district then where they were physically located.

See also
List of Alberta provincial electoral districts

References

External links
Elections Alberta
The Legislative Assembly of Alberta

Former provincial electoral districts of Alberta
Leduc, Alberta

redpajama_set_name: RedPajamaWikipedia


text:
"""blockmodel.py: Block Model element definitions"""
import numpy as np
import properties

from .base import ProjectElement
from .attribute import ArrayInstanceProperty

class BaseBlockModel(ProjectElement):
    """Basic orientation properties for all block models"""

    axis_u = properties.Vector3(
        "Vector orientation of u-direction",
        default="X",
        length=1,
    )
    axis_v = properties.Vector3(
        "Vector orientation of v-direction",
        default="Y",
        length=1,
    )
    axis_w = properties.Vector3(
        "Vector orientation of w-direction",
        default="Z",
        length=1,
    )
    corner = properties.Vector3(
        "Corner of the block model relative to Project coordinate reference system",
        default=[0.0, 0.0, 0.0],
    )

    @properties.validator
    def _validate_axes(self):
        """Check if mesh content is built correctly"""
        if not (
            np.abs(self.axis_u.dot(self.axis_v) < 1e-6)
            and np.abs(self.axis_v.dot(self.axis_w) < 1e-6)
            and np.abs(self.axis_w.dot(self.axis_u) < 1e-6)
        ):
            raise ValueError("axis_u, axis_v, and axis_w must be orthogonal")
        return True

    @property
    def parent_block_count(self):
        """Computed property for number of parent blocks

        This is required for ijk indexing. For tensor and regular block
        models, all the blocks are considered parent blocks.
        """
        raise NotImplementedError()

    def ijk_to_index(self, ijk):
        """Return index for single ijk triple"""
        return self.ijk_array_to_indices([ijk])[0]

    def ijk_array_to_indices(self, ijk_array):
        """Return an array of indices for a list of ijk triples"""
        blocks = self.parent_block_count
        if not blocks:
            raise AttributeError("parent_block_count is required to calculate index")
        if not isinstance(ijk_array, (list, tuple, np.ndarray)):
            raise ValueError("ijk_array must be a list of length-3 ijk values")
        ijk_array = np.array(ijk_array)
        if len(ijk_array.shape) != 2 or ijk_array.shape[1] != 3:
            raise ValueError("ijk_array must be n x 3 array")
        if not np.array_equal(ijk_array, ijk_array.astype(np.uint32)):
            raise ValueError("ijk values must be non-negative integers")
        if np.any(np.max(ijk_array, axis=0) >= blocks):
            raise ValueError(
                "ijk must be less than parent_block_count in each dimension"
            )
        index = np.ravel_multi_index(
            multi_index=ijk_array.T,
            dims=blocks,
            order="F",
        )
        return index

    def index_to_ijk(self, index):
        """Return ijk triple for single index"""
        return self.indices_to_ijk_array([index])[0]

    def indices_to_ijk_array(self, indices):
        """Return an array of ijk triples for an array of indices"""
        blocks = self.parent_block_count
        if not blocks:
            raise AttributeError(
                "parent_block_count is required to calculate ijk values"
            )
        if not isinstance(indices, (list, tuple, np.ndarray)):
            raise ValueError("indices must be a list of index values")
        indices = np.array(indices)
        if len(indices.shape) != 1:
            raise ValueError("indices must be 1D array")
        if not np.array_equal(indices, indices.astype(np.uint64)):
            raise ValueError("indices values must be non-negative integers")
        if np.max(indices) >= np.prod(blocks):
            raise ValueError("indices must be less than total number of parent blocks")
        ijk = np.unravel_index(
            indices=indices,
            shape=blocks,
            order="F",
        )
        ijk_array = np.c_[ijk[0], ijk[1], ijk[2]]
        return ijk_array

class TensorGridBlockModel(BaseBlockModel):
    """Block model with variable spacing in each dimension"""

    schema = "org.omf.v2.element.blockmodel.tensorgrid"

    tensor_u = properties.Array(
        "Tensor cell widths, u-direction",
        shape=("*",),
        dtype=float,
    )
    tensor_v = properties.Array(
        "Tensor cell widths, v-direction",
        shape=("*",),
        dtype=float,
    )
    tensor_w = properties.Array(
        "Tensor cell widths, w-direction",
        shape=("*",),
        dtype=float,
    )

    _valid_locations = ("vertices", "cells", "parent_blocks")

    def location_length(self, location):
        """Return correct attribute length based on location"""
        if location == "vertices":
            return self.num_nodes
        return self.num_cells

    def _tensors_defined(self):
        """Check if all tensors are defined"""
        tensors = [self.tensor_u, self.tensor_v, self.tensor_w]
        return all((tensor is not None for tensor in tensors))

    @property
    def num_nodes(self):
        """Number of nodes (vertices)"""
        if not self._tensors_defined():
            return None
        nodes = (
            (len(self.tensor_u) + 1)
            * (len(self.tensor_v) + 1)
            * (len(self.tensor_w) + 1)
        )
        return nodes

    @property
    def num_cells(self):
        """Number of cells"""
        if not self._tensors_defined():
            return None
        cells = len(self.tensor_u) * len(self.tensor_v) * len(self.tensor_w)
        return cells

    @property
    def parent_block_count(self):
        """Number of parent blocks equals number of blocks"""
        if not self._tensors_defined():
            return None
        blocks = [len(self.tensor_u), len(self.tensor_v), len(self.tensor_w)]
        return blocks

class RegularBlockModel(BaseBlockModel):
    """Block model with constant spacing in each dimension"""

    schema = "org.omf.v2.elements.blockmodel.regular"

    block_count = properties.List(
        "Number of blocks along u, v, and w axes",
        properties.Integer("", min=1),
        min_length=3,
        max_length=3,
    )
    block_size = properties.List(
        "Size of blocks in the u, v, and w dimensions",
        properties.Float("", min=0),
        min_length=3,
        max_length=3,
    )
    cbc = ArrayInstanceProperty(
        "Compressed block count - for regular block models this must "
        "have length equal to the product of block_count and all values "
        "must be 1 (if attributes exist on the block) or 0; the default "
        "is an array of 1s",
        shape=("*",),
        dtype=(int, bool),
    )

    _valid_locations = ("cells", "parent_blocks")

    @properties.Array(
        "Compressed block index - used for indexing attributes "
        "into the block model; must have length equal to the "
        "product of block_count plus 1 and monotonically increasing",
        shape=("*",),
        dtype=int,
        coerce=False,
    )
    def cbi(self):
        """Compressed block index"""
        if self.cbc is None:
            return None
        # Recalculating the sum on the fly is faster than checking md5
        cbi = np.concatenate(
            [
                np.array([0], dtype=np.uint32),
                np.cumsum(self.cbc, dtype=np.uint32),
            ]
        )
        return cbi

    @properties.validator("block_size")
    def _validate_size_is_not_zero(self, change):
        """Ensure block sizes are non-zero"""
        if 0 in change["value"]:
            raise properties.ValidationError(
                "Block size cannot be 0",
                prop="block_size",
                instance=self,
                reason="invalid",
            )

    @properties.validator("cbc")
    def validate_cbc(self, change):
        """Ensure cbc is correct size and values"""
        value = change["value"]
        if self.block_count and len(value.array) != np.prod(self.block_count):
            raise properties.ValidationError(
                "cbc must have length equal to the product of block_count",
                prop="cbc",
                instance=self,
                reason="invalid",
            )
        if np.max(value.array) > 1 or np.min(value.array) < 0:
            raise properties.ValidationError(
                "cbc must have only values 0 or 1",
                prop="cbc",
                instance=self,
                reason="invalid",
            )

    @property
    def num_cells(self):
        """Number of cells from last value in the compressed block index"""
        cbi = self.cbi
        if cbi is None:
            return None
        return cbi[-1]  # pylint: disable=E1136

    def location_length(self, location):
        """Return correct attribute length based on location"""
        return self.num_cells

    @property
    def parent_block_count(self):
        """Number of parent blocks equals number of blocks"""
        return self.block_count

    def reset_cbc(self):
        """Reset cbc to no sub-blocks"""
        if not self.block_count:
            raise ValueError("cannot reset cbc until block_count is set")
        cbc_len = np.prod(self.block_count)
        self.cbc = np.ones(cbc_len, dtype=np.bool)

class RegularSubBlockModel(BaseBlockModel):
    """Block model with one level of sub-blocking possible in each parent block"""

    schema = "org.omf.v2.elements.blockmodel.sub"

    parent_block_count = properties.List(
        "Number of parent blocks along u, v, and w axes",
        properties.Integer("", min=1),
        min_length=3,
        max_length=3,
    )
    sub_block_count = properties.List(
        "Number of sub blocks in each parent block, along u, v, and w axes",
        properties.Integer("", min=1),
        min_length=3,
        max_length=3,
    )
    parent_block_size = properties.List(
        "Size of parent blocks in the u, v, and w dimensions",
        properties.Float("", min=0),
        min_length=3,
        max_length=3,
    )
    cbc = ArrayInstanceProperty(
        "Compressed block count - for regular sub block models this must "
        "have length equal to the product of parent_block_count and all "
        "values must be the product of sub_block_count (if attributes "
        "exist on the sub blocks), 1 (if attributes exist on the parent "
        "block) or 0; the default is an array of 1s",
        shape=("*",),
        dtype=(int, bool),
    )

    _valid_locations = ("parent_blocks", "sub_blocks")

    @properties.Array(
        "Compressed block index - used for indexing attributes "
        "into the sub block model; must have length equal to the "
        "product of parent_block_count plus 1 and monotonically increasing",
        shape=("*",),
        dtype=int,
        coerce=False,
    )
    def cbi(self):
        """Compressed block index"""
        if self.cbc is None:
            return None
        cbi = np.concatenate(
            [
                np.array([0], dtype=np.uint64),
                np.cumsum(self.cbc, dtype=np.uint64),
            ]
        )
        return cbi

    @properties.List(
        "Size of sub blocks in the u, v, and w dimensions",
        properties.Float("", min=0),
        min_length=3,
        max_length=3,
    )
    def sub_block_size(self):
        """Computed sub block size"""
        if not self.sub_block_count or not self.parent_block_size:
            return None
        return self.parent_block_size / np.array(self.sub_block_count)

    @properties.validator("parent_block_size")
    def _validate_size_is_not_zero(self, change):
        """Ensure block sizes are non-zero"""
        if 0 in change["value"]:
            raise properties.ValidationError(
                "Block size cannot be 0",
                prop="parent_block_size",
                instance=self,
                reason="invalid",
            )

    @properties.validator("cbc")
    def validate_cbc(self, change):
        """Ensure cbc is correct size and values"""
        value = change["value"]
        if not self.parent_block_count:
            pass
        elif len(value.array) != np.prod(self.parent_block_count):
            raise properties.ValidationError(
                "cbc must have length equal to the product of parent_block_count",
                prop="cbc",
                instance=self,
                reason="invalid",
            )
        if not self.sub_block_count:
            pass
        elif np.any(
            (value.array != 1)
            & (value.array != 0)
            & (value.array != np.prod(self.sub_block_count))
        ):
            raise properties.ValidationError(
                "cbc must have only values of prod(sub_block_count), 1, or 0",
                prop="cbc",
                instance=self,
                reason="invalid",
            )

    @property
    def num_cells(self):
        """Number of cells from last value in the compressed block index"""
        cbi = self.cbi
        if cbi is None:
            return None
        return cbi[-1]  # pylint: disable=E1136

    def location_length(self, location):
        """Return correct attribute length based on location"""
        if location == "parent_blocks":
            return np.sum(self.cbc.array.astype(np.bool))
        return self.num_cells

    def reset_cbc(self):
        """Reset cbc to no sub-blocks"""
        if not self.parent_block_count:
            raise ValueError("cannot reset cbc until parent_block_count is set")
        cbc_len = np.prod(self.parent_block_count)
        self.cbc = np.ones(cbc_len, dtype=np.uint32)

    def refine(self, ijk):
        """Refine parent blocks at a single ijk or a list of multiple ijks"""
        if self.cbc is None or not self.sub_block_count:
            raise ValueError(
                "Cannot refine sub block model without specifying number "
                "of parent and sub blocks"
            )
        try:
            inds = self.ijk_array_to_indices(ijk)
        except ValueError:
            inds = self.ijk_to_index(ijk)
        self.cbc.array[inds] = np.prod(self.sub_block_count)  # pylint: disable=E1137

class OctreeSubBlockModel(BaseBlockModel):
    """Block model where sub-blocks follow an octree pattern in each parent"""

    schema = "org.omf.v2.elements.blockmodel.octree"

    max_level = 8  # Maximum times blocks can be subdivided
    level_bits = 4  # Enough for 0 to 8 refinements

    parent_block_count = properties.List(
        "Number of parent blocks along u, v, and w axes",
        properties.Integer("", min=1),
        min_length=3,
        max_length=3,
    )
    parent_block_size = properties.List(
        "Size of parent blocks in the u, v, and w dimensions",
        properties.Float("", min=0),
        min_length=3,
        max_length=3,
    )
    cbc = ArrayInstanceProperty(
        "Compressed block count - for octree sub block models this must "
        "have length equal to the product of parent_block_count and each "
        "value must be equal to the number of octree sub blocks within "
        "the corresponding parent block (since max level is 8 in each "
        "dimension, the max number of sub blocks in a parent is (2^8)^3), "
        "1 (if parent block is not subdivided) or 0 (if parent block is "
        "unused); the default is an array of 1s",
        shape=("*",),
        dtype=(int, bool),
    )
    zoc = ArrayInstanceProperty(
        "Z-order curves - sub block location pointer and level, encoded as bits",
        shape=("*",),
        dtype=int,
    )

    _valid_locations = ("parent_blocks", "sub_blocks")

    @properties.Array(
        "Compressed block index - used for indexing attributes "
        "into the sub block model; must have length equal to the "
        "product of parent_block_count plus 1 and monotonically increasing",
        shape=("*",),
        dtype=int,
        coerce=False,
    )
    def cbi(self):
        """Compressed block index"""
        if self.cbc is None:
            return None
        cbi = np.concatenate(
            [
                np.array([0], dtype=np.uint64),
                np.cumsum(self.cbc, dtype=np.uint64),
            ]
        )
        return cbi

    @properties.validator("cbc")
    def validate_cbc(self, change):
        """Ensure cbc is correct size and values"""
        value = change["value"]
        if not self.parent_block_count:
            pass
        elif len(value.array) != np.prod(self.parent_block_count):
            raise properties.ValidationError(
                "cbc must have length equal to the product of parent_block_count",
                prop="cbc",
                instance=self,
                reason="invalid",
            )
        if np.max(value.array) > 8 ** 8 or np.min(value.array) < 0:
            raise properties.ValidationError(
                "cbc must have values between 0 and 8^8",
                prop="cbc",
                instance=self,
                reason="invalid",
            )

    @properties.validator("zoc")
    def validate_zoc(self, change):
        """Ensure Z-order curve array is correct length and valid values"""
        value = change["value"]
        cbi = self.cbi
        if cbi is None:
            pass
        elif len(value.array) != cbi[-1]:
            raise properties.ValidationError(
                "zoc must have length equal to maximum compressed block index value",
                prop="zoc",
                instance=self,
                reason="invalid",
            )
        max_curve_value = 268435448  # -> 0b1111111111111111111111111000
        if np.max(value.array) > max_curve_value or np.min(value.array) < 0:
            raise properties.ValidationError(
                "zoc must have values between 0 and 8^8",
                prop="cbc",
                instance=self,
                reason="invalid",
            )

    @property
    def num_cells(self):
        """Number of cells from last value in the compressed block index"""
        cbi = self.cbi
        if cbi is None:
            return None
        return cbi[-1]  # pylint: disable=E1136

    def location_length(self, location):
        """Return correct attribute length based on location"""
        if location == "parent_blocks":
            return np.sum(self.cbc.array.astype(np.bool))
        return self.num_cells

    def reset_cbc(self):
        """Reset cbc to no sub-blocks"""
        if not self.parent_block_count:
            raise ValueError("cannot reset cbc until parent_block_count is set")
        cbc_len = np.prod(self.parent_block_count)
        self.cbc = np.ones(cbc_len, dtype=np.uint32)

    def reset_zoc(self):
        """Reset zoc to no sub-blocks"""
        if not self.parent_block_count:
            raise ValueError("cannot reset zoc until parent_block_count is set")
        zoc_len = np.prod(self.parent_block_count)
        self.zoc = np.zeros(zoc_len, dtype=np.int32)

    @staticmethod
    def bitrange(index, width, start, end):
        """Extract a bit range as an integer

        [start, end) is inclusive lower bound, exclusive upper bound.
        """
        return index >> (width - end) & ((2 ** (end - start)) - 1)

    @classmethod
    def get_curve_value(cls, pointer, level):
        """Get Z-order curve value from pointer and level

        Values range from 0 (pointer=[0, 0, 0], level=0) to
        268435448 (pointer=[255, 255, 255], level=8).
        """
        idx = 0
        iwidth = cls.max_level * 3
        for i in range(iwidth):
            bitoff = cls.max_level - (i // 3) - 1
            poff = 3 - (i % 3) - 1
            bitrange = (
                cls.bitrange(
                    index=pointer[3 - 1 - poff],
                    width=cls.max_level,
                    start=bitoff,
                    end=bitoff + 1,
                )
                << i
            )
            idx |= bitrange
        return (idx << cls.level_bits) + level

    @classmethod
    def get_pointer(cls, curve_value):
        """Get pointer value from Z-order curve value

        Pointer values are length-3 with values between 0 and 255
        """
        index = curve_value >> cls.level_bits
        pointer = [0] * 3
        iwidth = cls.max_level * 3
        for i in range(iwidth):
            bitrange = (
                cls.bitrange(
                    index=index,
                    width=iwidth,
                    start=i,
                    end=i + 1,
                )
                << (iwidth - i - 1) // 3
            )
            pointer[i % 3] |= bitrange
        pointer.reverse()
        return pointer

    @classmethod
    def get_level(cls, curve_value):
        """Get level value from Z-order curve value

        Level comes from the last 4 bits, with values between 0 and 8
        """
        return curve_value & (2 ** cls.level_bits - 1)

    @classmethod
    def level_width(cls, level):
        """Width of a level, in bits

        Max level of 8 has level width of 1; min level of 0 has level
        width of 256.
        """
        if not 0 <= level <= cls.max_level:
            raise ValueError("level must be between 0 and {}".format(cls.max_level))
        return 2 ** (cls.max_level - level)

    def refine(self, index, ijk=None, refinements=1):
        """Subdivide at the given index

        .. note::
           This method is for demonstration only.
           It is impractical and not intended to build an octree blockmodel
           using this method alone.

        If ijk is provided, index is relative to ijk parent block.
        Otherwise, index is relative to the entire block model.

        By default, blocks are refined a single level, from 1 sub-block
        to 8 sub-blocks. However, a greater number of refinements may be
        specified, where the final number of sub-blocks equals
        (2**refinements)**3.
        """
        cbi = self.cbi
        if ijk is not None:
            index += int(cbi[self.ijk_to_index(ijk)])
        parent_index = np.sum(index >= cbi) - 1  # pylint: disable=W0143
        if not 0 <= index < len(self.zoc):
            raise ValueError("index must be between 0 and {}".format(len(self.zoc)))

        curve_value = self.zoc[index]
        level = self.get_level(curve_value)
        if not 0 <= refinements <= self.max_level - level:
            raise ValueError(
                "refinements must be between 0 and {}".format(self.max_level - level)
            )
        new_width = self.level_width(level + refinements)

        new_pointers = np.indices([2 ** refinements] * 3)
        new_pointers = new_pointers.reshape(3, (2 ** refinements) ** 3).T
        new_pointers = new_pointers * new_width

        pointer = self.get_pointer(curve_value)
        new_pointers = new_pointers + pointer

        new_curve_values = sorted(
            [
                self.get_curve_value(pointer, level + refinements)
                for pointer in new_pointers
            ]
        )

        self.cbc.array[parent_index] += len(new_curve_values) - 1
        self.zoc = np.concatenate(
            [
                self.zoc[:index],
                new_curve_values,
                self.zoc[index + 1 :],
            ]
        )

class ArbitrarySubBlockModel(BaseBlockModel):
    """Block model with arbitrary, variable sub-blocks"""

    parent_block_count = properties.List(
        "Number of parent blocks along u, v, and w axes",
        properties.Integer("", min=1),
        min_length=3,
        max_length=3,
    )
    parent_block_size = properties.List(
        "Size of parent blocks in the u, v, and w dimensions",
        properties.Float("", min=0),
        min_length=3,
        max_length=3,
    )
    cbc = ArrayInstanceProperty(
        "Compressed block count - for arbitrary sub block models this must "
        "have length equal to the product of parent_block_count and each "
        "value must be equal to the number of sub blocks within the "
        "corresponding parent block, 1 (if attributes exist on the parent "
        "block) or 0; the default is an array of 1s",
        shape=("*",),
        dtype=(int, bool),
    )
    sub_block_corners = ArrayInstanceProperty(
        "Block corners normalized 0-1 relative to parent block",
        shape=("*", 3),
        dtype=float,
    )
    sub_block_sizes = ArrayInstanceProperty(
        "Block widths normalized 0-1 relative to parent block",
        shape=("*", 3),
        dtype=float,
    )

    _valid_locations = ("parent_blocks", "sub_blocks")

    @properties.Array(
        "Compressed block index - used for indexing attributes "
        "into the sub block model; must have length equal to the "
        "product of parent_block_count plus 1 and monotonically increasing",
        shape=("*",),
        dtype=int,
        coerce=False,
    )
    def cbi(self):
        """Compressed block index"""
        if self.cbc is None:
            return None
        cbi = np.r_[
            np.array([0], dtype=np.uint64),
            np.cumsum(self.cbc, dtype=np.uint64),
        ]
        return cbi

    @properties.Array(
        "Block centroids normalized 0-1 relative to parent block",
        shape=("*", 3),
        dtype=float,
        coerce=False,
    )
    def sub_block_centroids(self):
        """Block centroids normalized 0-1 relative to parent block

        Computed from sub_block_corners and sub_block_sizes
        """
        if self.sub_block_corners is None or self.sub_block_sizes is None:
            return None
        return self.sub_block_corners.array + self.sub_block_sizes.array / 2

    @properties.Array(
        "Block corners relative to parent block",
        shape=("*", 3),
        dtype=float,
        coerce=False,
    )
    def sub_block_corners_absolute(self):
        """Block corners relative to parent block

        Computed from sub_block_corners and sub_block_sizes
        """
        if self.sub_block_corners is None or self.parent_block_size is None:
            return None
        cbc = self.cbc
        all_indices = np.array(range(len(cbc)), dtype=np.uint64)
        unique_parent_ijks = self.indices_to_ijk_array(all_indices)
        parent_ijks = np.repeat(unique_parent_ijks, cbc, axis=0)
        corners = parent_ijks + self.sub_block_corners
        return corners * self.parent_block_size

    @properties.Array(
        "Block centroids relative to parent block",
        shape=("*", 3),
        dtype=float,
        coerce=False,
    )
    def sub_block_centroids_absolute(self):
        """Block centroids relative to parent block

        Computed from sub_block_corners and sub_block_sizes
        """
        if self.sub_block_centroids is None or self.parent_block_size is None:
            return None
        cbc = self.cbc
        all_indices = np.array(range(len(cbc)), dtype=np.uint64)
        unique_parent_ijks = self.indices_to_ijk_array(all_indices)
        parent_ijks = np.repeat(unique_parent_ijks, cbc, axis=0)
        centroids = parent_ijks + self.sub_block_centroids
        return centroids * self.parent_block_size

    @properties.Array(
        "Block widths relative to parent block",
        shape=("*", 3),
        dtype=float,
        coerce=False,
    )
    def sub_block_sizes_absolute(self):
        """Block widths relative to parent block

        Computed from sub_block_corners and sub_block_sizes
        """
        if self.sub_block_sizes is None or self.parent_block_size is None:
            return None
        return self.sub_block_sizes.array * self.parent_block_size

    @properties.validator("parent_block_size")
    def _validate_size_is_not_zero(self, change):
        """Ensure parent blocks are non-zero"""
        if 0 in change["value"]:
            raise properties.ValidationError(
                "Block size cannot be 0",
                prop="parent_block_size",
                instance=self,
                reason="invalid",
            )

    @properties.validator("cbc")
    def validate_cbc(self, change):
        """Ensure cbc is correct size and values"""
        value = change["value"]
        if not self.parent_block_count:
            pass
        elif len(value.array) != np.prod(self.parent_block_count):
            raise properties.ValidationError(
                "cbc must have length equal to the product of parent_block_count",
                prop="cbc",
                instance=self,
                reason="invalid",
            )
        if np.min(value.array) < 0:
            raise properties.ValidationError(
                "cbc values must be non-negative",
                prop="cbc",
                instance=self,
                reason="invalid",
            )
        return value

    def validate_sub_block_attributes(self, value, prop_name):
        """Ensure value is correct length"""
        cbi = self.cbi
        if cbi is None:
            return value
        if len(value) != cbi[-1]:
            raise properties.ValidationError(
                "{} attributes must have length equal to "
                "total number of sub blocks".format(prop_name),
                prop=prop_name,
                instance=self,
                reason="invalid",
            )
        return value

    @properties.validator("sub_block_corners")
    def _validate_sub_block_corners(self, change):
        """Validate sub block corners array is correct length"""
        change["value"] = self.validate_sub_block_attributes(
            change["value"], "sub_block_corners"
        )

    @properties.validator("sub_block_sizes")
    def _validate_sub_block_sizes(self, change):
        """Validate sub block size array is correct length and positive"""
        value = self.validate_sub_block_attributes(change["value"], "sub_block_sizes")
        if np.min(value.array) <= 0:
            raise properties.ValidationError(
                "sub block sizes must be positive",
                prop="sub_block_sizes",
                instance=self,
                reason="invalid",
            )

    @property
    def num_cells(self):
        """Number of cells from last value in the compressed block index"""
        cbi = self.cbi
        if cbi is None:
            return None
        return cbi[-1]  # pylint: disable=E1136

    def location_length(self, location):
        """Return correct attribute length based on location"""
        if location == "parent_blocks":
            return np.sum(self.cbc.array.astype(np.bool))
        return self.num_cells

    def reset_cbc(self):
        """Reset cbc to no sub-blocks"""
        if not self.parent_block_count:
            raise ValueError("cannot reset cbc until parent_block_count is set")
        cbc_len = np.prod(self.parent_block_count)
        self.cbc = np.ones(cbc_len, dtype=np.uint32)


Its a beautiful thing, watching a brand evolve, but not everybody has what it takes to take their brand to the next level...enter Hollie Philippa.
Taking the fitness world by storm, she does things her own way, and stands by her brand to the very end.
What we love about Hollie Philippa, is that she isn't afraid to say NO.
Its hard to JUST BE YOU with todays social media trying to mould and change each and every one of us into a Kardashian Clone... and so by stripping everything back... to the very beginning... what you will find is the TRUE YOU.
And that my friends is what sets you apart from everybody else, so its worth sending your energy in that direction.
We have missed this little sunbeam, and have loved watching her brand grow from strength to strength over the years as well as sharing photoshoot after photoshoot with her (and a few very strong coffee's along the way).
You only get one life time, and if you stand by your brand, you can build an empire and leave a legacy behind... and lets be honest... who doest want to do that?